Ghost Button

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

FreeReact and Vue
AAWCAG 2.2 target
8/18contrast pairs passing
4keyboard interactions

The Ghost Button is a native <button> with no fill until it is hovered, used where many actions sit side by side. Because it has no edge at rest, its label is the only thing that says it is a control, so that label has to read on its own.

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. A toggle flips its pressed state.

Semantics#

<button type="button">

Native role and keyboard handling.

Always
aria-label

Set from the label, so an icon-only ghost button still has a name. Pair it with a Tooltip showing the same words.

Only icon
aria-pressed

Pass it yourself when the button is on or off. The Active line is drawn by data-state, which assistive tech cannot see.

Toggles a tool or view
disabled

Removed from the tab order and not announced as actionable.

Disabled
aria-hidden

Chevrons 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

Label text · Rest, Hover, Active tint

The 16px bold label against the surface behind it. At rest that is the button's white fill (or the dark page for Inverse on dark); on hover and Active tint, a light wash of the intent.

WCAG 1.4.3 Contrast (Minimum) ↗
3:1

Icon-only glyph · Only icon

The chevron that stands in for text needs 3:1 against the fill. Neutral's muted #8f8f8f measures 3.23:1 on white, so it passes for a glyph but not for a text label.

WCAG 1.4.11 Non-text Contrast ↗
3:1

Active line · Active, Active tint

The 2px line is the only mark of the current tool, so it needs 3:1 against the surface. Neutral draws it #070707, other intents in their strong colour; Warning's #ff9000 line measures 2.27:1 on white. Back it with aria-pressed or aria-current so the state is not visual only.

WCAG 1.4.11 Non-text Contrast ↗WCAG 1.4.1 Use of Color ↗WCAG 4.1.2 Name, Role, Value ↗
3:1

Focus indicator · Focus visible

A 2px #1a6ef8 stroke inside the box, 4.54:1 on white. Focus also darkens the label to the hover colour.

WCAG 1.4.11 Non-text Contrast ↗WCAG 2.4.13 Focus Appearance ↗

Measured contrast#

Label against its own fill or wash, for every intent. Inverse on dark is measured on #2a2a2a, everything else on white. 10 of 18 pairs fall below their minimum (4.5:1 for text, 3:1 for graphics) and are marked in amber.

IntentRestHoverActive tint
Neutral · Ghost3.23below the minimum13.75passes13.17passes
Brand · Ghost5.40passes4.81passes4.81passes
Success · Ghost3.68below the minimum3.56below the minimum3.56below the minimum
Warning · Ghost4.00below the minimum3.86below the minimum3.86below the minimum
Error · Ghost4.49below the minimum4.17below the minimum4.17below the minimum
Inverse on dark · Ghost14.35passes11.20passes11.20passes

Motion#

  • Colour changes take 150ms and the Active line slides up over 200ms. Both are turned off under prefers-reduced-motion, so the line simply appears.

Checklist#

Neutral at rest is #8f8f8f on white, below 4.5:1. It reads as muted on purpose, but the label is real text: darken it to #6d6d6d before the code library ships.

Warning (#be6a00), Success (#229a14) and Error (#ec0d33) labels sit below 4.5:1 on white and on their washes: see the table.

Every icon-only ghost button has an aria-label and a Tooltip with the same text.

A row of ghost buttons that switches views should mark the current one with aria-pressed, not only the line.

At 52px (42px icon-only) the button is well over the 24px target minimum.