Fancy Button
How the Fancy Button works with a keyboard and assistive tech, and where it stands against WCAG 2.2 AA.
The Fancy Button is the Button with a rim, an inner highlight and a lift. It renders the same native <button> with the same props, so its keyboard and semantics are the Button's. What changes is the focus ring, which has to clear the rim, and the Secondary hover, which keeps its white fill.
Keyboard#
Moves focus to the button. A disabled button is skipped.
Moves focus to the previous focusable element.
Activates the button.
Activates the button when the key is released.
Semantics#
<button type="button">Native role and keyboard handling. For a sign-up form, pass type="submit" deliberately.
Alwaysaria-labelSet from the label, so the button still has a name when no text shows.
iconOnlyaria-busy="true"Announced as busy, and disabled so it cannot be pressed twice.
loadingdisabledRemoved from the tab order. It lies flat, without the rim or lift.
disabled or loadingaria-hiddenIcons beside a label are decorative.
IconsColour contrast#
Every variant and state has to meet WCAG 2.2 Level AA. Measure the colours as they land on screen: a translucent hover or press wash over whatever sits behind it.
Text label · Rest, Hover, Pressed
Label against the fill in every state, the same pairs as the Button's. The highlight and lift do not change the fill behind the text.
WCAG 1.4.3 Contrast (Minimum) ↗Rim · Primary, Secondary
The 2px rim is decoration on a filled Primary. On Secondary it is the only edge, so it needs 3:1 only when the label alone does not show it is a button.
WCAG 1.4.11 Non-text Contrast ↗Focus indicator · Focus visible
A 3px ring sits outside the rim with 1px of white between, in the intent's own colour, as on the Button. It contrasts with the page, not the fill.
WCAG 1.4.11 Non-text Contrast ↗WCAG 2.4.13 Focus Appearance ↗Disabled · Disabled
Inactive controls are exempt. The disabled fill (#f2f2f2) and label (#cbcbcb) are deliberately faint.
WCAG 1.4.3 Contrast (Minimum) ↗Measured contrast#
Label against fill, for every intent, Primary and Secondary, and every state. Secondary keeps its white fill on hover. Inverse on dark is measured on #2a2a2a, everything else on white. 15 of 36 pairs fall below their minimum (4.5:1 for text, 3:1 for graphics) and are marked in amber.
Motion#
- Colour changes take 150ms and are turned off under prefers-reduced-motion.
- Pressing drops the rim and lift at once; nothing moves position.
- The loading spinner only spins when motion is allowed.
Checklist#
Use it once per screen, for the one action the page exists for.
The label is a verb: Start free trial, not Get started on its own.
Success and Warning Primary at rest and hover, Error Primary at rest, every Success, Warning and Error Secondary state, and Brand Secondary pressed fall below 4.5:1: see the table. Keep the hero on Neutral or Brand Primary.
The rim and lift are not the only signal that it is pressable; the label still reads as an action.
Sizes run from 24px (XXS) to 72px (XL), all at or above the 24px target minimum.
