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.

22 installsAccessibilityInteractionSystemsTesting

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