Contributing
How to contribute
TypeScript UI is an open-source project. We welcome contributions of new components, bug fixes, documentation improvements, and feature requests. This guide walks you through setting up your environment and the process of adding components.
Prerequisites
Before you start contributing, make sure you have:
- Node.js 20+ — Required for building and running the development server
- pnpm — Package manager used by this project (install with
npm install -g pnpm) - Git — For cloning the repository and version control
- A text editor — VS Code, Cursor, or your preferred code editor
Getting started
1. Clone the repository
git clone https://github.com/boomspot/typescript-ui.git
cd typescript-ui
2. Install dependencies
pnpm install
3. Start the development server
pnpm dev
The documentation site will be available at http://localhost:3000. The dev server supports hot module reloading, so changes you make to files will appear instantly.
4. Run linting and type checks
pnpm lint
pnpm exec tsc --noEmit
These commands check your code for style issues and TypeScript errors.
Adding a new primitive
Primitives are low-level, reusable components built on Radix UI. They are copy-paste components that users can import and customize. Follow these steps to add a new primitive:
Step 1: Create the component file
Create a new file in src/components/ui/shadcn/ with your component name:
// src/components/ui/shadcn/your-component.tsx
'use client';
import { forwardRef } from 'react';
import { cn } from '@/lib/utils';
export interface YourComponentProps
extends React.HTMLAttributes<HTMLDivElement> {
// Define your props here
}
export const YourComponent = forwardRef<
HTMLDivElement,
YourComponentProps
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('base-styles dark:dark-styles', className)}
{...props}
/>
));
YourComponent.displayName = 'YourComponent';
export default YourComponent;
Step 2: Create a demo component
Create a demo file in src/components/demos/ to showcase the primitive:
// src/components/demos/YourComponentDemo.tsx
'use client';
import YourComponent from '@/components/ui/shadcn/your-component';
export default function YourComponentDemo() {
return (
<div className="space-y-4">
<YourComponent>Example content</YourComponent>
</div>
);
}
Step 3: Create a documentation page
Create a Markdoc page under src/app/components/shadcn/<name>/page.md:
---
title: Your Component
nextjs:
metadata:
title: Your Component - TypeScript UI
description: A brief description of what your component does.
---
A clear explanation of your component's purpose and use cases.
---
## Component Demo
{% shadcn-your-component /%}
## Usage
\`\`\`tsx
import YourComponent from '@/components/ui/shadcn/your-component'
export function Example() {
return <YourComponent>Hello</YourComponent>
}
\`\`\`
## Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| ... | ... | ... | ... |
Replace shadcn-your-component with your actual tag name (all lowercase, dashes instead of camelCase).
Step 4: Register the demo tag
Add your demo to src/markdoc/tags.js:
import YourComponentDemo from '@/components/demos/YourComponentDemo';
const tags = {
// ... existing tags
'shadcn-your-component': {
selfClosing: true,
render: YourComponentDemo,
},
};
Step 5: Add navigation entry
Add your component to src/lib/navigation.ts under the Primitives section:
// In navigation.ts, add to the Primitives section links array:
export const navigation = [
{
title: 'Primitives',
links: [
{ title: 'Your Component', href: '/components/shadcn/your-component' },
// ... other primitives
],
},
];
Adding a new section group
Section groups are collections of related components like features, CTAs, pricing, etc. Sections are published to npm and used as-is (not copy-paste). Follow these steps:
Step 1: Create the section folder
Create a new directory under src/components/ui/<group-name>/:
src/components/ui/your-sections/
├── YourSection1.tsx
├── YourSection2.tsx
└── index.ts
Step 2: Create your section components
Build your components with the naming convention: named export + Props interface + default export:
// src/components/ui/your-sections/YourSection1.tsx
'use client';
import clsx from 'clsx';
export interface YourSection1Props {
title: string;
description: string;
className?: string;
}
export function YourSection1({
title,
description,
className,
}: YourSection1Props) {
return (
<div className={clsx('py-16', className)}>
<h2 className="text-3xl font-bold text-slate-900 dark:text-white">
{title}
</h2>
<p className="mt-4 text-slate-600 dark:text-slate-400">
{description}
</p>
</div>
);
}
export default YourSection1;
Step 3: Create an index file
Export all components from src/components/ui/your-sections/index.ts:
export { YourSection1, type YourSection1Props } from './YourSection1';
export { YourSection2, type YourSection2Props } from './YourSection2';
Step 4: Create a showcase layout
Create src/components/YourSectionsSectionLayout.tsx to display all variants:
'use client';
import { YourSection1, YourSection2 } from '@/components/ui/your-sections';
const YourSectionsSectionLayout: React.FC = () => {
return (
<div className="space-y-24">
<section>
<h3 className="mb-6 text-xl font-semibold text-slate-900 dark:text-white">
Your Section 1
</h3>
<YourSection1
title="Example Title"
description="Example description"
/>
</section>
<section>
<h3 className="mb-6 text-xl font-semibold text-slate-900 dark:text-white">
Your Section 2
</h3>
<YourSection2 title="Another Title" />
</section>
</div>
);
};
export default YourSectionsSectionLayout;
Step 5: Create a documentation page
Create src/app/components/your-sections/page.md:
---
title: Your Sections
nextjs:
metadata:
title: Your Sections - TypeScript UI
description: Pre-built section components for [description].
---
Brief introduction to your section group.
---
## Component Variants
{% yoursectionssections /%}
## Usage
\`\`\`tsx
import { YourSection1, YourSection2 } from 'typescript-ui/your-sections';
export default function Page() {
return <YourSection1 title="..." description="..." />;
}
\`\`\`
## Props
...
Step 6: Register the showcase tag
Add to src/markdoc/tags.js:
import YourSectionsSectionLayout from '@/components/YourSectionsSectionLayout';
const tags = {
// ... existing tags
'yoursectionssections': {
selfClosing: true,
render: YourSectionsSectionLayout,
},
};
Step 7: Add navigation entry
Add to src/lib/navigation.ts under the Sections category:
// In navigation.ts, add to the Sections section links array:
export const navigation = [
{
title: 'Sections',
links: [
{ title: 'Your Sections', href: '/components/your-sections' },
// ... other sections
],
},
];
Step 8: Export from main index
Add to src/components/ui/index.ts:
export * from './your-sections';
Step 9: Configure tsup build
Add an entry to tsup.config.ts:
export default defineConfig({
entry: {
// ... existing entries
'your-sections/index': 'src/components/ui/your-sections/index.ts',
},
// ...
});
Step 10: Update package.json
Add an export to package.json:
{
"exports": {
"./your-sections": {
"types": "./dist/your-sections/index.d.ts",
"import": "./dist/your-sections/index.mjs",
"require": "./dist/your-sections/index.js"
}
}
}
Coding conventions
Follow these conventions for consistency:
Component structure
- Use named exports for the component function and a default export
- Always define a Props interface with full documentation
- Use
forwardReffor components that accept refs - Add
displayNameto components usingforwardRef
Styling
- Use Tailwind CSS classes exclusively—no inline styles or CSS modules
- Follow the slate/sky palette for default colors
- Always include dark mode support with
dark:variants - Use
clsxorcn(from@/lib/utils) for dynamic classes
Client/Server directives
- Use
'use client'only when necessary (hooks, event handlers, interactivity) - Leave Server Component directives off by default
- Section components typically need
'use client'for interactivity
Example component
'use client';
import clsx from 'clsx';
import { forwardRef } from 'react';
export interface MyComponentProps
extends React.HTMLAttributes<HTMLDivElement> {
variant?: 'default' | 'secondary';
size?: 'sm' | 'md' | 'lg';
}
export const MyComponent = forwardRef<HTMLDivElement, MyComponentProps>(
({ variant = 'default', size = 'md', className, ...props }, ref) => (
<div
ref={ref}
className={clsx(
'rounded-lg transition-colors',
variant === 'default' &&
'bg-sky-500 text-white hover:bg-sky-600 dark:bg-sky-600 dark:hover:bg-sky-700',
variant === 'secondary' &&
'bg-slate-200 text-slate-900 hover:bg-slate-300 dark:bg-slate-700 dark:text-white dark:hover:bg-slate-600',
size === 'sm' && 'px-2 py-1 text-sm',
size === 'md' && 'px-4 py-2 text-base',
size === 'lg' && 'px-6 py-3 text-lg',
className,
)}
{...props}
/>
),
);
MyComponent.displayName = 'MyComponent';
export default MyComponent;
Pre-PR checks
Before submitting a pull request, run these checks locally:
1. ESLint
pnpm lint
Ensures your code follows style guidelines. Fix issues automatically where possible:
pnpm lint --fix
2. TypeScript
pnpm exec tsc --noEmit
Checks for type errors without emitting files.
3. Build the docs site
pnpm build
Ensures the documentation site builds without errors.
4. Build the library
pnpm build:lib
Tests that the npm package builds correctly. Check the dist/ directory for output.
Pull request guidelines
When opening a pull request:
- Clear title — Describe what you're adding or fixing
- Detailed description — Explain the why and how
- Minimal scope — One feature or fix per PR
- Documentation — Include updated docs for new components
- Testing — Ensure pre-PR checks pass
- Examples — Link to relevant examples or screenshots
Questions?
If you have questions about contributing, open an issue or discussion on GitHub. We're here to help.