← All components

Primitive · 1 of 13

Button

Seven variants off one prop. Four of them are pills; three are bare text links that share no box at all. Knowing which is which is the whole API.

Vue island src/components/common/Button.vue rendered live from compiled/Button.js 7 variants

Props

Taken verbatim from defineProps<{...}>. A * marks a required prop.

PropTypeNotes
variant'primary' | 'secondary' | 'tertiary' | 'ghost' | 'text' | 'link' | 'icon'Falls through to primary when omitted — there is no explicit 'primary' case in the switch.
as'link'Chooses the element: <a href> when 'link', else <button>.
titlestringThe label. Optional — icon buttons omit it and let the slot carry the content.
hrefstringOnly read when as="link".
isDisabledbooleanVisual only — see the warning below.
type'submit'Declared in props but never bound in the template.
classstringAppended to the variant class. This is how callers give primary its box.

Variants

The four pill variants. primary is shown at a caller-supplied width because on its own it has neither height nor padding.

Primary variant="primary" (default) #5F20FE → hover #4004AF → active #1E113B. The only variant with an active color.
Secondary variant="secondary" Fill #1E113B (primary-900) → hover #1C1C27 (surface-tertiary). Carries its own h-11 px-6 py-1.
Tertiary variant="tertiary" Outline in #5F20FE. Hover it — the border goes darker, not lighter. See the divergence note.
Icon variant="icon" (no title) p-5 square with the same violet outline. The slot carries the icon; with no title there is no accessible name, so it needs an aria-label.

The text-like variants

These three use linkBaseClass, not the pill base. They have no radius, no background, no height — only color and (for ghost) padding. Swapping a primary for a text changes the layout, not just the skin.

Ghost variant="ghost" The header nav item. #F4F4F6 → hover/active #B19AFE. Bold, px-6 py-2.
Text variant="text" The footer link. #C7C7DB (gray-300) → hover/active #B19AFE. No padding at all.
Link variant="link" Underlined #B19AFE. The only variant with no hover state defined.

States

Hover and active are real here — move the pointer over the first canvas. So is "disabled": these buttons carry isDisabled and you can still click and focus them, because the prop is visual only (see findings).

Interactive — hover and press :hover · :active Transitions run on --transition-colors (150ms).
Disabled isDisabled={true} opacity 0.2 + not-allowed cursor. At 20% the label falls far below any contrast floor.

Findings

isDisabled does not disable the button. It only sets cursor-not-allowed opacity-20 on the class string. The template never binds the disabled attribute, so the element stays focusable, keyboard-activatable and clickable — and when as="link" the <a> still navigates. Anything relying on this prop to block submission is not protected.

The type prop is declared but never used. type?: 'submit' appears in defineProps and then nowhere in the template, so the rendered element keeps the implicit type="submit" of a bare <button> inside a form and type="button" semantics are unreachable. Passing type has no effect either way.

primary has no box of its own. Unlike secondary and tertiary it omits h-11 px-6 py-1, so an unstyled <Button /> renders as a text-height pill. Every caller compensates through classfooter.astro passes w-40, Cookies.vue passes w-full max-w-[12.5rem]!. The default variant is effectively unusable without a caller override.

Tertiary hover reduces contrast. The border moves #5F20FE → #4004AF (primary-700) on hover — darker, not lighter. Against the site's #0A0A0B ground that is roughly a 1.8:1 border, so the control dims at the moment of interaction. Faithfully reproduced above. (DESIGN.md §10.1 recorded this with the pre-redesign values — the direction is unchanged.)

variant="icon" is styling only. There is no icon element branch — the button always renders {{ title }} followed by the slot. An icon button is variant="icon" with no title and the SVG in the slot; nothing stops a caller passing both and getting a label crammed into the p-5 square. (An earlier revision accepted as="icon" — the current source only accepts as="link".)