Fancy Button

How the Fancy Button works with a keyboard and assistive tech, and where it stands against WCAG 2.2 AA.

FreeReact and Vue
AAWCAG 2.2 target
21/36contrast pairs passing
4keyboard interactions

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#

Tab

Moves focus to the button. A disabled button is skipped.

Shift+Tab

Moves focus to the previous focusable element.

Enter

Activates the button.

Space

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.

Always
aria-label

Set from the label, so the button still has a name when no text shows.

iconOnly
aria-busy="true"

Announced as busy, and disabled so it cannot be pressed twice.

loading
disabled

Removed from the tab order. It lies flat, without the rim or lift.

disabled or loading
aria-hidden

Icons beside a label are decorative.

Icons

Colour 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.

4.5:1

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) ↗
3:1 if needed

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 ↗
3:1

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 ↗
Not required

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.

Intent · variantRestHoverPressed
Neutral · Primary14.35passes8.59passes20.14passes
Neutral · Secondary5.17passes5.17passes12.04passes
Brand · Primary5.40passes8.53passes13.54passes
Brand · Secondary5.40passes5.40passes3.10below the minimum
Success · Primary1.94below the minimum3.68below the minimum7.62passes
Success · Secondary3.68below the minimum3.68below the minimum2.64below the minimum
Warning · Primary2.27below the minimum4.00below the minimum7.91passes
Warning · Secondary4.00below the minimum4.00below the minimum2.45below the minimum
Error · Primary4.49below the minimum7.03passes11.60passes
Error · Secondary4.49below the minimum4.49below the minimum3.01below the minimum
Inverse on dark · Primary14.35passes12.82passes8.85passes
Inverse on dark · Secondary14.35passes14.35passes6.58passes

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.