Primitive Libraries logo

Primitive Libraries

Organization
staple-lab
primitive-libraries

Use when building components on a headless primitive library - Base UI, Radix, React Aria Components, Ark UI - or deciding between them. Covers the composition/polymorphism API of each, the accessibility contract you inherit, state data-attributes for styling, portals, focus management, and what you must never re-implement by hand.

Overview

Publisherstaple-lab
Repositorydesign-system-skills
Skill nameprimitive-libraries
Stars
7
Forks
0
Bundled files
Instructions only
LicenseMIT
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • Self-contained

    Everything the model needs lives in the instructions — no extra files to sync.

  • Open source

    Published by staple-lab on GitHub. Read the source before you install it.

Installation

Install the Primitive Libraries AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
    https://github.com/staple-lab/design-system-skills/tree/main/plugins/design-system/skills/primitive-libraries
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/staple-lab/design-system-skills.git /tmp/design-system-skills
mkdir -p .claude/skills
cp -r /tmp/design-system-skills/plugins/design-system/skills/primitive-libraries .claude/skills/primitive-libraries
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Primitive Libraries in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Primitive Libraries on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Primitive Libraries is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

Headless primitive libraries

The primitive layer owns behaviour and accessibility; you own appearance. The single most valuable thing it buys you is the stuff that is invisible when correct and career-limiting when wrong: focus trapping, typeahead, scroll locking, aria wiring, RTL, touch targets, screen-reader announcements, and the fifty keyboard edge cases in a listbox.

Never re-implement a focus trap, a listbox, a combobox, a date picker or a dialog by hand. If the primitive library ships it, wrap it. The correct version of any of these is thousands of lines and years of bug reports; the version you write in an afternoon works for you on a MacBook with a mouse and fails for everyone else.

Picking one

One, and only one, per system. Two primitive libraries means two focus-management philosophies fighting inside the same portal — a real and very confusing class of bug.

  • Base UI (@base-ui/react) — the default for new systems. Stable 1.x, full-time maintenance, built with the benefit of Radix's lessons.
  • Radix (@radix-ui/react-*) — the incumbent. Widest ecosystem and the most examples in the wild (including in AI training data, which genuinely matters for agent-written code).
  • React Aria Components (react-aria-components) — the strongest a11y/i18n story. Choose it for enterprise, government, regulated, or anything with a VPAT.
  • Ark UI (@ark-ui/react) — when you must ship the same system to React and Vue/Solid/Svelte.

Full comparison in design-system-architect/references/stack-menu.md.

The composition API — the decision that leaks everywhere

Each library solves "render my own element instead of yours" differently, and this choice shows up in every component you write. Match the repo's existing convention; record it in design-system.config.json under conventions.polymorphism.

tsx
// Base UI — `render` takes an element or a function
<Menu.Item render={<Link href="/settings" />}>Settings</Menu.Item>
<Menu.Item render={(props, state) => <Link {...props} data-hot={state.highlighted} />} />

// Radix — `asChild` clones the single child and merges props onto it
<DropdownMenu.Item asChild>
  <Link href="/settings">Settings</Link>
</DropdownMenu.Item>

// React Aria — render props on state, plus named slots
<MenuItem href="/settings">
  {({ isSelected }) => <>Settings {isSelected && <CheckIcon />}</>}
</MenuItem>

asChild has one sharp edge worth knowing: it requires exactly one child element and merges props onto it, so a fragment, a text node, or a component that does not forward refs and spread props will fail — sometimes silently. Base UI's render avoids the fragment trap but has the same ref/spread requirement on the target.

Styling hooks: data-* state attributes

All three expose state as data attributes on the rendered element. Style from those, not from React state — no useState mirror of "is this open", no conditional class strings. The primitive is the source of truth, and CSS reading the DOM directly is both simpler and impossible to desync.

css
.button[data-disabled] { opacity: .5; pointer-events: none; }
.item[data-highlighted] { background: var(--ds-color-bg-subtle-hover); }
.content[data-state='open'] { animation: enter var(--ds-duration-normal) var(--ds-easing-decelerate); }
.content[data-state='closed'] { animation: exit var(--ds-duration-fast) var(--ds-easing-accelerate); }
.popover[data-side='top'] { transform-origin: bottom center; }

