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-monorepo
npx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.2.13

Then 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.13

Or, with the @ouro namespace set up:

npx shadcn@latest add @ouro/ouro-base @ouro/button

Its 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.13
import { 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 Text with 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; use tone, variant and size instead.
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-busy and 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 loading turns 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 sm breakpoint (640px). shadcn's default is md; for an icon-only button, pass the icon as children and an aria-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: primary reads --primary. A solid primary button, the page's main action, reads --ouro-action and --ouro-action-foreground instead when the theme sets them, with href too, 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 do Link, checkboxes, switches and the current tab. Optional --ouro-action-hover and --ouro-action-active set its state fills without changing the state layers. --ouro-action-disabled, --ouro-action-disabled-foreground and --ouro-action-disabled-opacity set its disabled look; opacity defaults to 0.5. shadcn's variant="destructive" is tone="destructive", and its variant="secondary" is tone="secondary".
variant
Type: "solid" | "soft" | "outline" | "plain"
Default: "solid"
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".

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: submit sends it, reset clears 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>. Pass href to render a link instead.
  • reloadDocument: Document navigation needs a link: pass href with reloadDocument.

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's linkComponent (your router's link); http(s)://, mailto: and other absolute URLs render as a plain <a>. An absolute URL with target="_blank" always gets rel="noopener noreferrer", merged with any rel you 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 download attribute 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. Remove href to render a disabled button, or don't render the link.
  • type: Links have no type. Remove href to 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, pass href: <Button href="/recipes">. For other elements, use parts.
  • color: Not a prop. Ouro UI uses tone: <Button tone="destructive">.
  • isLoading: Not a prop. Ouro UI uses loading: <Button loading>.
  • leftIcon: Not a prop. Ouro UI uses startIcon: <Button startIcon={PlusIcon}>.
  • rightIcon: Not a prop. Ouro UI uses endIcon: <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