Button.
Starts something that matters: book a demo, send the form, open the Toolbox. One primary per view, square geometry with a slight round, and a redline that means “this one.”
Playground.
Every variant, ground, size and state, on the real component.
import { Button } from "#/ui/site/button";
<Button href="/demo">
Book a demo
</Button>Variants.
Primary is the redline fill, the one action the view is for. Secondary is a hairline outline, for the action beside it.
Show codeHide code
<Button href="/demo">Book a demo</Button>
<Button href="/toolbox" variant="secondary">Open the Toolbox</Button>On ink.
On an ink plate the primary takes vellum, not the redline, so the plate keeps one bright thing and it is the action. Set onInk; a reversed page does not need it, because its tokens already invert.
Show codeHide code
<Button href="/demo" onInk>Book a demo</Button>
<Button href="/toolbox" variant="secondary" onInk>Open the Toolbox</Button>Sizes.
md is every call to action on the site. sm is for a control inside a panel or a form row, where a full CTA would shout.
Show codeHide code
<Button href="/demo">Book a demo</Button>
<Button href="/demo" size="sm">Book a demo</Button>Trailing arrow.
The arrow travels up and out on hover. arrow is for an internal link that leaves the page’s subject; external also opens a new tab, as any https: link does on its own.
Show codeHide code
<Button href="/redliner" variant="secondary" arrow>Try the Redliner</Button>
<Button href="https://..." variant="secondary" external>Follow on LinkedIn</Button>Pending.
While a request is in flight, a spinner leads the label and the label stays, so the button still says what it is doing. Clicks are held and aria-busy is set. It is not dimmed: the spinner already says busy.
Show codeHide code
<Button type="submit" pending={status === "sending"}>Request a demo</Button>Disabled.
Sixty percent opacity and a not-allowed cursor, everywhere on the site. Prefer explaining why beside it, as the cookie preferences do under Global Privacy Control, to a button that silently does nothing.
Show codeHide code
<Button onClick={save} disabled={gpc}>Save preferences</Button>Dimensioned.
Hover it. Extension lines drop from its edges and a dimension draws in under it, as if the button were a feature on a drawing. For the hero’s primary CTA and the closing band, nowhere else.
Show codeHide code
<Button href="/demo" dimension>Book a demo</Button>Pen circle.
Hover it. A redline ellipse draws itself around the button, the way a checker circles a callout. It is the Redliner’s mark and only the Redliner’s: every CTA pointing at it sets penCircle, and no other CTA does. It stills the travelling arrow, so there is one hover event, not two.
Show codeHide code
<Button href="/redliner" variant="secondary" penCircle arrow>Try the Redliner</Button>Best practices.
Do
- One primary per view. Everything else is secondary or a text link.
- Say what happens: Book a demo, Send the request. Never Submit or Click here.
- Use pending while a request is in flight, so the label stays and the click is held.
- Reach for size sm only inside a panel or a form row.
Don't
- Do not put two primaries side by side.
- Do not copy Button's classes into a hand-rolled button. Add a prop instead.
- Do not use the pen circle on anything that does not lead to the Redliner.
- Do not dim a pending button or swap its label for Sending...