Actions
Button
A button, or a link styled as one. The obvious name for any action: pressing it gives physical feedback, it has a 44px touch target on phones, and loading handles the busy state without the layout jumping.
In Spoonjoy
Spoonjoy, a recipe app, is built with Ouro HX (how).
See it live, no sign-in needed: Home · A recipe · Recipes
In its source: app/components/oauth.tsx · app/components/recipe/IngredientParseInput.tsx · app/components/recipe/ManualIngredientInput.tsx
Install
Once per app, with Tailwind CSS v4 and an @/* path alias (Ouro HX is built on Base UI, so --base base; skip -t in an existing app):
npx shadcn@latest init --base base --preset nova --yes --no-monoreponpx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.2.13Then the component. From v0.8.0 it installs only the helpers it imports and never touches your stylesheet (Installing):
npx shadcn@latest add ourostack/ouro-ui/button#v1.2.13Or, with the @ouro namespace set up:
npx shadcn@latest add @ouro/ouro-base @ouro/buttonIts tests, stories and Playwright spec are optional. They need Vitest 4.1.11 or later, and from v0.8.0 never change a version your app has (Testing):
npx shadcn@latest add ourostack/ouro-ui/button#v1.2.13 ourostack/ouro-ui/button-tests#v1.2.13import { Button } from "@/components/ouro/button/button";Paths use shadcn's default @/ alias; the CLI rewrites them to yours.
Example
<Button onClick={save} loading={saving}>Save</Button>
<Button href="/recipes/new" startIcon={PlusIcon}>New recipe</Button>
<Button tone="destructive" variant="soft" type="submit">Delete</Button>Usage
Use href to navigate (it renders a link); use type="submit" in a form.
Use instead when
- Use
Textwith an<a>instead when a link sits inside running text.
Props
The props the modes share. Type: ButtonProps.
children(required)- Type:
ReactNode - The label. For an icon-only button, also pass
aria-label. className- Type:
string - Layout only: margins, width, grid placement (
mt-4,col-span-2). Your classes always win over Ouro UI's. Appearance classes warn in development; usetone,variantandsizeinstead. endIcon- Type:
ButtonIcon - Icon after the label. Pass the component, not an element:
endIcon={ArrowRightIcon}. fullWidth- Type:
boolean - Stretches the button to the width of its container.
loading- Type:
boolean - Shows a centered spinner, keeps the width, sets
aria-busyand disables the button. It stays focusable, so focus is not lost when a pressed button turns busy. loadingLabel- Type:
string - Default:
"Loading" - What screen readers hear, politely, when
loadingturns on. It is spoken through a shared live region, so the button's name stays its label. Use the action in progress:loadingLabel="Saving recipe". size- Type:
"sm" | "md" | "lg" - Default:
"md" - Control size. Each size is taller on phones and steps down to desktop density from the
smbreakpoint (640px). shadcn'sdefaultismd; for an icon-only button, pass the icon as children and anaria-label. Any other string is a type error whose message names the fix. startIcon- Type:
ButtonIcon - Icon before the label. Pass the component, not an element:
startIcon={PlusIcon}. tone- Type:
"primary" | "secondary" | "destructive" | "success" | "warning" - Default:
"primary" - Color role from the theme, named after its token:
primaryreads--primary. A solid primary button, the page's main action, reads--ouro-actionand--ouro-action-foregroundinstead when the theme sets them, withhreftoo, so a theme can give its actions their own colour. Only the solid look reads them: soft, outline and plain primary buttons keep--primary, as doLink, checkboxes, switches and the current tab. Optional--ouro-action-hoverand--ouro-action-activeset its state fills without changing the state layers.--ouro-action-disabled,--ouro-action-disabled-foregroundand--ouro-action-disabled-opacityset its disabled look; opacity defaults to 0.5. shadcn'svariant="destructive"istone="destructive", and itsvariant="secondary"istone="secondary". variant- Type:
"solid" | "soft" | "outline" | "plain" - Default:
"solid" - Visual weight:
solidfor the primary action,softfor secondary actions,outlinefor a quiet bordered control,plainfor a borderless one. shadcn'sdefaultissolid(the default),ghostandlinkareplain, anddestructiveandsecondaryare tones:tone="destructive". Any other string is a type error whose message names the fix. Variant is visual only. For a toggle, passaria-pressed; for the current nav item,aria-current="page".
Modes
ButtonProps is a union: pass the props of exactly one mode. Mixing them is a type error.
Button: ButtonButtonProps
A Button without href: renders a native <button>.
ref- Type:
Ref<HTMLButtonElement> - Reaches the rendered element.
type- Type:
"button" | "submit" | "reset" - Default:
"button"(never submits a form by accident) - The button's role in a form:
submitsends it,resetclears it.
Also accepts the attributes in React's ButtonHTMLAttributes and HTMLAttributes (id, aria-*, event handlers), passed to the rendered element, except those named on this page.
Not in this mode (typed never):
href: Not set: this is a<button>. Passhrefto render a link instead.reloadDocument: Document navigation needs a link: passhrefwithreloadDocument.
Link: ButtonLinkProps
A Button with href: renders a link that looks like a button.
href(required)- Type:
string - Where the link goes. Internal paths render through
OuroProvider'slinkComponent(your router's link);http(s)://,mailto:and other absolute URLs render as a plain<a>. An absolute URL withtarget="_blank"always getsrel="noopener noreferrer", merged with anyrelyou pass. ref- Type:
Ref<HTMLAnchorElement> - Reaches the rendered element.
reloadDocument- Type:
boolean - Loads the document instead of using the provider's router link, for a server sign-in or a resource route. A present
downloadattribute does the same.
Also accepts the attributes in React's AnchorHTMLAttributes and HTMLAttributes (id, aria-*, event handlers), passed to the rendered element, except those named on this page.
Not in this mode (typed never):
disabled: A link cannot be disabled. Removehrefto render a disabled button, or don't render the link.type: Links have notype. Removehrefto make a submit button:<Button type="submit">.
Don't use
These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.
asChild: Not a prop. To render a link, passhref:<Button href="/recipes">. For other elements, use parts.color: Not a prop. Ouro UI usestone:<Button tone="destructive">.isLoading: Not a prop. Ouro UI usesloading:<Button loading>.leftIcon: Not a prop. Ouro UI usesstartIcon:<Button startIcon={PlusIcon}>.rightIcon: Not a prop. Ouro UI usesendIcon:<Button endIcon={ArrowRightIcon}>.
className is for layout only
Pass margins, width and grid placement (mt-4, w-full, col-span-2). Your classes always win over Ouro UI's: Button merges className last with cn (tailwind-merge), so a class that conflicts with its own replaces it.layout-classes
Appearance comes from props, not classes. In development, a class that sets appearance logs one warning per component instance, naming the prop to use instead. A class sets appearance when, after its variants (hover:, sm:) and any ! are removed, it starts with bg-, text-, border-, rounded-, shadow-, font-, ring-, from-, via-, to-, fill- or stroke-, or is border, rounded, shadow or ring, or has a dark: variant. Text wrapping, overflow and table layout classes (text-wrap, text-ellipsis, border-collapse) are layout and do not warn.
Alignment classes (text-center) warn too: set alignment on a parent element.
Types
ButtonIcon
An icon component, such as one from lucide-react or @heroicons/react.
type ButtonIcon = ComponentType<SVGProps<SVGSVGElement>>;Claim sources
- layout-classes: test:registry/ouro/components/ouro/button/button.test.tsx::className > lets app layout classes win over library styles
verify:testExists(registry/ouro/components/ouro/button/button.test.tsx::className > lets app layout classes win over library styles)