build-primitive
Back to list

PrototyperAI/build-primitive

Build Primitive

GitHub

Build foundational UI primitives from scratch with strong ARIA, keyboard, focus, and state handling.

AccessibilityInteractionSystemsTesting

No visual demo for this skill

Tooling or audit guidance without a UI surface to embed.

Skill markdown
# Prototyper UI — Building Primitive Components

How to build a 10/10 component from scratch when no Base UI primitive exists. ARIA contracts first, then keyboard navigation, then state management, then styling hooks, then form integration.

**When does this apply?** When you need a component that Base UI does not provide — Breadcrumb, Avatar, Badge, Card, Skeleton, Rating, Pagination, Stepper, Tag Input, File Upload, Calendar, Data Table, Carousel, Command Palette, Resizable Panels, Tree View, etc.

**Philosophy:** A primitive-quality component is invisible infrastructure. It handles every edge case so consumers never have to. Every interactive element must be keyboard-operable, every state must be announced to assistive technology, every animation must respect `prefers-reduced-motion`, and every form control must participate in native form submission.

---

## 1. Decision Gate

Before building from scratch, verify that no existing solution handles the hard parts.

```
Does a Base UI primitive exist for this component?
│
├── Yes → Use /create-component skill (wrap the primitive)
│
├── Partially (e.g., Collapsible exists but not Accordion)
│   └── Compose from existing primitives + custom logic
│       (Accordion = Collapsible + custom keyboard nav + ARIA)
│
└── No primitive exists
    │
    ├── Is it purely presentational? (Avatar, Badge, Card, Skeleton)
    │   └── Section 2 — Presentational Template (simple, no hooks)
    │
    ├── Is it a simple interactive? (Breadcrumb, Pagination, Rating)
    │   └── Section 3 — Interactive Template (ARIA + keyboard)
    │
    ├── Is it a form control? (Tag Input, File Upload, Color Picker)
    │   └── Section 4 — Form Control Template (ARIA + keyboard + form)
    │
    └── Is it a complex composite? (Calendar, Data Table, Tree View)
        └── Section 5 — Composite Template (all patterns combined)
```

### Can a third-party library handle the hard parts?

Before building complex interactivity from scratch, check if a headless library provides the state machine:

| Component              | Consider                       | Why                                            |
| ---------------------- | ------------------------------ | ---------------------------------------------- |
| Calendar / Date Picker | `react-aria` (date primitives) | Date math, locale, time zones                  |
| Data Table             | `@tanstack/react-table`        | Sorting, filtering, pagination, virtualization |
| Command Palette        | `cmdk`                         | Fuzzy search, keyboard nav, scoring            |
| Carousel               | `embla-carousel-react`         | Snap points, drag physics, loop                |
| Resizable Panels       | `react-resizable-panels`       | Drag, constraints, persistence                 |
| Virtual Lists          | `@tanstack/react-virtual`      | Windowing, dynamic heights                     |
| DnD / Sortable         | `@dnd-kit/core`                | Drag physics, collision, accessibility         |

If a headless library exists, wrap it with Prototyper UI styling patterns (data-slot, cn(), tokens, animation) rather than reimplementing the state machine.

---

## 2. Presentational Template

For components with no interactive behavior — just semantic HTML, styling, and slots.

Used by: Avatar, Badge, Card, Skeleton, Separator, AspectRatio

```tsx
"use client";

import { cva, type VariantProps } from "class-variance-authority";

import { cn } from "@/lib/utils";

// --- Variants ---

const avatarVariants = cva(
  [
    "relative inline-flex items-center justify-center overflow-hidden",
    "rounded-full bg-muted text-muted-foreground",
    "select-none",
  ],
  {
    variants: {
      size: {
        sm: "size-8 text-xs",
        default: "size-10 text-sm",
        lg: "size-12 text-base",
      },
    },
    defaultVariants: {
      size: "default",
    },
  },
);

// --- Root ---

function Avatar({
  className,
  size,
  ...props
}: React.ComponentProps<"span"> & VariantProps<typeof avatarVariants>) {
  return (
    <span
      data-slot="avatar"
      className={cn(avatarVariants({ size }), className)}
      {...props}
    />
  );
}

// --- Image (with fallback handling) ---

function AvatarImage({
  className,
  onError,
  ...props
}: React.ComponentProps<"img">) {
  return (
    <img
      data-slot="avatar-image"
      className={cn("aspect-square size-full object-cover", className)}
      onError={onError}
      {...props}
    />
  );
}

// --- Fallback ---

function AvatarFallback({ className, ...props }: React.ComponentProps<"span">) {
  return (
    <span
      data-slot="avatar-fallback"
      className={cn(
        "flex size-full items-center justify-center bg-muted font-medium",
        className,
      )}
      {...props}
    />
  );
}

export { Avatar, AvatarImage, AvatarFallback, avatarVariants };
```

### Presentational Rules

- Use semantic HTML elements (`<nav>`, `<ol>`, `<figure>`, `<time>`, `<hr>`) — never a `<div>` when