Zostel
Forms Button

Button

The action on every screen: book, join, go. Warm Orange for the one thing to do; quieter variants for the rest.

Preview

Variants

Brand

Chips and controls

SVGPNG

Sizes

States

Usage

tsx
import { Button, buttonVariants, ActionButton, ChevronRightIcon } from "zostel-ui";

<Button>Book</Button>
<Button variant="secondary">Details</Button>
<Button variant="outline">Save</Button>
<Button variant="ghost">Not now</Button>
<Button variant="link">Read more</Button>
<Button variant="destructive">Cancel stay</Button>
<Button variant="brand">Go</Button>
<Button variant="tagline">LIVE IT. NOW</Button>

{/* a file chip, and a viewer's round control (the glyph is decorative: the button has the name) */}
<Button asChild variant="chip" size="sm"><a href="/brand/kit/svg/backpack.svg" download>SVG</a></Button>
<Button variant="control" size="icon" aria-label="Next"><ChevronRightIcon size={16} title="" /></Button>

<Button size="sm">Small</Button>
<Button size="lg">Large</Button>

{/* with no type a Button is type="button": in a form, only the one you mark submits */}
<form onSubmit={search}>
  <Button onClick={clear}>Clear</Button>
  <Button type="submit">Search</Button>
</form>

{/* off while it may hold the focus (a stepper at its limit, Subscribe while sending): aria-disabled keeps the focus; your handler does nothing */}
<Button aria-disabled={sending || undefined} onClick={() => !sending && send()}>Subscribe</Button>

{/* a link that looks like a button; buttonVariants for anything that isn't a <Button> */}
<Button asChild><a href="/destinations">See the places</a></Button>
<a href="/posters" className={buttonVariants({ variant: "secondary" })}>Make a poster</a>

{/* an action that arrives as data: a link or a button, never both */}
<ActionButton action={{ label: "Find a trip", href: "/trips" }} />
<ActionButton action={{ label: "Try again", onClick: retry }} variant="secondary" />

Props

variant
Type: "default" | "secondary" | "outline" | "field" | "ghost" | "link" | "destructive" | "chip" | "control" | "brand" | "tagline"
Default: "default"

default = the primary: orange-ink under white text, one step darker on hover; secondary = Beige under Steel Grey; outline = white with a Beige line; field = a picker's field (the date and guest pickers, the Combobox, the currency field): the card's colours with the field edge, 3:1 by day and at night, medium weight; chip = a small file or link chip, Beige that turns orange-ink; control = a viewer's round control (previous, next, zoom, close), Beige that turns Steel Grey; brand = orange-ink that turns Steel Grey; tagline = Steel Grey that turns orange-ink, in capitals. Every one a pill (rounded-full), bold, in sentence case, flat: no shadow, no lift, no gradient. On a touch screen every size has a 44 px target without looking bigger.

size
Type: "default" | "sm" | "lg" | "icon" | "icon-sm"
Default: "default"

default 36 px · sm 30 px (the chip's) · lg 44 px · icon a 36 px circle · icon-sm a 30 px circle (a stepper in a card), both 44 px on a phone (give them an aria-label).

type
Type: "button" | "submit" | "reset"
Default: "button"

button unless you say otherwise, so a Button in a form never submits it by accident: give type="submit" to the one that should. With asChild the child keeps its own.

asChild
Type: boolean
Default: false

Render the child element (a link, say) with the button’s look (Radix Slot).

disabled
Type: boolean
Default: false

Greys it out and stops clicks. For a control that turns off while it may hold the focus, use aria-disabled instead: it looks the same, keeps the focus, and your handler skips the action.

className
Type: string
Default: —

More classes, merged with the component’s own (tailwind-merge). buttonVariants({ variant, size, className }) returns the same merged classes for a link or any other element.

ActionButton

A link or a button that arrives as data, the one way the kit draws them.

action
Type: { label, href } | { label, onClick } | ReactElement
Default: —

The action as data: a link (href) or a button (onClick), never both. The href passes through safeHref: http, https, mailto, tel and relative links render; anything else (javascript:, data:) renders nothing, with a warning in development. A ready element is shown as it is.

variant · size · className
Type: Button's
Default: —

The look, as on Button. EmptyState, CtaCard, ActivityCard and OfferBanner all draw their actions with it.