Contributing
Design principles
TypeScript UI is built on a set of core principles that guide every design decision. These principles ensure that components are accessible, performant, maintainable, and delightful to use.
Accessible by default
Components must be accessible without extra effort from the developer. No aria- attributes should be required for basic functionality.
Rationale: Accessibility is a feature, not an afterthought. Users with disabilities deserve the same experience as everyone else. Building accessibility into primitives ensures it's inherited by all compositions.
Examples in the codebase:
- Radix UI primitives — All components like Accordion, Dialog, and Navigation Menu include full ARIA attributes and keyboard navigation
- Labeled form inputs — Form components include associated labels, not just placeholders
- aria-expanded on menus — The mobile menu uses
aria-expanded="true|false"to announce state changes to screen readers - prefers-reduced-motion — Animated components like stats counters and logo marquees respect the
prefers-reduced-motionmedia query, disabling animations for users who prefer it
Fully typed with TypeScript
Every component exports a Props interface. No any types. Type-safe by default.
Rationale: TypeScript catches mistakes at development time, not runtime. Fully typed props improve IDE autocomplete, enable refactoring, and make APIs self-documenting.
Examples in the codebase:
export interface CTASimpleProps {
title: string;
description: string;
primaryButton: {
text: string;
href: string;
};
secondaryButton?: {
text: string;
href: string;
};
className?: string;
}
export function CTASimple({
title,
description,
primaryButton,
secondaryButton,
className,
}: CTASimpleProps) {
// ...
}
Users see full autocomplete and errors if they pass the wrong type.
Composition over configuration
Simple components that compose easily beat complex components with dozens of props.
Rationale: Complex props matrices explode in size and become hard to maintain. Composition—using multiple small components together—keeps each component focused and lets users build what they need.
Examples in the codebase:
- Button variants — Instead of a single Button with 20 props, the button primitive exports different variants (default, outline, ghost, link, etc.) that can be combined
- Feature sections — Separate components for FeatureCard, FeatureGrid, and FeatureIconList let users choose the layout that fits their content
- Pricing sections — PricingCard, PricingSimple, and PricingComparison each serve a distinct use case
Dark mode is not optional
All components ship with full dark mode support. Dark mode must be complete, not an afterthought.
Rationale: Dark mode is expected by modern applications. Supporting it from day one ensures all components work in both light and dark contexts. This is a baseline requirement, not a bonus feature.
Examples in the codebase:
Every component includes both light and dark styles:
export function MyComponent({ title, description }) {
return (
<div>
<h2 className="text-3xl font-bold text-slate-900 dark:text-white">
{title}
</h2>
<p className="text-slate-600 dark:text-slate-400">
{description}
</p>
</div>
);
}
The dark: variants are defined in the same class list, not in separate CSS files. Dark mode is controlled via a dark class on the root element (typically managed by next-themes).
Responsive by default
All components work on mobile without extra configuration. Mobile-first design is the default.
Rationale: Most users browse on mobile. Responsive design should not be optional or require a separate mobile variant—it's the default. Use Tailwind's mobile-first breakpoints (sm, md, lg, xl) to enhance on larger screens.
Examples in the codebase:
- Flexible layouts — CTASectionLayout uses
flex flex-col items-center justify-center gap-4 sm:flex-rowto stack buttons vertically on mobile and horizontally on larger screens - Responsive grids — FeatureGrid adapts from 1 column on mobile to 2-4 columns on desktop based on content
- Responsive text — Headers use
text-2xl sm:text-3xl md:text-4xlto scale across breakpoints
Zero runtime CSS
All styling is static Tailwind CSS. No emotion, styled-components, or runtime style injection.
Rationale: Static styles are faster (no runtime calculation), smaller (Tailwind's CSS is minimal and tree-shakeable), and more predictable. Runtime CSS adds complexity and performance overhead.
Examples in the codebase:
- Tailwind utilities only — Every component uses
className="..."with Tailwind utilities - No CSS modules — Styling is colocated via className, not in separate
.module.cssfiles - No dynamic CSS-in-JS — Variants are handled via conditional classes, not runtime style injection
// Good: Static Tailwind classes
<div
className={clsx(
'py-16 px-6 rounded-lg',
variant === 'solid' && 'bg-sky-500 text-white',
variant === 'outline' && 'border border-sky-500 text-sky-900 dark:text-sky-100',
)}
>
{children}
</div>
Sensible defaults with escape hatches
Components have clear defaults but always allow customization via className.
Rationale: Defaults make components easy to use out of the box. The className prop escape hatch lets power users override anything without forking the component.
Examples in the codebase:
export interface MyComponentProps
extends React.HTMLAttributes<HTMLDivElement> {
title: string;
description: string;
className?: string; // Escape hatch
}
export function MyComponent({
title,
description,
className,
}: MyComponentProps) {
return (
<div className={clsx('py-16 text-center', className)}>
<h2 className="text-3xl font-bold">
{title}
</h2>
<p className="mt-4 text-slate-600">
{description}
</p>
</div>
);
}
Users can extend the component:
import { MyComponent } from '@/components/MyComponent';
export function CustomPage() {
return (
<MyComponent
title="Custom styles"
description="Add your own classes"
className="!py-32 !text-left" // Override defaults
/>
);
}
Use clsx/cn for dynamic classes
Conditional classes should be formatted clearly and combined using utility functions.
Rationale: String concatenation is error-prone. clsx and cn (alias for clsx with Tailwind merge) make conditional logic clear and handle edge cases like duplicate classes.
Examples in the codebase:
import clsx from 'clsx';
<button
className={clsx(
'px-4 py-2 rounded-lg transition-all',
// Variants
variant === 'primary' && 'bg-sky-500 text-white hover:bg-sky-600',
variant === 'secondary' && 'bg-slate-200 text-slate-900 dark:bg-slate-700 dark:text-white',
// Sizes
size === 'sm' && 'text-sm',
size === 'lg' && 'text-lg px-6 py-3',
// Custom override
className,
)}
>
{children}
</button>
Prefer 'use client' only when necessary
Server Components are the default. Use 'use client' only for interactivity, state, or hooks.
Rationale: Server Components are more secure (API keys stay private), lighter (less JavaScript shipped), and better for SEO. Client Components should be reserved for features that actually need them.
Examples in the codebase:
Server Component (no 'use client' directive):
// src/components/Header.tsx
export function StaticHeader() {
return (
<header className="bg-white dark:bg-slate-900">
<h1 className="text-2xl font-bold">Welcome</h1>
</header>
);
}
Client Component (with 'use client' because of interactivity):
// src/components/Nav.tsx
'use client';
import { useState } from 'react';
import { Menu } from './Menu';
export function InteractiveNav() {
const [isOpen, setIsOpen] = useState(false);
return (
<nav>
<button onClick={() => setIsOpen(!isOpen)}>
Toggle Menu
</button>
{isOpen && <Menu />}
</nav>
);
}
Color palette: slate and sky
Use slate for neutral text/backgrounds and sky for accent colors. Never hardcode colors—use Tailwind color names so CSS variables can override them.
Rationale: A consistent palette prevents design fragmentation. Using Tailwind's naming keeps CSS variables in sync. Slate is neutral and professional; sky is friendly and accessible.
Examples in the codebase:
// Text colors
className="text-slate-900 dark:text-white" // Foreground
className="text-slate-600 dark:text-slate-400" // Secondary text
className="text-slate-500 dark:text-slate-500" // Muted
// Accent colors
className="bg-sky-500 hover:bg-sky-600 dark:bg-sky-600"
className="border border-sky-500"
className="text-sky-700 dark:text-sky-300"
Never use hex colors directly. Always use Tailwind utilities.
Summary
These principles work together:
- Accessibility ensures inclusive experiences
- TypeScript catches errors early
- Composition keeps code maintainable
- Dark mode is standard
- Responsive design works everywhere
- Static Tailwind is performant
- Sensible defaults make components easy to use
- clsx and 'use client' sparingly keep code clean
- Consistent colors and Tailwind utilities enable theming
Design in practice
Every component in TypeScript UI follows these principles. When you're unsure about a design decision, refer back to this guide.