# PettyUI Design Specification **Date:** 2026-03-28 **Status:** Draft **Author:** Mats Bosson + Claude ## Vision PettyUI is a pure headless, AI-native UI component library for SolidJS. It provides zero-opinion styling, bulletproof accessibility, and an API designed primarily for AI coding agents to generate correctly — with humans as the secondary audience. The north star: **the easiest component library for AI to use, and the most pleasant for humans when they need to intervene.** --- ## Core Principles 1. **AI-first, human-friendly** — Every API decision optimizes for LLM code generation correctness. Consistent patterns, explicit types, machine-readable docs, and no implicit contracts. 2. **Pure headless** — Zero styling, zero theme, zero CSS. Components provide behavior, accessibility, and state. Users own all visual decisions. 3. **Progressive disclosure** — Simple props API for the 90% case. Compound component API for full control. Hook-level escape hatches for the 2%. 4. **Solid-native** — Built from scratch on SolidJS signals and context. No framework translation layers, no state machines, no abstraction overhead. 5. **SSR-correct from day one** — Every component works with SolidStart SSR. Deterministic IDs, no hydration mismatches, proper portal handling. 6. **Single-purpose components** — No overloaded behavior via boolean flags. `Select` and `MultiSelect` are separate components. AI never guesses modes. 7. **Pit of success** — Wrong usage is hard. Right usage is obvious. Errors are specific, actionable, and include fix instructions. --- ## Architecture ### Package Structure Single npm package with sub-path exports. Monorepo internally, single `pettyui` package externally. ``` pettyui/ ├── src/ │ ├── primitives/ # Internal shared building blocks (NOT exported) │ │ ├── create-controllable-signal.ts │ │ ├── create-disclosure-state.ts │ │ ├── create-register-id.ts │ │ ├── create-collection.ts │ │ ├── create-list-navigation.ts │ │ ├── create-typeahead.ts │ │ └── create-form-control.ts │ │ │ ├── utilities/ # Standalone exported utilities │ │ ├── presence/ # — keeps exiting elements mounted during animation │ │ ├── dismiss/ # Escape key, click-outside, focus-outside handling │ │ ├── focus-trap/ # Focus trapping for modals │ │ ├── scroll-lock/ # Body scroll prevention │ │ ├── portal/ # SSR-safe portal │ │ └── visually-hidden/ # Screen-reader-only content │ │ │ ├── components/ # All UI components │ │ ├── accordion/ │ │ ├── alert-dialog/ │ │ ├── checkbox/ │ │ ├── collapsible/ │ │ ├── combobox/ │ │ ├── context-menu/ │ │ ├── dialog/ │ │ ├── drawer/ │ │ ├── dropdown-menu/ │ │ ├── hover-card/ │ │ ├── listbox/ │ │ ├── menubar/ │ │ ├── multi-select/ │ │ ├── navigation-menu/ │ │ ├── number-field/ │ │ ├── pagination/ │ │ ├── popover/ │ │ ├── progress/ │ │ ├── radio-group/ │ │ ├── select/ │ │ ├── separator/ │ │ ├── slider/ │ │ ├── switch/ │ │ ├── tabs/ │ │ ├── text-field/ │ │ ├── toast/ │ │ ├── toggle/ │ │ ├── toggle-group/ │ │ └── tooltip/ │ │ │ ├── schemas/ # Zod schemas for every component (source of truth) │ │ ├── dialog.schema.ts │ │ ├── select.schema.ts │ │ └── ... │ │ │ ├── ai/ # AI integration layer │ │ ├── generate-prompt.ts # Generates LLM system prompt from schemas │ │ ├── mcp/ # MCP server for Claude Code, Cursor, etc. │ │ └── catalog.ts # json-render compatible catalog │ │ │ └── index.ts │ ├── llms.txt # Machine-readable component reference for AI ├── openui.yaml # OpenUI spec for tooling ├── package.json └── tsconfig.json ``` ### Distribution ```bash # Install npm install pettyui # Import (sub-path for tree-shaking) import { Dialog } from "pettyui/dialog"; import { Select } from "pettyui/select"; import { Presence } from "pettyui/presence"; # CLI for copying styled implementations into user projects npx pettyui add dialog --style tailwind npx pettyui add select --style vanilla ``` Sub-path exports ensure tree-shaking works perfectly. Each component is independently importable. The CLI copies styled example implementations (using PettyUI headless primitives) into the user's project for full ownership — the shadcn model. ### Dependencies Minimal, deliberate: - `@floating-ui/dom` — Positioning for popovers, tooltips, dropdowns, selects - `solid-js` (peer dependency) — SolidJS core **Build/dev only (not shipped to users):** - `zod` 4.x — Schema definitions for AI tooling, type generation, and validation. Zod is used at build time to generate `llms.txt`, `openui.yaml`, and system prompts. It is NOT a runtime dependency of the component library itself. Uses Zod 4 (stable since early 2026) for improved performance and smaller types. No other runtime dependencies. Utilities (focus-trap, scroll-lock, dismiss, presence) are built in-house and exported standalone. --- ## Internal State Pattern Every component follows the same internal architecture: ### Signals + Context ```tsx // 1. Root creates signals and provides context function DialogRoot(props: DialogProps) { const [open, setOpen] = createControllableSignal({ value: () => props.open, defaultValue: () => props.defaultOpen ?? false, onChange: props.onOpenChange, }); const context: InternalDialogContext = { open, setOpen, modal: () => props.modal ?? true, contentId: createSignal(undefined), titleId: createSignal(undefined), descriptionId: createSignal(undefined), }; return ( {props.children} ); } ``` ### Dual Context (Public + Internal) Learned from corvu. Every component exposes two context layers: - **Internal context** — Includes setters, registration functions, refs. Never exposed to consumers. Used only by component parts internally. - **Public context** — Read-only accessors. Exported via `Dialog.useContext()`. Safe for consumers to build custom parts. ```tsx // Internal — only accessible to Dialog.Content, Dialog.Trigger, etc. const InternalDialogContext = createContext(); // Public — accessible to consumers via Dialog.useContext() const DialogContext = createContext(); // Public context only exposes read-only accessors type DialogContextValue = { open: Accessor; modal: Accessor; }; ``` ### Controllable Signal Primitive The `createControllableSignal` primitive handles controlled vs uncontrolled state for every stateful component: ```tsx function createControllableSignal(options: { value: Accessor; // Controlled value (if provided) defaultValue: Accessor; // Uncontrolled default onChange?: (value: T) => void; // Callback on change }): [Accessor, (value: T) => void]; ``` When `value()` is not `undefined`, the component is controlled. Otherwise it manages its own internal signal. This pattern is used by every stateful component: Dialog (open), Select (value), Accordion (expandedItems), Tabs (activeTab), etc. ### ID Registration Child parts register their IDs with the root context for ARIA linking: ```tsx // In Dialog.Title const context = useInternalDialogContext(); const id = createUniqueId(); // SSR-deterministic createEffect(() => context.titleId[1](id)); onCleanup(() => context.titleId[1](undefined)); // In Dialog.Content — uses the registered ID
``` `createUniqueId()` from SolidJS produces deterministic IDs for SSR hydration. --- ## Component API Design ### Dual-Layer API: Simple + Compound Every component offers two API surfaces. The simple API covers 90% of use cases with minimal cognitive load. The compound API provides full compositional control. #### Layer 1: Simple Props API ```tsx // Dialog — 2 props to get a working accessible modal Confirm deletion

