Prototyper UI

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 easingcubic-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:

RoleTokenShadowComponents
Surfacebg-surfaceshadow-surfaceCards, panels, tabs
Overlaybg-overlayshadow-overlayDialogs, popovers, menus, dropdowns
Fieldbg-field-backgroundshadow-fieldInputs, 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 surface
  • bg-surface-secondary — nested containers (derived via color-mix)
  • bg-surface-tertiary — deeper nesting (derived via color-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 + ...props spread on every component
  • Consistent styling: Tailwind utilities, cn() for merging, data-slot for 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:

TierLight ModeDark ModeUsed For
shadow-surfaceMulti-layer subtle shadowNone (tonal contrast instead)Cards, panels
shadow-fieldSubtle shadow + 1px edgeNoneForm inputs
shadow-overlayHeavy multi-layer shadowSubtle 1px inset white glowPopovers, menus, dialogs

What We Derive vs. Define Manually

Manually definedDerived 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 backgroundsBorder/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

Aspectshadcn/uiHeroUI v3Prototyper UI
Primitive libraryRadix UIReact AriaBase UI
Ownership modelCopy-pastePackage dependencyCopy-paste
Color spaceHSLOKLCHOKLCH
Surface systemAd-hoc (3 tokens)Role-based (surface/overlay/field)Role-based (surface/overlay/field)
Shadow systemSize-based (sm/md/lg)Semantic (surface/overlay)Semantic (surface/field/overlay)
AnimationCSS + TailwindCSS + GPU acceleratedCSS + Tailwind
Component APIFlat exportsCompound (dot notation)Flat exports
StylingTailwind-in-componentBEM + CSS layersTailwind-in-component
Dark mode shadowsSame as lightZeroed / inset glowZeroed / inset glow

On this page