Component Library Setup
Building a reusable component library with typestyles
Component Library Setup
This guide shows you how to set up a reusable component library using typestyles, suitable for publishing to npm.
Project structure
my-ui-library/
├── src/
│ ├── tokens/
│ │ ├── index.ts # Token exports
│ │ ├── colors.ts # Color definitions
│ │ ├── spacing.ts # Spacing scale
│ │ └── typography.ts # Font tokens
│ ├── components/
│ │ ├── Button/
│ │ │ ├── Button.tsx
│ │ │ ├── Button.styles.ts
│ │ │ └── index.ts
│ │ ├── Card/
│ │ │ ├── Card.tsx
│ │ │ ├── Card.styles.ts
│ │ │ └── index.ts
│ │ ├── Input/
│ │ │ └── ...
│ │ └── index.ts # Component exports
│ ├── utils/
│ │ └── style-utils.ts # Shared style utilities
│ ├── index.ts # Main library export
│ └── styles.css # Optional global CSS
├── package.json
├── tsconfig.json
├── vite.config.ts
└── README.md
Package configuration
package.json
{
"name": "@myorg/ui-library",
"version": "1.0.0",
"description": "A React component library built with typestyles",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./tokens": {
"import": "./dist/tokens/index.js",
"types": "./dist/tokens/index.d.ts"
},
"./styles.css": "./dist/styles.css"
},
"files": ["dist"],
"scripts": {
"build": "tsc && vite build",
"dev": "vite",
"test": "vitest",
"typecheck": "tsc --noEmit"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0",
"typestyles": "^1.0.0"
},
"devDependencies": {
"@types/react": "^18.0.0",
"typestyles": "^1.0.0",
"typescript": "^5.0.0",
"vite": "^5.0.0"
}
}
Note: typestyles should be a peer dependency so consuming apps can control the version.
Token system
Colors
// src/tokens/colors.ts
import { tokens } from 'typestyles';
export const colors = tokens.create('color', {
// Brand
brand50: '#eff6ff',
brand100: '#dbeafe',
brand200: '#bfdbfe',
brand300: '#93c5fd',
brand400: '#60a5fa',
brand500: '#3b82f6',
brand600: '#2563eb',
brand700: '#1d4ed8',
brand800: '#1e40af',
brand900: '#1e3a8a',
// Gray scale
gray50: '#f9fafb',
gray100: '#f3f4f6',
gray200: '#e5e7eb',
gray300: '#d1d5db',
gray400: '#9ca3af',
gray500: '#6b7280',
gray600: '#4b5563',
gray700: '#374151',
gray800: '#1f2937',
gray900: '#111827',
// Semantic
success: '#10b981',
warning: '#f59e0b',
danger: '#ef4444',
info: '#3b82f6',
});
// Semantic aliases
export const semanticColors = tokens.create('semantic-color', {
primary: colors.brand500,
primaryHover: colors.brand600,
secondary: colors.gray500,
secondaryHover: colors.gray600,
text: colors.gray900,
textMuted: colors.gray500,
background: colors.gray50,
surface: '#ffffff',
surfaceRaised: colors.gray100,
border: colors.gray200,
});
Spacing
// src/tokens/spacing.ts
import { tokens } from 'typestyles';
export const spacing = tokens.create('space', {
0: '0',
1: '4px',
2: '8px',
3: '12px',
4: '16px',
5: '20px',
6: '24px',
8: '32px',
10: '40px',
12: '48px',
16: '64px',
20: '80px',
24: '96px',
// Semantic aliases
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
xl: '32px',
'2xl': '48px',
'3xl': '64px',
});
Typography
// src/tokens/typography.ts
import { tokens } from 'typestyles';
export const fontSize = tokens.create('font-size', {
xs: '0.75rem', // 12px
sm: '0.875rem', // 14px
base: '1rem', // 16px
lg: '1.125rem', // 18px
xl: '1.25rem', // 20px
'2xl': '1.5rem', // 24px
'3xl': '1.875rem', // 30px
'4xl': '2.25rem', // 36px
});
export const fontWeight = tokens.create('font-weight', {
normal: '400',
medium: '500',
semibold: '600',
bold: '700',
});
export const lineHeight = tokens.create('line-height', {
none: '1',
tight: '1.25',
snug: '1.375',
normal: '1.5',
relaxed: '1.625',
loose: '2',
});
Token exports
// src/tokens/index.ts
export { colors, semanticColors } from './colors';
export { spacing } from './spacing';
export { fontSize, fontWeight, lineHeight } from './typography';
Component implementation
Button component
// src/components/Button/Button.styles.ts
import { styles } from 'typestyles';
import { semanticColors, spacing, fontSize, fontWeight } from '../../tokens';
export const button = styles.component('button', {
base: {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
gap: spacing[2],
borderRadius: '6px',
lineHeight: '1.5',
cursor: 'pointer',
border: 'none',
transition: 'all 150ms ease',
'&:disabled': {
opacity: 0.5,
cursor: 'not-allowed',
},
'&:focus': {
outline: 'none',
boxShadow: `0 0 0 3px ${semanticColors.primary}33`,
},
},
variants: {
intent: {
primary: {
backgroundColor: semanticColors.primary,
color: '#ffffff',
'&:hover:not(:disabled)': {
backgroundColor: semanticColors.primaryHover,
},
},
secondary: {
backgroundColor: semanticColors.secondary,
color: '#ffffff',
'&:hover:not(:disabled)': {
backgroundColor: semanticColors.secondaryHover,
},
},
outline: {
backgroundColor: 'transparent',
border: `1px solid ${semanticColors.border}`,
color: semanticColors.text,
'&:hover:not(:disabled)': {
backgroundColor: semanticColors.surfaceRaised,
},
},
ghost: {
backgroundColor: 'transparent',
color: semanticColors.text,
'&:hover:not(:disabled)': {
backgroundColor: semanticColors.surfaceRaised,
},
},
},
size: {
sm: {
padding: `${spacing[1]} ${spacing[3]}`,
fontSize: fontSize.sm,
fontWeight: fontWeight.medium,
},
md: {
padding: `${spacing[2]} ${spacing[4]}`,
fontSize: fontSize.base,
fontWeight: fontWeight.medium,
},
lg: {
padding: `${spacing[3]} ${spacing[6]}`,
fontSize: fontSize.lg,
fontWeight: fontWeight.medium,
},
},
width: {
auto: {},
full: { width: '100%' },
},
},
defaultVariants: {
intent: 'primary',
size: 'md',
width: 'auto',
},
});
// src/components/Button/Button.tsx
import { cx } from 'typestyles';
import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from 'react';
import { button } from './Button.styles';
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
size?: 'sm' | 'md' | 'lg';
fullWidth?: boolean;
children: ReactNode;
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ variant = 'primary', size = 'md', fullWidth = false, children, className, ...props }, ref) => {
return (
<button
ref={ref}
className={cx(
button({
intent: variant,
size,
width: fullWidth ? 'full' : 'auto',
}),
className,
)}
{...props}
>
{children}
</button>
);
},
);
Button.displayName = 'Button';
// src/components/Button/index.ts
export { Button } from './Button';
export type { ButtonProps } from './Button';
Theming support
Dark theme
// src/tokens/themes.ts
import { tokens } from 'typestyles';
export const darkTheme = tokens.createTheme('dark', {
base: {
'semantic-color': {
primary: '#60a5fa',
primaryHover: '#3b82f6',
text: '#f9fafb',
textMuted: '#9ca3af',
background: '#111827',
surface: '#1f2937',
surfaceRaised: '#374151',
border: '#4b5563',
},
},
});
export const highContrastTheme = tokens.createTheme('high-contrast', {
base: {
'semantic-color': {
text: '#000000',
background: '#ffffff',
primary: '#0000ff',
border: '#000000',
},
},
});
Theme provider
// src/components/ThemeProvider/ThemeProvider.tsx
import { createContext, useContext, useState, type ReactNode } from 'react';
import { darkTheme } from '../../tokens/themes';
interface ThemeContextValue {
theme: 'light' | 'dark';
setTheme: (theme: 'light' | 'dark') => void;
}
const ThemeContext = createContext<ThemeContextValue>({
theme: 'light',
setTheme: () => {},
});
export function useTheme() {
return useContext(ThemeContext);
}
interface ThemeProviderProps {
children: ReactNode;
defaultTheme?: 'light' | 'dark';
}
export function ThemeProvider({ children, defaultTheme = 'light' }: ThemeProviderProps) {
const [theme, setTheme] = useState(defaultTheme);
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<div className={theme === 'dark' ? darkTheme.className : ''}>{children}</div>
</ThemeContext.Provider>
);
}
Build configuration
Vite config
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import dts from 'vite-plugin-dts';
import path from 'path';
export default defineConfig({
plugins: [
react(),
dts({
insertTypesEntry: true,
}),
],
build: {
lib: {
entry: path.resolve(__dirname, 'src/index.ts'),
name: 'MyUILibrary',
formats: ['es', 'cjs'],
fileName: (format) => `index.${format === 'es' ? 'js' : 'cjs'}`,
},
rollupOptions: {
external: ['react', 'react-dom', 'typestyles'],
output: {
globals: {
react: 'React',
'react-dom': 'ReactDOM',
typestyles: 'TypeStyles',
},
},
},
},
});
TypeScript config
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"declaration": true,
"declarationMap": true,
"outDir": "dist"
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
Main exports
// src/index.ts
// Components
export { Button } from './components/Button';
export type { ButtonProps } from './components/Button';
export { Card } from './components/Card';
export type { CardProps } from './components/Card';
export { Input } from './components/Input';
export type { InputProps } from './components/Input';
// Theme
export { ThemeProvider, useTheme } from './components/ThemeProvider';
export { darkTheme, highContrastTheme } from './tokens/themes';
// Tokens (for custom styling)
export { colors, semanticColors, spacing, fontSize, fontWeight, lineHeight } from './tokens';
Usage examples
Installation
npm install @myorg/ui-library typestyles react react-dom
Basic usage
import { Button, Card, Input } from '@myorg/ui-library';
function App() {
return (
<Card>
<Card.Header>
<h2>Login</h2>
</Card.Header>
<Card.Body>
<Input label="Email" type="email" />
<Input label="Password" type="password" />
</Card.Body>
<Card.Footer>
<Button variant="primary" fullWidth>
Sign In
</Button>
</Card.Footer>
</Card>
);
}
With theming
import { ThemeProvider, Button } from '@myorg/ui-library';
function App() {
return (
<ThemeProvider defaultTheme="dark">
<Button variant="primary">I adapt to the theme!</Button>
</ThemeProvider>
);
}
Using tokens for custom styling
import { Button } from '@myorg/ui-library';
import { semanticColors, spacing } from '@myorg/ui-library/tokens';
import { styles } from 'typestyles';
const customCard = styles.component('custom-card', {
base: {
padding: spacing[6],
border: `2px solid ${semanticColors.primary}`,
borderRadius: '12px',
},
});
function CustomComponent() {
return (
<div className={customCard()}>
<Button variant="primary">Library Button</Button>
</div>
);
}
Publishing
Build the library
npm run build
Publish to npm
npm login
npm publish --access public
Versioning
Follow semantic versioning:
- Patch (1.0.1): Bug fixes, token value changes
- Minor (1.1.0): New components, new tokens (backward compatible)
- Major (2.0.0): Breaking changes, removed components, renamed tokens
Best practices
- Export prop types - Let consumers extend your components
- Document breaking changes - Keep a detailed changelog
- Test thoroughly - Components should work in any React app
- Minimize dependencies - Keep the library lightweight
- Provide examples - Show how to customize and extend
- Support tree-shaking - Use ES modules and avoid side effects
- Version tokens separately - Consider a separate tokens package
- Test with multiple React versions - Ensure compatibility
Troubleshooting
Styles not working in consumer app
Make sure typestyles is installed in the consumer app:
npm install typestyles
TypeScript errors in consumer
Ensure the consumer has compatible TypeScript settings:
{
"compilerOptions": {
"moduleResolution": "bundler",
"esModuleInterop": true
}
}
Duplicate typestyles instances
If you see duplicate style tags, ensure typestyles is a peer dependency, not a regular dependency.
Aside (Starlight-style)
Thin wrapper over the design-system alert styles; use Aside for
Starlight-like note / tip / caution / danger types.
Note
Pro tip
Caution
Danger
Badge
neutral accent success warning danger tip
Cards & link card
Card title
card component styles from @examples/design-system.
Model variant-driven UI with styles.component and optional slots.
Read more →Steps
Numbered procedure — pass <li> children in order:
-
Install dependencies with your package manager.
-
Register styles in a side-effect entry so build extraction can emit CSS (
typestyles-entry.ts). -
Run the dev server and open the docs.
Link button (CTA)
File tree
- src/
- index.ts
- styles.ts
-
Alert / callout (direct)
The docs use the same alert styles as @examples/design-system (see Alert.astro).
Info
Tip
Warning
Danger
Success