This cannot be undone.

OK
// Select — flat props, works immediately // Controlled — consumer manages state for multi-value selection. Docs: https://pettyui.dev/components/multi-select ``` These errors serve both humans and AI agents. AI can parse the fix instruction and self-correct. --- ## AI Integration Layer PettyUI ships first-class tooling for AI code generation. This is not an afterthought — it is a core design pillar. ### 1. `llms.txt` (Ships in Package) Located at the package root. Concise, structured component reference optimized for LLM consumption. ``` # PettyUI Component Reference ## Dialog Modal overlay that blocks interaction with the page. ### Simple API Title

Content here

Close
### Props - open: boolean — Whether dialog is visible (controlled) - defaultOpen: boolean (default: false) — Initial state (uncontrolled) - onOpenChange: (open: boolean) => void — Called when state should change - modal: boolean (default: true) — Whether to trap focus and block outside interaction ### Parts (Compound API) - Dialog.Trigger — Button that opens the dialog. Use `as` prop to customize element. - Dialog.Portal — Portals content to document.body. Override with `target` prop. - Dialog.Overlay — Backdrop behind dialog. Auto-generated if not provided. - Dialog.Content — The dialog panel. Receives focus on open. - Dialog.Title — Accessible title. Linked via aria-labelledby. - Dialog.Description — Accessible description. Linked via aria-describedby. - Dialog.Close — Button that closes the dialog. ### Keyboard - Escape: Closes the dialog - Tab: Cycles focus within dialog (when modal) ### Data Attributes - data-state="open" | "closed" on Trigger, Content, Overlay - data-opening / data-closing on Content, Overlay (during animations) ``` ### 2. `openui.yaml` (Repository Root) Machine-readable component specification following the OpenUI format: ```yaml name: PettyUI version: 0.1.0 framework: solid components: Dialog: description: Modal overlay that blocks interaction with the page. props: open: type: boolean description: Whether the dialog is currently open. defaultOpen: type: boolean default: false description: Initial open state for uncontrolled usage. onOpenChange: type: function description: Called when the open state should change. modal: type: boolean default: true description: Whether to trap focus and block outside interaction. parts: [Trigger, Portal, Overlay, Content, Title, Description, Close] example: | Confirm OK ``` ### 3. MCP Server ```bash npx pettyui mcp ``` Exposes to AI agents: - **Component list** with full props, types, descriptions - **Usage examples** per component (simple + compound) - **Installed components** (reads user's project config) - **Search** across components and props - **Validation** — check if generated code uses valid props/parts ### 4. System Prompt Generator ```tsx import { generatePrompt } from "pettyui/ai"; const systemPrompt = generatePrompt({ components: ["dialog", "select", "tabs", "checkbox"], styling: "tailwind", // or "vanilla" or "panda" }); // Returns a string containing complete component reference // derived from Zod schemas — always up to date ``` ### 5. CLI for Source Ownership (Separate Package) The CLI is a separate package (`pettyui-cli` or `create-pettyui`) — not part of the core headless library. It is a convenience tool, not a requirement. ```bash npx pettyui add dialog --style tailwind npx pettyui add select --style vanilla npx pettyui add all --style tailwind ``` Copies styled example implementations into the user's project. These files: - Import PettyUI headless primitives as a dependency - Add styling (Tailwind classes, CSS, etc.) - Are fully owned and editable by the developer - Are readable by AI agents (source code in the project, not node_modules) **Scope note:** The CLI is a Wave 2+ deliverable. The core headless library ships first without it. ### 6. json-render Catalog Optional export for generative UI applications: ```tsx import { catalog } from "pettyui/catalog"; // AI generates JSON conforming to catalog schema // json-render validates and renders through PettyUI components const ui = await model.generate({ schema: catalog.schema, prompt: "Create a settings form with email and notification toggle", }); render(catalog, ui.json); ``` --- ## Component Inventory ### Wave 1: Breadth (Simple + Medium Complexity) Ship first. Proves the architecture, establishes patterns. | Component | Archetype | Complexity | |-----------|-----------|------------| | Accordion | Collection | Medium | | AlertDialog | Overlay | Simple | | Checkbox | Form Control | Simple | | Collapsible | Form Control | Simple | | Dialog | Overlay | Medium | | Drawer | Overlay | Medium | | HoverCard | Overlay | Simple | | Pagination | Collection | Medium | | Popover | Overlay | Medium | | Progress | Form Control | Simple | | RadioGroup | Collection | Simple | | Separator | — | Trivial | | Slider | Form Control | Medium | | Switch | Form Control | Simple | | Tabs | Collection | Medium | | TextField | Form Control | Simple | | Toggle | Form Control | Simple | | ToggleGroup | Collection | Simple | | Tooltip | Overlay | Simple | | VisuallyHidden | Utility | Trivial | ### Wave 2: Depth (Complex Interaction Components) Ship after Wave 1 is stable. These components stress-test every primitive. | Component | Archetype | Complexity | |-----------|-----------|------------| | Select | Collection + Overlay | High | | MultiSelect | Collection + Overlay | High | | Combobox | Collection + Overlay | High | | DropdownMenu | Collection + Overlay | High | | ContextMenu | Collection + Overlay | High | | Menubar | Collection | High | | NavigationMenu | Collection + Overlay | High | | NumberField | Form Control | Medium | | Toast | Overlay | High | | Listbox | Collection | Medium | ### Wave 3: Advanced (High-Demand Gaps) Components neither Kobalte nor corvu provide well. Market differentiators. | Component | Notes | |-----------|-------| | Command | Command palette (Cmd+K pattern) | | DatePicker | Calendar-based date selection | | Calendar | Standalone calendar display | | TreeView | Hierarchical list with expand/collapse | | DataTable | Headless table with sort/filter/pagination primitives | | Carousel | Slide-based content display | | Stepper | Multi-step wizard flow | | ColorPicker | Color selection with swatch/slider/area | | FileUpload | Drop zone + file list management | --- ## SSR Strategy ### Deterministic IDs Use SolidJS `createUniqueId()` which produces deterministic IDs during SSR. Server and client generate the same IDs, preventing hydration mismatches. ### Portal Handling The `` utility detects SSR and renders content inline during server rendering, then moves it to the target container on hydration. ### No Browser-Only APIs at Import Time All browser APIs (`document`, `window`, `MutationObserver`, `ResizeObserver`) are accessed lazily inside `onMount` or guarded by `isServer` checks. Importing any PettyUI component in an SSR context never throws. ### SolidStart Compatibility - Test against every SolidStart release - Export conditions: `"solid"` for SolidJS compilation, `"import"` for ESM, `"require"` for CJS - No use of APIs that differ between Solid's server and client builds --- ## Bundle Size Strategy ### Target Each component should add less than **5KB gzipped** to a production bundle. Complex components (Select, Combobox) may be up to **10KB gzipped**. For reference: Radix Dialog is ~9.2KB gzipped. Kobalte Popover is ~35KB gzipped. We target Radix-level or better. ### How - No heavy dependencies (no `@internationalized/*`, no state machine libraries) - Shared primitives are tiny (signals + context, not state machines) - `@floating-ui/dom` is the only significant dependency (~3KB gzipped) and only loaded by components that need positioning - Sub-path exports ensure only imported components are bundled - Development-only error messages are stripped via `process.env.NODE_ENV` dead code elimination --- ## Testing Strategy ### Unit Tests Every component gets: - Rendering tests (basic, with props, with children) - ARIA attribute verification - Keyboard interaction tests - Controlled and uncontrolled state tests - SSR rendering tests (no hydration mismatch) ### Accessibility Testing - Automated: `axe-core` on every component in every state (via Vitest) - E2E (Wave 2+): Playwright tests across Chromium, Firefox, and WebKit — verifies real keyboard navigation, focus management, and ARIA behavior in actual browsers including Safari - Manual: Test with VoiceOver (macOS/iOS), NVDA (Windows), TalkBack (Android) - Keyboard-only navigation tests for every interactive component ### Bundle Size Monitoring CI checks bundle size on every PR. Any component exceeding the target triggers a warning. --- ## Documentation Strategy Three audiences, three formats: 1. **AI agents** — `llms.txt`, `openui.yaml`, MCP server, Zod schemas. Machine-readable, always derived from source of truth. 2. **Developers getting started** — Website with simple API examples first, compound API second. Every component has a copy-paste example that works on first try. 3. **Design system authors** — API reference with full TypeScript types, public context docs, composition patterns, and styling guides. --- ## Development Tooling Pinned versions as of 2026-03-28: - **Runtime:** SolidJS 1.9.x (stable). Architecture is Solid 2.0-ready — no use of deprecated APIs. - **Build:** Vite 8.x with `vite-plugin-solid` 2.11.x - **Package manager:** pnpm 10.x - **Unit testing:** Vitest 4.x + `@solidjs/testing-library` 0.8.x - **E2E testing:** Playwright (Wave 2+) — real browser a11y testing across Chromium, Firefox, and WebKit/Safari. Critical for verifying keyboard navigation and screen reader behavior in Safari/VoiceOver. - **Type checking:** TypeScript 6.x strict mode - **Formatting:** Biome — 25x faster than Prettier, handles formatting + general lint rules - **Linting:** Minimal ESLint config with `eslint-plugin-solid` only — Biome has no Solid-specific equivalent, so ESLint handles framework-specific reactive rules (signal access, effect dependencies). All other lint rules handled by Biome. - **Bundling:** tsdown 0.x for package builds (ESM + CJS + `.d.ts`) — Rust-based successor to tsup (same author), 3-5x faster, Rolldown engine (same as Vite 8). Near-identical config to tsup. If edge cases arise, tsup 8.x is the drop-in fallback. - **Schemas:** Zod 4.x (build-time only) — `z.toJSONSchema()` directly generates OpenUI spec - **Positioning:** @floating-ui/dom 1.7.x - **CI:** GitHub Actions — biome check, eslint (solid rules), type-check, vitest, bundle size check, SSR verification - **Docs site:** SolidStart + MDX ### SolidJS 2.0 Readiness Solid 2.0 is in beta (2.0.0-beta.4 as of writing). PettyUI targets Solid 1.9.x stable but: - Avoids deprecated Solid 1.x APIs (`createResource` in favor of patterns that map to 2.0's `createAsync`) - Uses `createUniqueId`, `createSignal`, `createEffect`, `createContext` — all stable across both versions - Will ship a Solid 2.0 compatibility update when 2.0 reaches stable release - CI runs tests against both `solid-js@latest` and `solid-js@next` to catch breaking changes early --- ## Competitive Positioning | Feature | Kobalte | corvu | PettyUI | |---------|---------|-------|---------| | Components | 50+ | 9 | 25+ (Wave 1-2) | | Bundle per component | ~35KB gzip | Small | <5KB gzip target | | Simple props API | No | No | Yes | | Compound API | Yes | Yes | Yes | | `as` prop | Yes | Yes | Yes | | Children-as-function | No | Yes | Yes | | Dual context | No | Yes | Yes | | Animation data attrs | No | Yes | Yes | | Presence primitive | External | External | Built-in | | SSR-first | Fragile | Unknown | Yes | | AI integration | None | None | llms.txt, MCP, OpenUI, CLI | | Zod schemas | No | No | Yes | | Typed error messages | No | No | Yes | | Extracted utilities | No | Yes | Yes | PettyUI's unique differentiators: 1. **Dual-layer API** — Simple props + compound components 2. **AI-native** — First component library designed for LLM code generation 3. **Pit of success errors** — Specific, actionable, parseable by AI 4. **Single-purpose components** — No overloaded boolean flags 5. **Bundle size** — Radix-level footprint in the SolidJS ecosystem