Form
Validate required fields and try a local recipe-name rejection.
Interactions that feel right to the humans using them. Code that's obvious to the agents building with them.
Installs with the shadcn CLI, uses shadcn's theme tokens, and sits alongside the shadcn/ui components you already have.
Real components, in small local examples. Open only the ones you want to try.
Validate required fields and try a local recipe-name rejection.
Open a sheet and choose cookbooks with a touch or the keyboard.
Navigate actions and a submenu. This example does not change a recipe.
Delete a local list item, then undo. Changes stay on this page.
Read a small recipe table with row links. This example does not sort or filter.
How agents use it
An agent that learned on shadcn/ui writes what it knows. Ouro HX answers where the agent already looks: the type check, the source it just installed, and the console. Every message here is real output from the code, checked against the code on every CI run.
The agent writes
shadcn's destructive variant
<Button variant="destructive">Delete</Button>The type check
error TS2322: Type '"destructive"' is not assignable to type '"solid" | "soft" | "outline" | "plain" | (string & { 'From shadcn? Use tone="destructive" or tone="secondary". default is "solid"; ghost and link are "plain"': never; }) | undefined'.
The installed source components/ouro/button/button.tsx
/** Visual weight: `solid` for the primary action, `soft` for secondary actions, `outline` for a quiet bordered control, `plain` for a borderless one. shadcn's `default` is `solid` (the default), `ghost` and `link` are `plain`, and `destructive` and `secondary` are tones: `tone="destructive"`. Any other string is a type error whose message names the fix. Variant is visual only. For a toggle, pass `aria-pressed`; for the current nav item, `aria-current="page"`. */variant?: | "solid" | "soft" | "outline" | "plain" | (string & { 'From shadcn? Use tone="destructive" or tone="secondary". default is "solid"; ghost and link are "plain"': never }) | undefined;
The console, in development
[ouro-ui] Button: variant="destructive" is not a variant. Fix: <Button tone="destructive">.
The agent writes
A loading flag named the usual way
<Button isLoading>Save</Button>The type check
error TS2322: Type 'boolean' is not assignable to type '{ 'Use <Button loading>': never; }'.
The installed source components/ouro/button/button.tsx
/** Not a prop. Ouro UI uses `loading`: `<Button loading>`. */isLoading?: { 'Use <Button loading>': never };
The console, in development
[ouro-ui] Button: isLoading is not a prop. Fix: <Button loading>.
The agent writes
Radix's asChild for a link
<Button asChild>
<a href="/recipes">Recipes</a>
</Button>The type check
error TS2322: Type 'boolean' is not assignable to type '{ 'Use <Button href="/path">': never; }'.
The installed source components/ouro/button/button.tsx
/** Not a prop. To render a link, pass `href`: `<Button href="/recipes">`. For other elements, use parts. */asChild?: { 'Use <Button href="/path">': never };
The console, in development
[ouro-ui] Button: asChild is not a prop. Fix: <Button href="/path">.
Principles
These rules replace what an agent would otherwise need a skill or a rules file for.
A component takes props that describe the whole thing, and building-block parts wait in a separate file for when props can't say what you need.
What a component needs, like a sheet's title, is a required prop, so leaving it out is a type error.
loading handles the spinner, aria-busy, the screen reader announcement and the layout, so nothing else needs setting.
tone values are the theme's token names (primary reads --primary), and variant and size are named options, so the props speak the theme's language instead of color names.
Margins and widths go in className, and an appearance class gets a one-time warning that names the prop to use.
When an agent guesses a common name, it either works or fails the type check with an error that names the real prop, and hovering it in the editor shows the same note.
In use
Spoonjoy, a recipe app, uses 60 Ouro HX components, from its navigation to its recipe pages, forms and cooking timers.
Install
The shadcn CLI copies a component into components/ouro/ with the helpers it imports. Its tests and stories are an optional -tests item that never changes your test tools' versions. Ouro HX is under construction, so props can still change.
In an app with Tailwind CSS v4 and an @/* path alias. --base base because Ouro HX is built on Base UI; skip -t in an existing app.
npx shadcn@latest init --base base --preset nova --yes --no-monorepoThen add the base tokens, once per app. From v0.8.0 it is the only add that writes to your stylesheet, so later adds leave it alone.
npx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.2.13npx shadcn@latest add ourostack/ouro-ui/sheet#v1.2.13The tag pins the newest release. Without it, the address installs main, which is for contributors.
It serves the newest release. Put this in components.json:
{
"registries": {
"@ouro": "https://ui.ouro.bot/r/{name}.json"
}
}npx shadcn@latest add @ouro/ouro-base @ouro/sheetimport { Button } from "@/components/ouro/button/button";
import { Sheet } from "@/components/ouro/sheet/sheet";
export function SaveRecipe() {
return (
<Sheet title="Save to cookbook" trigger={<Button>Save</Button>}>
<CookbookList />
</Sheet>
);
}Switching from shadcn
Ouro HX reads the same theme variables as shadcn/ui, so your theme carries over unchanged. Its components install to components/ouro beside your components/ui, so you can switch one at a time.
Next
Ouro AX is the agent-facing direction within Ouro UI. Its scope is still being defined.