Contributing

Architecture guide

TypeScript UI has a dual architecture: a documentation site built with Next.js 16 and Markdoc, and an npm package containing reusable React components. This guide explains how both layers work together.


Documentation site architecture

The documentation site is a Next.js 16 application using the App Router. It compiles Markdoc pages into React Server Components and serves them at build time.

How pages are compiled

  1. Markdoc pages — Documentation lives in .md files under src/app/ with YAML frontmatter
  2. Layered webpack confignext.config.mjs re-registers the Markdoc rule per bundle layer (rsc, ssr, app-pages-browser) so pages compile as Server Components, not client code
  3. Tags as components — Custom Markdoc tags (src/markdoc/tags.js) render React components: {% callout %}, {% buttons %}, {% ctasections %}, etc.
  4. DocsLayout — All Markdoc documents render through src/components/DocsLayout.tsx, which adds a table of contents and sidebar navigation

Table of contents generation

The src/lib/sections.ts module exports a collectSections function that extracts H2 and H3 headings from Markdoc nodes at build time. Each documentation page gets a table of contents sidebar showing its sections. H3 headings must follow an H2, or an error is thrown.

Search indexing

The src/markdoc/search.mjs webpack loader runs at build time to build a FlexSearch index of all documentation pages. The index is embedded in the build output, enabling fast client-side search via the ⌘K command palette.

Key configuration

next.config.mjs — Combines three middleware:

  • withMarkdoc — Registers the Markdoc loader
  • withSearch — Builds the FlexSearch index
  • withLayeredMarkdocRule — Ensures pages compile as Server Components by layer

tailwind.config.ts — Uses selector strategy for dark mode (adds a dark class to the root element) and includes a custom typography plugin for prose styling.


npm package architecture

The npm package is built with tsup and distributed as ESM, CJS, and TypeScript definitions. Entry points are organized by section group.

Build process

tsup.config.ts — Defines one entry point per section group:

// tsup.config.ts excerpt
export default defineConfig({
  entry: {
    'features/index': 'src/components/ui/features/index.ts',
    'cta/index': 'src/components/ui/cta/index.ts',
    'pricing/index': 'src/components/ui/pricing/index.ts',
    // ... 10+ more groups
  },
  format: ['cjs', 'esm'],
  dts: true,
});

This generates files like dist/features/index.js, dist/features/index.mjs, and dist/features/index.d.ts.

Exports

package.json — Each group has a separate export:

{
  "exports": {
    "./features": {
      "types": "./dist/features/index.d.ts",
      "import": "./dist/features/index.mjs",
      "require": "./dist/features/index.js"
    },
    "./cta": { ... },
    "./pricing": { ... }
  }
}

Users can import from a specific group (import { FeatureGrid } from 'typescript-ui/features') to only bundle what they need.

Externals

Dependencies listed in tsup.config.ts as external are not bundled:

external: ['react', 'react-dom', 'next', 'next/image', 'next/link', 'clsx']

This keeps the package size small and avoids duplicate React instances.


Component tiers

TypeScript UI has two types of components with different distribution models:

Primitives (copy-paste)

Location: src/components/ui/shadcn/

  • Low-level, single-purpose components built on Radix UI (accordion, alert, badge, button, etc.)
  • Published as documentation with source code, not as npm package exports
  • Users copy the .tsx file into their project and customize it
  • Includes 50+ primitives

Why copy-paste?

  • Primitives are highly customizable—copy-paste lets users modify them directly
  • Reduces decision fatigue (users control the CSS)
  • Smaller bundle sizes (no unused variants)
  • Full type safety (modifications are in their own project)

Sections (npm package)

Location: src/components/ui/<group>/

  • High-level, feature-complete components (CTASimple, FeatureGrid, PricingTiered, etc.)
  • Published to npm as separate entry points per group
  • Users import and use directly: import { CTASimple } from 'typescript-ui/cta'
  • Includes 60+ sections across 13 groups (features, cta, pricing, testimonials, faq, stats, logos, team, headers, footers, auth, contact, avatars, buttons, social icons)

Why npm package?

  • Sections are feature-complete and less frequently customized
  • Bundling them reduces bundle size (tree-shake unused components)
  • Easier to update across projects
  • Full TypeScript support

Styling architecture

CSS variables (theming)

src/styles/tailwind.css defines CSS variables for every color, border radius, and spacing token used by the design system:

:root {
  --background: 0 0% 100%;
  --foreground: 0 0% 9%;
  --primary: 0 0% 9%;
  --primary-foreground: 0 0% 98%;
  /* ... more variables */
}

.dark {
  --primary: 0 0% 98%;
  --primary-foreground: 0 0% 9%;
  /* ... more dark mode overrides */
}

Components use these via Tailwind's HSL variables: bg-primary, text-primary-foreground, etc.

Dark mode

Dark mode is implemented via the selector strategy in tailwind.config.ts. The dark class is added to the root element (usually via next-themes), and components use dark: prefixes for dark mode styles:

