AI Elements

@elirobinson/ai-elements is the assistant tier: conversation logs, prompt inputs, tool and reasoning panels, and the rest of the chrome an AI product needs and a general component library has no business owning. It is the one package in this system whose source is not ours — it is vendored from vercel/ai-elements at a pinned release, and skinned with the system's tokens rather than rewritten.

It requires Tailwind 4

Read this before you install. Every vendored component is Tailwind utility markup, and without Tailwind 4 in your app it renders as unstyled HTML — no error, just a page that looks broken.

That requirement is the entire reason this is a separate package. @elirobinson/react has no UI dependencies; Elements needs Tailwind 4, shadcn/ui, Radix and lucide-react. Folding them into @elirobinson/react would force Tailwind on every consumer of Button and Input for a feature most of them will never use. Installing this package is how you opt in — see Installation, which is three stylesheet imports and one directive.

It also means the two tiers are not alternatives. Use @elirobinson/react for the product around the assistant and for anything a model returns as structured output; use Elements for the conversation itself.

Vendored at a pinned release

The tree in this repo is upstream's bytes at ai-elements@1.9.0 (bc87126), plus a transform layer, and nothing else. elements.lock.json records the pin, every vendored file opens with a header naming its upstream path, and pnpm sync:elements re-derives the expected bytes from fresh upstream bytes and compares — so every difference is attributable to upstream moving, or to somebody editing a file they should not have.

What that buys you as a consumer: the version you install names the upstream release it came from, in data rather than in prose.

node -p "require('@elirobinson/ai-elements/upstream').upstream.ref"

Most of that transform layer is mechanical import rewriting. The part worth knowing about is the skin, which rewrites the places where upstream reaches for Tailwind's own palette — text-zinc-500, bg-red-100 dark:bg-red-900/30 — because no alias can re-point a literal. It also re-points shadcn's --accent, which upstream means as a subtle hover tint and this system means as Miltinson Amber, a brand signal.

How it comes out on-brand

@elirobinson/tokens/tailwind.css maps Tailwind's colour, radius, shadow and font namespaces onto the system's tokens with @theme inline, so bg-background, text-muted-foreground, border-border and rounded-md compile to var(--token). Because the aliases are inline, the utilities read the token at paint time: the theme, palette and platform dials all move Elements at runtime, with no rebuild and no edit to a vendored file.

The same stylesheet declares @custom-variant dark pointing at [data-theme="dark"], so dark: follows the system's theme dial rather than the reader's operating system. Left alone those are two independent switches, and a theme toggle would move every token and none of the dark: utilities.

Accessibility

Elements was not built to this system's contracts, and shadcn/Radix defaults land at 32 to 40px — which clears WCAG 2.2 AA and misses this system's 44x44 floor. pnpm a11y:elements mounts every component in the components tier across two themes, three palettes and two platform settings, and runs four browser-settled checks over each.

The per-control verdicts are published rather than written down here: each one names the control, the geometry a browser measured, which of the two touch-target floors it was held to, and why that one. Read them from the contracts:

node -p "Object.keys(require('@elirobinson/ai-patterns/contracts').vendoredElementTargets)"

One control is a documented exception. React Flow paints its own licence notice into the corner of the canvas components; it appears in no vendored class string, so the transform layer cannot reach it, and its licence requires it to stay.

Licensing

The vendored tree is Apache-2.0. The package ships LICENSE (the licence text) and NOTICE (attribution to Vercel and to shadcn, plus the complete list of modifications required by §4(b)). Both are generated from the same lockfile as the code, so neither can drift from it. Redistributing the package means shipping the NOTICE with it.

What is in it

There are 74 vendored files across three subpath namespaces, and no page on this site keeps a list of them. The roster is upstream's, it changes on a re-sync, and a list written into prose would be wrong from that commit onward — so the component index is generated from @elirobinson/ai-elements/manifest, which is rebuilt from the emitted declaration files on every build and cannot name a component the package does not ship.

Next steps

  • Component index — the generated list of every subpath and what it exports.
  • Installation — the package, its peers, and the three stylesheet imports.
  • Examples — a chat round trip, a tool panel that reads, and a structured surface.
  • All components — the two tiers side by side, which is where this one is reached from.