Design Principles
Core principles that guide Prototyper UI's design and development.
Prototyper UI follows a set of core principles that prioritize ownership, beauty, consistency, and developer experience. These principles shape every decision — from token naming to component API design.
You Own The Code
Components are files you copy into your project, read, edit, and extend. There is no hidden abstraction layer, no runtime CSS-in-JS, no provider you must wrap your app in. You install a component, it lands in your codebase, and it's yours.
This is the shadcn philosophy: the component library is a starting point, not a dependency.
# Install a component — it copies source into your project
npx shadcn@latest add https://prototyper-ui.com/r/button.json// You get a real file you can edit
// registry/ui/button.tsx
function Button({ className, variant, size, ...props }) {
return (
<ButtonPrimitive
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}Why this matters: No version lock-in. No waiting for upstream fixes. No fighting abstraction layers to customize behavior. You change the file, you change the component.
Base UI Native
Every interactive component is built on Base UI — the unstyled primitive library from the Material UI team. Base UI handles the hard problems (focus management, keyboard navigation, ARIA attributes, scroll locking, portal rendering) so we focus entirely on design.
// Base UI provides the behavior, we provide the style
import { Select as SelectPrimitive } from "@base-ui/react/select"
function SelectTrigger({ className, children, ...props }) {
return (
<SelectPrimitive.Trigger
className={cn("flex h-9 w-full items-center ...", className)}
{...props}
>
{children}
<SelectPrimitive.Icon render={<ChevronsUpDownIcon />} />
</SelectPrimitive.Trigger>
)
}What Base UI gives us for free:
- WCAG 2.1 AA compliance across all components
- Keyboard navigation (arrow keys, type-ahead, focus trapping)
- Screen reader announcements via ARIA attributes
- Data attributes (
data-open,data-disabled,data-highlighted) as a public styling contract - Portal rendering for overlays
- Scroll lock for modals
Beautiful by Default
Components should look exceptional without any customization. The design system uses:
- OKLCH color space — perceptually uniform,
color-mix()works predictably for derived states - Multi-layer shadows — three stacked shadow layers simulate realistic light (subtle edge shadow is critical for dark mode)
- Role-based surfaces — surfaces, overlays, and fields each have dedicated tokens and shadow tiers
- Fluid easing —
cubic-bezier(0.32, 0.72, 0, 1)as the signature curve (Apple-style deceleration) - Gradient buttons — the default button uses a three-layer gradient system with primary color ramps
Beauty is not decoration — it's the result of precise tokens, consistent spacing, and careful shadow/color relationships.
Role-Based Surfaces
Inspired by HeroUI v3's surface system, surfaces are categorized by their role in the UI, not by arbitrary elevation numbers:
| Role | Token | Shadow | Components |
|---|---|---|---|
| Surface | bg-surface | shadow-surface | Cards, panels, tabs |
| Overlay | bg-overlay | shadow-overlay | Dialogs, popovers, menus, dropdowns |
| Field | bg-field-background | shadow-field | Inputs, selects, comboboxes |
Light mode uses shadows for depth. Dark mode uses background lightness stepping — shadows are reduced or zeroed because dark surfaces already show depth through tonal difference.
Three surface tiers handle nesting (card-in-card):
bg-surface— primary surfacebg-surface-secondary— nested containers (derived viacolor-mix)bg-surface-tertiary— deeper nesting (derived viacolor-mix)
CSS Utilities for Consistency
Every component references shared CSS utilities instead of reimplementing focus, disabled, and invalid patterns. This guarantees identical behavior everywhere:
/* Focus ring — buttons, links, interactive elements */
@utility focus-ring {
outline: 2px solid var(--color-ring);
outline-offset: 2px;
}
/* Focus ring — form fields (sits on border, no offset) */
@utility focus-field-ring {
outline: 2px solid var(--color-ring);
outline-offset: -1px;
}
/* Disabled state — universal across all components */
@utility status-disabled {
opacity: 0.5;
pointer-events: none;
cursor: not-allowed;
}// Every component uses the same utilities
<Button className="focus-visible:focus-ring disabled:status-disabled" />
<Input className="focus-within:focus-field-ring data-disabled:status-disabled" />
<Toggle className="focus-visible:focus-ring data-disabled:status-disabled" />Why not per-component styles? Because 5 different focus ring implementations (different widths, colors, offsets) is how inconsistency creeps in. One utility, one look, everywhere.
Flat Exports, shadcn-Compatible
Components use flat named exports — no Object.assign compound patterns, no dot notation. This is what shadcn users know and what LLMs generate best.
// Named exports — clear, greppable, tree-shakeable
import {
Select,
SelectTrigger,
SelectContent,
SelectItem,
SelectValue,
} from "@/components/ui/select"
<Select>
<SelectTrigger>
<SelectValue placeholder="Pick one" />
</SelectTrigger>
<SelectContent>
<SelectItem value="a">Option A</SelectItem>
<SelectItem value="b">Option B</SelectItem>
</SelectContent>
</Select>Every component root and sub-component has a data-slot attribute for external targeting:
/* Target any component part from outside */
[data-slot="select-trigger"] { /* ... */ }
[data-slot="select-content"] { /* ... */ }Progressive Disclosure
Components work with minimal props and scale up as requirements grow. The simplest usage should be a single line; advanced usage reveals more knobs.
// Level 1: Minimal — just works
<ComboboxInput placeholder="Search..." />
// Level 2: Control what's visible
<ComboboxInput placeholder="Search..." showClear showTrigger={false} />
// Level 3: Full composition
<InputGroup>
<ComboboxPrimitive.Input className="..." />
<InputGroupAddon align="inline-end">
<ComboboxClear />
<ComboboxTrigger />
</InputGroupAddon>
</InputGroup>CSS-First Animation
No JavaScript animation libraries. All transitions and animations use CSS, data attributes for state, and prefers-reduced-motion support.
/* Overlay enter/exit via Tailwind animate utilities */
data-open:animate-in data-closed:animate-out
data-closed:fade-out-0 data-open:fade-in-0
data-closed:zoom-out-95 data-open:zoom-in-95
/* Interactive elements use transition-colors */
transition-colors duration-200
/* Easing tokens from the design system */
--ease-out-fluid: cubic-bezier(0.32, 0.72, 0, 1);
--ease-smooth: cubic-bezier(0.4, 0, 0.2, 1);Reduced motion is respected automatically. All components use Tailwind's motion-reduce: variant which maps to both prefers-reduced-motion media query and data-reduce-motion attribute.
Type Safety
Full TypeScript with strict mode. Component props are derived from Base UI's type definitions, extended where needed:
// Props extend Base UI types — full IntelliSense
function SelectTrigger({
className,
size = "default",
children,
...props
}: SelectPrimitive.Trigger.Props & {
size?: "sm" | "default" | "lg"
}) {
// ...
}Variant types are generated by class-variance-authority:
import { cva, type VariantProps } from "class-variance-authority"
const buttonVariants = cva("...", {
variants: {
variant: { default: "...", destructive: "...", outline: "..." },
size: { default: "h-9", sm: "h-8", lg: "h-10" },
},
})
// VariantProps<typeof buttonVariants> gives you { variant?: "default" | ... }LLM-Friendly
Prototyper UI is designed to be generated correctly by AI. Every component follows the same patterns:
- Consistent file structure:
"use client", imports, component functions, exports - Consistent naming:
ComponentName+ComponentNameSubPart(e.g.,Select,SelectTrigger,SelectContent) - Consistent props:
className+...propsspread on every component - Consistent styling: Tailwind utilities,
cn()for merging,data-slotfor identification - Rich examples: Every component has multiple examples showing common patterns
- Structured docs: MDX pages with live previews, installation commands, and usage snippets
When an LLM reads one component, it understands all of them.
Design Token Architecture
Color Space: OKLCH
All colors use the OKLCH color space with color-mix(in oklab) for derived states:
/* Base token (manually defined per theme) */
--primary: 39.11% 0.084 240.8;
/* Derived hover state (90% base + 10% foreground) */
--primary-hover: color-mix(in oklab, oklch(var(--primary)) 90%, oklch(var(--primary-foreground)) 10%);
/* Derived soft variant (15% opacity) */
--primary-soft: color-mix(in oklab, oklch(var(--primary)) 15%, transparent);Why OKLCH? Perceptually uniform — a 10% mix shift looks like 10% regardless of the base color. HSL-based mixing produces unpredictable results across hues.
Shadow System
Three semantic shadow tiers, mode-adaptive:
| Tier | Light Mode | Dark Mode | Used For |
|---|---|---|---|
shadow-surface | Multi-layer subtle shadow | None (tonal contrast instead) | Cards, panels |
shadow-field | Subtle shadow + 1px edge | None | Form inputs |
shadow-overlay | Heavy multi-layer shadow | Subtle 1px inset white glow | Popovers, menus, dialogs |
What We Derive vs. Define Manually
| Manually defined | Derived via color-mix() |
|---|---|
Base colors (--primary, --destructive, etc.) | Hover states (90% base + 10% foreground) |
Foreground colors (--primary-foreground, etc.) | Soft variants (15% base + transparent) |
Color ramps (--primary-light/middle/dark) | Surface tiers (secondary, tertiary) |
| Surface/overlay/field backgrounds | Border/separator progressions |
Why not derive everything? Tested with 5 brand colors — ramp derivation collapses for light colors, washes chroma for dark colors. Foreground can't be auto-derived (green needs dark text in both modes). Manual definition where it matters, derivation where it's safe.
Comparison with Other Libraries
| Aspect | shadcn/ui | HeroUI v3 | Prototyper UI |
|---|---|---|---|
| Primitive library | Radix UI | React Aria | Base UI |
| Ownership model | Copy-paste | Package dependency | Copy-paste |
| Color space | HSL | OKLCH | OKLCH |
| Surface system | Ad-hoc (3 tokens) | Role-based (surface/overlay/field) | Role-based (surface/overlay/field) |
| Shadow system | Size-based (sm/md/lg) | Semantic (surface/overlay) | Semantic (surface/field/overlay) |
| Animation | CSS + Tailwind | CSS + GPU accelerated | CSS + Tailwind |
| Component API | Flat exports | Compound (dot notation) | Flat exports |
| Styling | Tailwind-in-component | BEM + CSS layers | Tailwind-in-component |
| Dark mode shadows | Same as light | Zeroed / inset glow | Zeroed / inset glow |