Attribute names differ by library — Radix leans on data-state="open|closed", Base UI exposes data-open plus per-part attributes, React Aria uses data-pressed/data-focus-visible/data-selected. Check the component's docs rather than guessing; a selector that never matches is silent.

The data-side / data-align attributes on floating content are the ones people miss: they are how a popover animates from the trigger rather than from a fixed direction, and it is a large perceived-quality difference for two lines of CSS.

What you must get right when wrapping

  1. Forward the ref. Consumers need it for focus management, measurement and third-party integrations. In React 19 ref is a normal prop and forwardRef is no longer required, but if the codebase targets React 18 you still need it.
  2. Spread ...rest onto the element the consumer thinks they are styling — usually the outermost rendered node. Swallowing unknown props breaks aria-label, data-testid, analytics attributes and every escape hatch at once.
  3. Merge className, don't replace it. Use clsx/cn. A component that drops the consumer's className has no escape hatch, and people will fork it.
  4. Do not fight the primitive's a11y. If you find yourself adding role or aria-* to a primitive's output, you are probably about to break it. The exception is genuine app-level context (aria-label on an icon-only button) — that comes from the consumer.
  5. Keep the compound structure. Flattening <Dialog.Root><Dialog.Trigger/><Dialog.Content/></Dialog.Root> into <Dialog trigger={...} content={...}/> looks tidier and costs you every composition case — and consumers will need one within a month. Wrap the parts, keep the shape.
  6. Portals + stacking. Floating content renders in a portal at the document root, so it escapes your CSS scoping. That is why z tokens exist, and why theme attributes must live on <html> (a [data-theme] on a subtree will not reach a portal).

Controlled and uncontrolled

Every primitive supports both. Your wrapper must preserve both — this is the number one thing wrappers accidentally break.

tsx
// The wrapper stays transparent: pass through, never intercept.
export function Select({ value, defaultValue, onValueChange, ...rest }: SelectProps) {
  return <BaseSelect value={value} defaultValue={defaultValue} onValueChange={onValueChange} {...rest} />;
}

Do not "helpfully" add useState inside the wrapper. The moment you do, the controlled case has two sources of truth and the component starts fighting its consumer.

The a11y you still own

The primitive cannot do these for you:

  • Accessible names. An icon-only button needs aria-label. Make it required in the type system when there is no text child — a props type is a better enforcement mechanism than a lint rule or a code review.
  • Focus visibility. Primitives manage focus; they do not style it. Every interactive element needs a visible ring at 3:1 against both the component and the page. Use :focus-visible, never :focus (which fires on mouse click and gets designers asking you to remove it — at which point keyboard users lose it entirely).
  • Colour contrast. Owned by the token layer and gated in the token build.
  • Touch targets. 44×44 CSS px minimum for anything tappable. A 32px control can meet it with padding or a pseudo-element that extends the hit area beyond the visual bounds.
  • Motion. prefers-reduced-motion is your job. See the motion-system skill.
  • Copy. Error messages, empty states, and announcement text are content, not markup — but they are the part screen-reader users actually hear.

Version pinning

Primitives are behavioural infrastructure: a minor bump can change focus order or keyboard handling in a way no type error catches. Pin exact versions in the design system package, upgrade deliberately, and run the behavioural suite on upgrade — that suite exists precisely to make these upgrades boring.

Frequently asked questions

What does the Primitive Libraries AI skill do?

Use when building components on a headless primitive library - Base UI, Radix, React Aria Components, Ark UI - or deciding between them. Covers the composition/polymorphism API of each, the accessibility contract you inherit, state data-attributes for styling, portals, focus management, and what you must never re-implement by hand.

Why use Primitive Libraries on TypingMind?

Because you install it once and use it with any model. Primitive Libraries is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Primitive Libraries in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/staple-lab/design-system-skills/tree/main/plugins/design-system/skills/primitive-libraries. TypingMind reads its SKILL.md and installs it as a skill you can enable per chat.

Which AI models can use Primitive Libraries?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Primitive Libraries?

As many as you like. As long as a model supports skills, you can use Primitive Libraries with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Primitive Libraries AI skill free?

Yes. It is published on GitHub by staple-lab under the MIT license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