Design System Integration
Work with existing component libraries — shadcn/ui, Radix, Base UI, and others — to accelerate development without sacrificing design quality or customization.
Pre-built component libraries handle accessibility, keyboard navigation, and cross-browser behaviour. For a design engineer, the skill is choosing the right library and customizing it to match your design system without fighting its architecture.
Library Categories
Headless (Radix, Base UI, Ark UI, Ariakit)
Headless libraries provide behaviour without styling. They manage keyboard interactions, focus trapping, ARIA attributes, and state — but render no visual output. You supply all the styles, markup structure, and animation.
| Library | Approach | Best for |
|---|---|---|
| Radix UI | Primitives with React Context for shared state | Complex composites: dialogs, dropdowns, popovers, tabs. The most mature headless library |
| Base UI | Unstyled, fully headless components with a smaller API surface | Projects that want Radix-like behaviour with fewer abstraction layers |
| Ark UI | Headless components with framework-agnostic state machines | Projects using multiple frameworks (React + Vue) or wanting state-machine control |
| Ariakit | Lightweight accessibility primitives | Minimalist projects that need keyboard + ARIA without overhead |
import * as Dialog from "@radix-ui/react-dialog"
import { X } from "lucide-react"
export function Modal({ open, onOpenChange, title, children }) {
return (
<Dialog.Root open={open} onOpenChange={onOpenChange}>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/50 data-[state=open]:animate-in" />
<Dialog.Content className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-lg rounded-lg bg-background p-6 shadow-lg">
<Dialog.Title className="text-lg font-semibold">{title}</Dialog.Title>
{children}
<Dialog.Close asChild>
<button className="absolute right-4 top-4" aria-label="Close">
<X className="size-4" />
</button>
</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
)
}Copy-Paste (shadcn/ui)
shadcn/ui is not an npm package — it is a collection of components you copy into your project and own. The source code is your code. You can modify every line, delete what you do not need, and style everything with your Tailwind config.
| Characteristic | Detail |
|---|---|
| Distribution | CLI copies source files into components/ui/ |
| Styling | Tailwind classes + cn() utility, no CSS-in-JS |
| Customization | Edit the source directly — it is your code |
| Dependency | Radix UI primitives under the hood for interactive components |
| Themeability | CSS variables in globals.css for the built-in theme |
// components/ui/button.tsx
// You own this file. Change variants, sizes, styles directly.
import { cva } from "class-variance-authority"
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
// Add custom variants here:
// premium: "bg-gradient-to-r from-purple-600 to-blue-600 text-white",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
// Add custom sizes:
// xl: "h-12 rounded-md px-10 text-base",
},
},
}
)Ownership changes everything. With shadcn/ui, if a component needs different behaviour, you edit the file. No forking a library, no workaround wrapper, no waiting for upstream. This makes it the most design-engineer-friendly distribution model.
Styled + Themeable (Park UI, Chakra, Mantine)
These libraries ship with a default theme that you override. They provide complete, styled components out of the box with a theming API for customization.
| Library | Approach | Best for |
|---|---|---|
| Park UI | Built on Ark UI + Panda CSS, styled out of the box | Projects that want Radix-level behaviour with an opinionated look |
| Chakra | Component library with a comprehensive theme object | Rapid prototyping where speed matters more than bundle size |
| Mantine | 100+ hooks and components, all styled | Feature-heavy apps needing rich inputs, date pickers, notifications |
import { Dialog } from "@park-ui/dialog"
export function ConfirmDelete() {
return (
<Dialog.Root>
<Dialog.Trigger>Delete project</Dialog.Trigger>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Title>Are you sure?</Dialog.Title>
<Dialog.Description>
This action cannot be undone.
</Dialog.Description>
<Dialog.CloseTrigger>Cancel</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Dialog.Root>
)
}Choosing the Right Library
| Scenario | Recommended | Why |
|---|---|---|
| "I want full control over every pixel" | Radix UI or Ark UI + your own styles | Headless gives maximum control — you write all the CSS |
| "I want to move fast and customize later" | shadcn/ui | Copy into your project, modify as needed, no abstraction overhead |
| "I need 50+ component types out of the box" | Mantine or Park UI | Complete component sets for complex dashboards and admin panels |
| "I'm building a design system for multiple teams" | Radix UI + custom themed layer | Headless primitives as the foundation, your design system on top |
| "I'm prototyping and need something fast" | shadcn/ui init | A full UI in minutes; replace components as the project matures |
Customization Strategies
1. Token override (shadcn/ui)
Modify the CSS variable values in globals.css. Every shadcn component references these variables — change one value, update the entire system.
:root {
--radius: 0.5rem; /* Default: 0.5rem — bump for rounder UI */
--primary: oklch(0.43 0.12 236); /* Brand blue */
--primary-foreground: oklch(0.985 0.003 100);
--font-sans: "Inter", system-ui, sans-serif;
--font-mono: "JetBrains Mono", monospace;
}
.dark {
--primary: oklch(0.72 0.11 210);
--primary-foreground: oklch(0.14 0.018 250);
}2. Component override (headless libraries)
Wrap headless primitives in your own themed component. The design engineer pattern is: headless behaviour + your tokens = your design system.
// components/ui/popover.tsx
import * as Popover from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
export function PopoverContent({ className, ...props }) {
return (
<Popover.Portal>
<Popover.Content
className={cn(
"z-50 rounded-lg border bg-popover p-4 text-popover-foreground shadow-md",
"data-[state=open]:animate-in data-[state=closed]:animate-out",
className
)}
sideOffset={4}
{...props}
/>
</Popover.Portal>
)
}3. Extending shadcn/ui
Add new variants and components to your shadcn/ui directory. The component files are yours.
const buttonVariants = cva(/* ... */, {
variants: {
variant: {
// Existing variants...
premium: "bg-gradient-to-r from-purple-600 to-blue-600 text-white shadow-lg hover:from-purple-700 hover:to-blue-700",
},
},
})Combining Multiple Libraries
It is common to use multiple libraries together. For example: shadcn/ui for the component layer (Button, Input, Card, Dialog) plus Radix UI directly for complex patterns (Dropdown Menu, Toolbar, Collapsible) that shadcn does not provide.
// components/ui/ dropdown-menu.tsx
// shadcn/ui-style wrapper around Radix UI
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuContent = forwardRef<
React.ComponentRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
className={cn(
"z-50 min-w-32 rounded-lg border bg-popover p-1 shadow-md",
"data-[state=open]:animate-in data-[state=closed]:animate-out",
className
)}
sideOffset={4}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))Every library you add increases bundle size, maintenance burden, and the risk of conflicting style sheets. Before adding a second library, ask: can I extend the first one? A custom shadcn/ui component is smaller and more maintainable than a second library import for one component.
Agent Workflows with Design Systems
When using AI agents with a design system library, specificity matters. The agent needs to know which components are available and which conventions to follow.
This project uses shadcn/ui. The following components are installed:
Button, Input, Dialog, Select, Tabs, Badge, Card
Create a "ProjectSettingsDialog" that:
1. Opens via a Button with variant="outline" and text "Settings"
2. Contains a Dialog with Tabs: "General", "Members", "Billing"
3. Each tab has relevant form fields using shadcn/ui Input and Select
4. Uses CSS variable tokens from globals.css — no inline colors
5. TypeScript, React, "use client"
Reference the existing Dialog component at components/ui/dialog.tsx
for the exact import pattern and style conventions.| Agent prompt addition | What it enforces |
|---|---|
| "Use shadcn/ui [Component]" | Correct imports, available variants, consistent styling |
| "This project uses theme tokens — no inline colors or arbitrary values" | Design system adherence |
| "Follow the existing [component] style pattern in components/ui/" | File structure, naming, and export conventions |
| "Keep this as a Server Component if possible" | Correct boundary placement |
Component Development
Build components against their specifications with correct prop APIs, variant handling, accessibility attributes, and test coverage.
Forms & Input
Build production-grade forms with validation, error handling, loading states, and accessibility — from simple contact forms to complex multi-step workflows.