@abhir9/pd-design-system
v0.1.31
Published
Production-grade design system with adapter layer support
Maintainers
Readme
@pd-design/system
Production-grade design system with adapter layer support. Built for teams who need a flexible, type-safe component library that can work with different UI engines without changing application code.
🎯 What People Love About PD Design System
- ✅ 3 Lines to Get Started - No complex setup, just works
- ✅ System Mode Built-In - Automatically follows OS dark/light preference
- ✅ Zero-Config Persistence - Pass
storageKeyprop, localStorage sync is automatic - ✅ Cross-Tab Sync - Theme changes sync across browser tabs instantly
- ✅ Type-Safe - Full TypeScript with autocomplete for all props
- ✅ Accessible - WCAG 2.1 AA compliant, built on Radix UI
- ✅ Style Isolation - Works with Tailwind, MUI, AntD, Bootstrap, or plain CSS
- ✅ 50+ Components - Buttons, Inputs, Modals, Dropdowns, and more
- ✅ 1000+ Icons - All Lucide icons with TypeScript autocomplete
Why PD Design System?
🚀 Incredibly Easy to Use
// That's it! Just 3 lines of code
<ThemeProvider storageKey="my-theme">
<Button>Click me</Button>
</ThemeProvider>No complex setup. No manual theme management. No localStorage code. Everything works automatically.
✨ Key Features
- 🎯 Zero-Config Theme System - Pass
storageKeyand everything works - 🌓 System Mode Support - Automatically follows OS dark/light preference
- 💾 Automatic Persistence - localStorage sync with one prop
- 🔄 Cross-Tab Sync - Theme changes sync across browser tabs
- 🎨 Semantic Tokens - Design tokens that adapt to themes automatically
- 📦 Type-Safe - Full TypeScript with autocomplete
- ♿ Accessible - WCAG 2.1 AA compliant out of the box
- 🔌 Adapter Layer - Switch UI engines without changing app code
- 🎭 Style Isolation - Works with any CSS framework (Tailwind, MUI, AntD, etc.)
Philosophy
This design system follows a headless-first, adapter-based architecture:
- Props-only API: Consumers use only props and variants, no className required
- Full TypeScript: Complete autocomplete and type safety
- Adapter Layer: Switch between UI engines (shadcn, Material, etc.) without app changes
- Semantic Tokens: Design tokens that work across themes
- Framework Agnostic: Design tokens and semantics are framework-agnostic
- Zero-Config Theming: Automatic system detection and localStorage sync
Installation
npm install @pd-design/systemQuick Start
Get started in 3 simple steps:
import { ThemeProvider, Button } from '@pd-design/system';
import '@pd-design/system/styles.css';
function App() {
return (
<ThemeProvider
adapter="shadcn"
theme="base"
storageKey="my-app-theme"
>
<Button variant="primary" size="md">
Click me
</Button>
</ThemeProvider>
);
}That's it! 🎉 The ThemeProvider automatically handles:
- ✅ System preference detection - Follows OS dark/light mode
- ✅ localStorage persistence - Remembers user's theme choice
- ✅ Cross-tab sync - Theme changes sync across browser tabs
- ✅ Class management - Automatically adds
pd-rootandpd-darkclasses - ✅ CSS variables - Sets all theme variables automatically
Important: Style Isolation
The design system uses scoped styling to prevent style conflicts:
- ✅ All Tailwind classes are prefixed with
pd-- No collision with consumer Tailwind - ✅ All CSS variables are scoped under
.pd-root- No token leakage - ✅ Preflight is disabled - Consumer styles are never reset
- ✅ Works with any consumer setup - Tailwind, MUI, AntD, Bootstrap, or plain CSS
Always wrap your components with ThemeProvider (or PdThemeProvider) to create the scoped boundary:
<ThemeProvider storageKey="my-theme">
{/* Your design system components */}
</ThemeProvider>The ThemeProvider automatically adds the pd-root class to your body element, so all tokens are properly scoped.
Complete Example
Here's a complete example showing how easy it is:
import { ThemeProvider, Button, useTheme } from '@pd-design/system';
import '@pd-design/system/styles.css';
function App() {
return (
<ThemeProvider
adapter="shadcn"
theme="base"
storageKey="my-app-theme" // Enable localStorage persistence
>
<ThemeToggle />
<Button variant="primary">Hello World</Button>
</ThemeProvider>
);
}
function ThemeToggle() {
const { config, setConfig } = useTheme();
return (
<Button onClick={() => {
const nextMode = config.mode === 'dark' ? 'light' : 'dark';
setConfig({ mode: nextMode });
// ✅ Automatically saved to localStorage
// ✅ Syncs across tabs
// ✅ Updates theme immediately
}}>
Switch to {config.mode === 'dark' ? 'Light' : 'Dark'} Mode
</Button>
);
}What happens automatically:
- ✅ Reads theme from localStorage on mount
- ✅ Detects system preference if mode is
'system' - ✅ Saves theme changes to localStorage
- ✅ Syncs changes across browser tabs
- ✅ Updates CSS variables and classes
- ✅ Manages
pd-rootandpd-darkclasses
Configuration
For advanced use cases, you can configure the design system globally:
import { setDesignSystemConfig } from '@pd-design/system';
// Set adapter (shadcn or material)
setDesignSystemConfig({
adapter: 'shadcn',
theme: 'base',
mode: 'light',
});Note: When using ThemeProvider with storageKey, you don't need to call setDesignSystemConfig manually - it's handled automatically.
ThemeProvider Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| adapter | 'shadcn' \| 'material' | 'shadcn' | UI engine adapter |
| theme | 'base' \| 'brand' | 'base' | Theme name |
| mode | 'light' \| 'dark' \| 'system' | 'system' | Color mode (system follows OS) |
| storageKey | string? | undefined | localStorage key for persistence (enables sync) |
PdThemeProvider vs ThemeProvider
PdThemeProvider: Lightweight wrapper that creates the.pd-rootscoped boundary. Use for simple theming needs without context.ThemeProvider: Full-featured provider with React context, system preference detection, localStorage sync, and CSS variable management. Recommended for most use cases.
Both support the same theme and mode props.
Components
Button
<Button
variant="primary" // primary | secondary | ghost | destructive
size="md" // sm | md | lg
startIcon="Download" // Icon name (TypeScript autocomplete)
endIcon="ArrowRight" // Icon name (TypeScript autocomplete)
loading={false}
disabled={false}
asChild={false} // Render as child element
>
Click me
</Button>For form submission buttons:
<form onSubmit={handleSubmit}>
<Button type="submit">Submit Form</Button>
<Button type="reset">Reset</Button>
</form>Button with Icons:
<Button startIcon="Download">Download</Button>
<Button endIcon="ArrowRight">Next</Button>
<Button startIcon="Trash2" variant="destructive">Delete</Button>
<Button startIcon="Save">Save</Button>Input
<Input
size="md" // sm | md | lg
error={false}
disabled={false}
placeholder="Enter text..."
/>Select
<Select placeholder="Choose an option..." size="md">
<SelectItem value="1">Option 1</SelectItem>
<SelectItem value="2">Option 2</SelectItem>
</Select>Checkbox
<Checkbox
checked={isChecked}
onCheckedChange={setIsChecked}
size="md"
/>Radio
<RadioGroup value={value} onValueChange={setValue}>
<div className="flex items-center space-x-2">
<RadioItem value="option1" id="r1" />
<label htmlFor="r1">Option 1</label>
</div>
</RadioGroup>Switch
<Switch
checked={isOn}
onCheckedChange={setIsOn}
size="md"
/>Modal
<Modal open={open} onOpenChange={setOpen}>
<ModalTrigger asChild>
<Button>Open Modal</Button>
</ModalTrigger>
<ModalContent>
<ModalHeader>
<ModalTitle>Title</ModalTitle>
<ModalDescription>Description</ModalDescription>
</ModalHeader>
<ModalClose asChild>
<Button>Close</Button>
</ModalClose>
</ModalContent>
</Modal>Tooltip
<Tooltip content="This is a tooltip" side="top">
<Button>Hover me</Button>
</Tooltip>Dropdown
<Dropdown trigger={<Button>Menu</Button>}>
<DropdownItem>Profile</DropdownItem>
<DropdownItem>Settings</DropdownItem>
<DropdownSeparator />
<DropdownItem>Logout</DropdownItem>
</Dropdown>Toast
<ToastProvider>
<Toast
open={open}
onOpenChange={setOpen}
title="Success"
description="Action completed"
/>
</ToastProvider>Icons
The design system includes all Lucide React icons with TypeScript autocomplete support.
Using Icons with Components
Use icon names as strings. TypeScript will autocomplete available icon names:
import { Button } from '@pd-design/system';
<Button startIcon="Download">Download</Button>
<Button endIcon="ArrowRight">Next</Button>
<Button startIcon="Trash2" variant="destructive">Delete</Button>Using Icons Standalone
Import icons from the Icons namespace for standalone use:
import { Icons } from '@pd-design/system';
<Icons.Download className="h-4 w-4" />
<Icons.Trash2 className="h-5 w-5 text-red-500" />
<Icons.Heart className="h-6 w-6 fill-red-500" />Combining Icons and Components
You can use icons standalone alongside components:
import { Button, Icons } from '@pd-design/system';
<Button startIcon="Download">
<Icons.Check className="ml-2 h-4 w-4" />
Download Complete
</Button>Available Icons
All Lucide React icons are available. TypeScript provides autocomplete for icon names when using with components. Browse all icons in Storybook or check the Lucide Icons website.
Popular icons: Download, Upload, Trash2, Edit, Save, Search, Settings, User, Mail, Bell, Heart, Star, Share, Copy, Check, X, Plus, Minus, ArrowRight, ArrowLeft, Home, Menu, and 1000+ more.
Variant System
All components follow a strict, typed variant system:
- variant:
primary|secondary|ghost|destructive - size:
sm|md|lg - state:
loading|disabled
No arbitrary strings or raw class overrides are allowed.
Theming
The design system supports themes and modes as separate concepts:
- Theme (
theme): The design theme name -'base'|'brand' - Mode (
mode): The color scheme -'light'|'dark'|'system'
System Mode 🌓
The 'system' mode automatically follows your OS dark/light preference:
<ThemeProvider storageKey="my-theme">
{/*
- If OS is dark → uses dark mode
- If OS is light → uses light mode
- Automatically updates when OS preference changes
- Works perfectly with localStorage persistence
*/}
</ThemeProvider>How it works:
- On mount, detects OS preference
- Listens to OS preference changes in real-time
- Only applies system changes when mode is
'system' - When user manually selects
'light'or'dark', system changes are ignored
localStorage Persistence 💾
Pass storageKey to enable automatic theme persistence:
<ThemeProvider storageKey="my-app-theme">
{/*
✅ Reads saved preference on mount
✅ Saves changes automatically
✅ Syncs across browser tabs
✅ Works with system mode
*/}
</ThemeProvider>What gets saved:
- User's manual selection (
'light'|'dark') 'system'preference (so it knows to follow OS)
Example flow:
- User selects "Dark" → Saved to localStorage
- User refreshes page → Reads "Dark" from localStorage
- User opens new tab → Reads "Dark" from localStorage (synced)
- User changes to "System" → Saved, now follows OS preference
Theme Examples
// Base theme, dark mode
<ThemeProvider theme="base" mode="dark" storageKey="my-theme">
{/* Your app */}
</ThemeProvider>
// Brand theme, light mode
<ThemeProvider theme="brand" mode="light" storageKey="my-theme">
{/* Your app */}
</ThemeProvider>
// Base theme, follows OS preference (recommended)
<ThemeProvider theme="base" storageKey="my-theme">
{/* Default mode is 'system' - follows OS */}
</ThemeProvider>Default Values
- adapter:
'shadcn'(default) - theme:
'base'(default) - mode:
'system'(default - follows OS preference) - storageKey:
undefined(no persistence by default, pass a key to enable)
Custom Themes
You can override tokens using CSS variables scoped to .pd-root:
<PdThemeProvider
theme="base"
mode="light"
className="pd-root [--pd-primary:220_80%_50%]"
>
{/* Custom primary color */}
</PdThemeProvider>Or via CSS:
.pd-root {
--pd-primary: 220 80% 50%;
--pd-primary-foreground: 0 0% 98%;
}Per-Widget Theming
You can wrap only part of your page with PdThemeProvider for widget-level theming:
<div>
{/* Consumer styles */}
<PdThemeProvider theme="base" mode="dark">
{/* Design system widget with dark theme */}
</PdThemeProvider>
</div>Adapter Layer
The adapter layer allows switching UI engines without changing application code:
// Switch from shadcn to material
setDesignSystemConfig({ adapter: 'material' });Currently supported adapters:
- shadcn: Radix UI + Tailwind CSS
- material: Material UI (coming soon)
Storybook
View all components and variants in Storybook:
npm run storybookStorybook includes:
- ✅ All component variants with live examples
- ✅ Interactive controls for all props
- ✅ Theme switcher (base/brand) - See components in different themes
- ✅ Mode switcher (light/dark/system) - Test system mode detection
- ✅ Adapter switcher (shadcn/material) - Switch UI engines
- ✅ Icons browser with search - Browse 1000+ Lucide icons
- ✅ Accessibility panel - WCAG compliance checks
- ✅ Code snippets - Copy-paste ready examples
- ✅ Design tokens showcase - See all color primitives and semantic tokens
- ✅ Dark mode preview - See how components look in dark mode
Architecture
src/
├── tokens/ # Base & semantic tokens
├── theme/ # Theme system & provider
├── primitives/ # Headless components
├── adapters/ # UI engine adapters
├── components/ # Public components
└── styles/ # Scoped CSS (tokens.css, base.css)Style Isolation Architecture
The design system implements 4 guardrails to prevent style conflicts:
- Prefix all Tailwind classes (
pd-) - Utilities never collide with consumer Tailwind - Disable Tailwind preflight - Consumer styles are never reset
- Scope all tokens under
.pd-root- Tokens never leak outside the boundary - Ship compiled CSS - Consumer doesn't need Tailwind to use components
This ensures the design system works seamlessly with:
- ✅ Consumer Tailwind projects
- ✅ Material UI (MUI)
- ✅ Ant Design
- ✅ Bootstrap
- ✅ Plain CSS projects
- ✅ Any CSS framework or reset
TypeScript
Full TypeScript support with autocomplete for all props:
<Button
variant="primary" // ✅ Autocomplete: primary | secondary | ghost | destructive
size="md" // ✅ Autocomplete: sm | md | lg
startIcon="Download" // ✅ Autocomplete: All Lucide icon names
endIcon="ArrowRight" // ✅ Autocomplete: All Lucide icon names
/>
<PdThemeProvider
theme="base" // ✅ Autocomplete: base | brand
mode="system" // ✅ Autocomplete: light | dark | system
/>All types are exported and available for use:
import type {
Variant,
Size,
ButtonType,
ThemeName,
ThemeMode,
LucideIconName
} from '@pd-design/system';API Reference
Exports
// Components
import {
Button,
ButtonGroup,
PdThemeProvider,
Icons
} from '@pd-design/system';
// Theme (Recommended: ThemeProvider with storageKey)
import {
ThemeProvider, // Full-featured provider with localStorage sync
useTheme, // Hook to access/update theme
setDesignSystemConfig,
getDesignSystemConfig
} from '@pd-design/system';
// Types
import type {
Variant,
Size,
ButtonType,
ThemeName,
ThemeMode,
AdapterType,
LucideIconName
} from '@pd-design/system';
// Icons
import { Icons } from '@pd-design/system';
// or individual icons
import { Download, Trash2 } from '@pd-design/system';
// Utilities
import {
getIcon,
renderIcon,
iconExists,
getAvailableIconNames
} from '@pd-design/system';
// Styles (required)
import '@pd-design/system/styles.css';Component Props
All components use consistent prop patterns:
- variant: Visual style (
primary,secondary,ghost,destructive) - size: Component size (
sm,md,lg) - disabled: Disabled state
- loading: Loading state (where applicable)
- startIcon/endIcon: Icon names with TypeScript autocomplete
Accessibility
- ✅ Radix UI primitives for ARIA compliance
- ✅ Keyboard navigation support (Tab, Enter, Space, Arrow keys)
- ✅ Focus ring standards (visible focus indicators)
- ✅ Screen reader labels (aria-label, aria-describedby)
- ✅ Contrast-safe tokens (WCAG AA compliant)
- ✅ Disabled state handling (aria-disabled, pointer-events)
- ✅ Loading state indicators (aria-busy)
Common Use Cases
Use Case 1: Basic Setup with Persistence
<ThemeProvider storageKey="my-app-theme">
<App />
</ThemeProvider>What you get:
- ✅ System mode detection (follows OS)
- ✅ Theme persistence (remembers user choice)
- ✅ Cross-tab sync
- ✅ Zero configuration
Use Case 2: Force Light Mode
<ThemeProvider mode="light">
<App />
</ThemeProvider>What you get:
- ✅ Always light mode
- ✅ Ignores system preference
- ✅ No localStorage (no storageKey)
Use Case 3: Custom Theme with System Mode
<ThemeProvider
theme="brand"
storageKey="my-brand-theme"
>
<App />
</ThemeProvider>What you get:
- ✅ Brand theme
- ✅ System mode with persistence
- ✅ User can override system preference
Use Case 4: Theme Toggle Component
function ThemeToggle() {
const { config, setConfig } = useTheme();
const toggle = () => {
const modes: ThemeMode[] = ['light', 'dark', 'system'];
const currentIndex = modes.indexOf(config.mode);
const nextIndex = (currentIndex + 1) % modes.length;
setConfig({ mode: modes[nextIndex] });
};
return (
<Button onClick={toggle}>
{config.mode === 'system' && '🌓 System'}
{config.mode === 'light' && '☀️ Light'}
{config.mode === 'dark' && '🌙 Dark'}
</Button>
);
}Browser Support
- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
- Mobile browsers (iOS Safari, Chrome Mobile)
Quick Reference
ThemeProvider Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| adapter | 'shadcn' \| 'material' | 'shadcn' | UI engine adapter |
| theme | 'base' \| 'brand' | 'base' | Theme name |
| mode | 'light' \| 'dark' \| 'system' | 'system' | Color mode |
| storageKey | string? | undefined | localStorage key (enables persistence) |
What Happens When You Pass storageKey?
✅ Automatic localStorage sync - Reads and writes theme preference
✅ Cross-tab synchronization - Changes sync across browser tabs
✅ System mode support - Follows OS preference when mode is 'system'
✅ Class management - Automatically adds pd-root and pd-dark classes
✅ CSS variables - Sets all theme variables automatically
System Mode Behavior
mode="system"(default): Follows OS dark/light preference, updates automaticallymode="light": Always light mode, ignores system preferencemode="dark": Always dark mode, ignores system preference
When storageKey is provided:
- User's manual selection (
light/dark) is saved systempreference is also saved- On refresh, uses saved preference
- System changes only apply when mode is
'system'
Contributing
This is an internal design system. For contributions, please follow the architecture guidelines in ARCHITECTURE.md.
License
MIT
