Button
Compact actions, links, and toggles. Seven variants, sizes for common layouts, and Base UI focus and disabled behavior.
Usage
import { Button } from "@tulipfarm/sprout/button";
<Button onClick={save}>Save</Button>
<Button variant="outline" size="sm">
<PlusIcon data-icon="inline-start" />
Add item
</Button>
<Button variant="ghost" size="icon-sm" aria-label="Delete file">
<TrashIcon />
</Button>Variants are default, outline, secondary, ghost, destructive, link, and scope. Sizes are default, xs, sm, lg, and the square icon, icon-xs, icon-sm, and icon-lg. Give icon-only buttons an aria-label. Mark leading or trailing icons with data-icon="inline-start" or "inline-end" to tighten padding.
Links
<Button
variant="outline"
nativeButton={false}
render={<Link to="/files" />}
>
Open files
</Button>With nativeButton=, Button renders a real link. It keeps link semantics, so screen readers announce a link and Enter follows it. A disabled link gets aria-disabled, leaves the tab order, and ignores clicks.
States
<Button aria-invalid aria-describedby="owner-error">Assign</Button>
<Button
variant={selected ? "secondary" : "ghost"}
aria-pressed={selected}
onClick={toggle}
>
Board
</Button>Button styles disabled, aria-disabled, aria-busy, aria-invalid, aria-expanded, and aria-pressed. Your application owns the error text and the pressed state. For busy and blocked actions, pass state as shown below.
Action states
import { Button, type ButtonState } from "@tulipfarm/sprout/button";
const state: ButtonState = sending
? { kind: "busy" }
: !enabled
? {
kind: "blocked",
reason: "Turn on Enable execution first.",
fix: { kind: "jump", label: "Go to Enable execution", target: "enable-execution" },
}
: { kind: "ready" };
<Button state={state} pendingLabel="Activating…" onClick={activate}>
Activate saved instructions
</Button>Pass state when an action can be busy or blocked. Busy setsaria-busy and, with pendingLabel, swaps the label for a spinner and that text. Busy and blocked buttons are disabled but stay in the tab order, so keyboard users reach them and hear the reason through aria-describedby.
With state, Button renders a data-slot="button-action" wrapper with data-state that also holds the reason, andclassName styles that wrapper. Keep passing a state, even{ kind: "ready" }, for the button's lifetime. Changing its kind then keeps the same button element and its focus. Long labels wrap in default, sm, and lgsizes.
type ButtonBlockerFix =
| { kind: "jump"; label: string; target: string } // element id on this page
| { kind: "link"; label: string; link: ReactElement } // your router <Link>
| { kind: "action"; label: string; onClick: () => void };A jump fix calls jumpTo, which scrolls to the element and focuses it, or its first focusable child. A linkfix renders your link through render. An actionfix runs a callback, such as Try again. Leave fix out to show only the reason. useActionControl(state) returns the same button props and reason for controls you build yourself.
Action groups
import { ActionGroup } from "@tulipfarm/sprout/button";
<ActionGroup
state={reviewState}
actions={[
{ label: "Keep my draft", variant: "outline", onClick: keepDraft },
{ label: "Use current settings", variant: "outline", onClick: adoptCurrent },
]}
/>ActionGroup puts several actions under one state. Every button turns busy or blocked together, and one reason sits under the row.
Triggers and class-only styling
import { Menu } from "@base-ui/react/menu";
import { Button, buttonVariants } from "@tulipfarm/sprout/button";
<Menu.Trigger render={<Button variant="outline" size="sm" />}>
Sort
</Menu.Trigger>
<a className={buttonVariants({ variant: "ghost", size: "sm" })} href={url}>
Open in calendar
</a>Pass Button to any Base UI trigger or close part through render. The trigger supplies its ARIA state and Button styles it. Use buttonVariants() when another component owns the element. Tailwind utilities passed throughclassName override Button's own styles.
Pattern sizes
Pattern sizes fit common layouts: back (icon-only below 640px when its label is hidden), onboarding, bud and bud-labeled floating launchers, chat-row, task-link, card-title, result-card, calendar-title, calendar, calendar-icon, nav (tree rows), column-header, column-header-end, table-link, and badge. buttonVariantNames and buttonSizeNames list every option.
Behavior and accessibility
Buttons default to type="button"; pass type="submit" or "reset", and form for an external submit. Tab reaches every enabled button. Enter and Space activate buttons; Enter follows links. Focus shows a 2px ring. Coarse pointers get 44px minimum targets. Hover and press transitions take 120ms and stop when the user prefers reduced motion.
Button renders data-slot="button", data-variant, and data-size. Override data-slot when a wrapping component needs its own hook. Refs reach the rendered element.