--- name: author-ui-primitives description: Build idiomatic headless primitives in `packages/ui` for Remix. Use when authoring or revising accessible UI behavior, required structural styles, public primitive events, or shared primitive utilities under `packages/ui/src`. --- # Author UI Primitives Use this skill when building or revising `packages/ui` primitive APIs. The public `@remix-run/ui/*` entries are sourced from `packages/ui/src/*` and are not available through the `remix` package. ## Source Layout Each public primitive has a top-level `packages/ui/src/.ts` or `packages/ui/src/.tsx` entry. Common files: - `.ts` or `.tsx`: the public primitive entry and implementation. - `/README.md`: usage docs for the primitive. - `.test.ts` or `.test.tsx`: primitive tests. - `demos/.demo.tsx`: demo cases for the shared UI demo app. - `lib/*`: shared implementation-only behavior used by multiple primitives. Current examples: - low-level behavior modules: `anchor`, `popover`, `listbox` - composed behavior modules: `combobox`, `menu`, `select` - self-contained controls: `accordion`, `tabs`, `toggle` When adding or moving public entries, update both `exports` and `publishConfig.exports` in `packages/ui/package.json`. Public entries use the flat `./` path. ## Layering Choose the smallest layer that fits the job. - A headless primitive owns behavior, ARIA, registration, keyboard handling, refs, and public events. It exports named providers and mixins. - A shared primitive utility belongs in `src/lib` only when multiple primitives already need it. Good composition flows downward: - `select` composes `popover` and `listbox`. - `combobox` composes `popover` and `listbox`, then owns input text, filtering, and popup timing. - `menu` composes `popover`, outside interactions, typeahead, and hover aim. - `accordion`, `tabs`, and `toggle` keep their own primitive contexts. ## Public API Shape Primitive modules export named bindings, and callers namespace them at import time: ```tsx import * as select from '@remix-run/ui/select' function StatusSelect() { return () => (