Button

The action and CTA affordance, polymorphic by intent.

The action and CTA affordance. Polymorphic: with href it is an<a> (a CTA that navigates), otherwise a<button> (a real action) — same look, picked by intent rather than by a prop.

primary fills with --surface-action, which is the brand green in both modes (ADR 0014). secondary is an outline that reads --text-default, so it adapts inside tone contexts where a fill would not. A link-styled affordance stays the Link primitive's job — there is deliberately no third variant.

Hover sweeps the fill up from the bottom edge over--duration-base instead of crossfading it in (ADR 0034). The shade is the one the crossfade already used; only the delivery changed.

The layer is revealed with clip-path, not scaled withtransform. Scaling is the usual way to write this and it is wrong here: a squashed box squashes its own border-radius, so mid-sweep the bottom corners flatten and the rising fill reads as a square-ended bar poking out of a rounded button. Hover slowly and watch the corners.

Re-tone the preview and toggle the theme: the green fill is held across both modes and every tone, because --surface-action is not tone-aware on purpose (ADR 0013) — a CTA should read as the brand green wherever it lands.

Variants

Tone

Disabled

Only the <button> form has this state: a disabled link is a link you don't render. It drops out of the sweep entirely rather than sweeping in a greyed fill, which would read as a button that responded.

Tone

Props

packages/primitives/src/Button.astro
PropTypeRequired
variant"primary" | "secondary"
hrefstring
type"button" | "submit" | "reset"
classstring