// Component with dark mode support
export function MyComponent() {
  return (
    <div className="bg-slate-100 dark:bg-slate-900 text-slate-900 dark:text-white">
      Content
    </div>
  );
}

Dark mode is not optional—all components include full dark mode support.

Tailwind content paths

Components use Tailwind classes, so tailwind.config.ts includes the npm package in content paths:

content: [
  './src/**/*.{js,ts,jsx,tsx,md}',
]

And in user projects:

content: [
  './node_modules/typescript-ui/dist/**/*.{js,mjs}',
]

No runtime CSS

All styling is static Tailwind CSS—no CSS-in-JS, emotion, styled-components, or runtime style injection. This keeps bundle sizes minimal and improves performance.


Directory structure

typescript-ui/
├── src/
│   ├── app/                          # Next.js App Router
│   │   ├── docs/                     # Documentation pages (.md files)
│   │   │   ├── installation/
│   │   │   ├── theming/
│   │   │   ├── how-to-contribute/
│   │   │   ├── architecture-guide/
│   │   │   └── design-principles/
│   │   ├── components/               # Component showcase pages
│   │   │   ├── shadcn/               # Primitive component showcases
│   │   │   │   ├── button/page.md
│   │   │   │   └── ...
│   │   │   └── auth/page.md           # Section group showcases
│   │   └── layout.tsx
│   ├── components/
│   │   ├── ui/                       # Component library
│   │   │   ├── shadcn/               # Primitives (Radix + tailwind)
│   │   │   │   ├── button.tsx
│   │   │   │   ├── accordion.tsx
│   │   │   │   └── ...
│   │   │   ├── features/             # Section group: features
│   │   │   │   ├── FeatureGrid.tsx
│   │   │   │   ├── FeatureCard.tsx
│   │   │   │   └── index.ts
│   │   │   ├── cta/                  # Section group: CTAs
│   │   │   │   ├── CTASimple.tsx
│   │   │   │   ├── CTAWithBackground.tsx
│   │   │   │   └── index.ts
│   │   │   └── ...                   # 11 more section groups
│   │   ├── demos/                    # Demo components for Markdoc tags
│   │   │   ├── ShadcnButtonDemo.tsx
│   │   │   └── ...
│   │   ├── DocsLayout.tsx            # Wraps all .md pages
│   │   ├── CTASectionLayout.tsx      # Showcase layout for CTA group
│   │   ├── FeatureSectionLayout.tsx  # Showcase layout for features
│   │   └── ...                       # More showcase layouts
│   ├── markdoc/
│   │   ├── tags.js                   # Custom Markdoc tags
│   │   ├── nodes.js                  # Custom Markdoc node renderers
│   │   └── search.mjs                # FlexSearch loader
│   ├── lib/
│   │   ├── navigation.ts             # Sidebar navigation structure
│   │   ├── sections.ts               # Table of contents generator
│   │   └── utils.ts                  # Helper functions (cn, clsx)
│   └── styles/
│       ├── tailwind.css              # CSS variables, dark mode
│       └── prism.css                 # Syntax highlighting
├── tsup.config.ts                    # npm package builder config
├── next.config.mjs                   # Next.js config with Markdoc layering
├── tailwind.config.ts                # Tailwind CSS config
└── package.json                      # Dependencies and exports

Build and deployment

Development

pnpm dev

Starts Next.js dev server with Markdoc and search support (via webpack).

Production build (docs site)

pnpm build

Builds the Next.js app to .next/. Markdoc pages are pre-compiled, and the FlexSearch index is generated.

Library build (npm package)

pnpm build:lib

Uses tsup to bundle components into dist/. Generates ESM, CJS, and type definitions for each section group. The package is ready to publish.


Key technologies

TechPurpose
Next.js 16App Router, server components, static builds
MarkdocMarkdown parsing with custom components as tags
Webpack (via --webpack flag)Layered Markdoc compilation to ensure Server Components
Tailwind CSSUtility-first styling with CSS variables for theming
Radix UIHeadless component primitives for accessibility
Radix IconsIcon library (50+ icons)
tsupFast bundler for the npm package
FlexSearchClient-side full-text search index
TypeScript 5.3+Full type safety across the codebase

Design philosophy

  1. Monorepo in one repository — Docs site and npm package share source code
  2. Static-first — Markdoc pages compile at build time into Server Components
  3. Tree-shakeable — Entry points per section group let users import only what they need
  4. Type-safe — Full TypeScript throughout; all components are fully typed
  5. Accessible by default — Radix UI primitives include keyboard navigation and ARIA
  6. Theming via CSS variables — Colors, spacing, and other tokens are overridable
  7. Zero runtime CSS — All styles are static Tailwind; no emotion or styled-components

Further reading

Explore the source code to deepen your understanding. Key files to review: next.config.mjs, src/markdoc/tags.js, src/components/DocsLayout.tsx, and tsup.config.ts.

Previous
How to contribute