Button
The action on every screen: book, join, go. Warm Orange for the one thing to do; quieter variants for the rest.
Preview
Usage
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
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "secondary" | "outline" | "field" | "ghost" | "link" | "destructive" | "chip" | "control" | "brand" | "tagline" | "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 | "default" | "sm" | "lg" | "icon" | "icon-sm" | "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 | "button" | "submit" | "reset" | "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 | boolean | false | Render the child element (a link, say) with the button’s look (Radix Slot). |
disabled | boolean | 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 | string | — | 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. |
variant"default" | "secondary" | "outline" | "field" | "ghost" | "link" | "destructive" | "chip" | "control" | "brand" | "tagline""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"default" | "sm" | "lg" | "icon" | "icon-sm""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"button" | "submit" | "reset""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.
asChildbooleanfalseRender the child element (a link, say) with the button’s look (Radix Slot).
disabledbooleanfalseGreys 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.
classNamestringMore 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.
| Prop | Type | Default | Description |
|---|---|---|---|
action | { label, href } | { label, onClick } | ReactElement | — | 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 | Button's | — | The look, as on Button. EmptyState, CtaCard, ActivityCard and OfferBanner all draw their actions with it. |
action{ label, href } | { label, onClick } | ReactElementThe 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 · classNameButton'sThe look, as on Button. EmptyState, CtaCard, ActivityCard and OfferBanner all draw their actions with it.