# Contrast pair

Two colors drawn as text on its background, with their contrast ratio and the mark it earns: AAA, AA, AA large or fails.

Block: `smbk-contrast-pair`. Status: experimental. Figma: the Figma file, https://www.figma.com/community/file/1688324308358870630. Desktop and mobile: differ by presentation only.

## Options

| Axis | Values | Default | Meaning |
|---|---|---|---|
| `result` | `aaa`, `aa`, `aa-large`, `fails` | `aaa` | The mark the ratio earns: AAA at 7:1 and up, AA at 4.5:1, AA large at 3:1 (large text and controls only), else fails. Worked out from the two colors unless given; it sets the badge’s words and type. |

Parts: `smbk-contrast-pair__sample`, `smbk-contrast-pair__text`, `smbk-contrast-pair__result`, `smbk-contrast-pair__ratio`, `smbk-contrast-pair__badge`, `smbk-contrast-pair__names`, `smbk-contrast-pair__hidden`

## Markup

```html
<div
  style="--smbk-contrast-pair-foreground:rgba(35, 37, 39, 1);--smbk-contrast-pair-background:rgba(243, 245, 248, 1)"
  class="smbk-contrast-pair smbk-contrast-pair--result-aaa"
>
  <span class="smbk-contrast-pair__sample" aria-hidden="true">Aa</span>
  <div class="smbk-contrast-pair__text">
    <div class="smbk-contrast-pair__result">
      <span class="smbk-contrast-pair__ratio"><span aria-hidden="true">14.08:1</span><span class="smbk-contrast-pair__hidden">Contrast 14.08 to 1,</span></span>
      <span class="smbk-badge smbk-badge--look-soft smbk-badge--tone-success smbk-badge--size-sm smbk-contrast-pair__badge">
        <span class="smbk-badge__label">AAA</span>
      </span>
    </div>
    <p class="smbk-contrast-pair__names">Text default on surface default</p>
  </div>
</div>
```

## React

```tsx
import { ContrastPair } from './design-system/components/contrast-pair/contrast-pair.js';

<ContrastPair result="aaa" foreground="#232527" background="#f3f5f8" names="Text default on surface default" />
```

## Stylesheet

```css
/* smbk-contrast-pair — Contrast pair. Figma "Contrast pair" 7416:115 (Result: AAA, AA, AA large, Fails).
 * One color pair drawn as it is, on the default surface inside a subtle edge: a 48 px sample (the background color with
 * "Aa" in Heading XS in the text color), then the ratio in Label MD with a small soft badge (smbk-badge) that says the
 * result in words, and under them the two roles' names in Body XS. The sample's two colors are data: the wrapper writes
 * them to two custom properties on the block, and only the sample reads them. The badge's type follows the result. */

.smbk-contrast-pair {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  gap: var(--smbk-space-inline-lg);
  min-width: 0;
  padding: var(--smbk-space-inset-md);
  border: var(--smbk-border-width-hairline) solid var(--smbk-color-border-subtle);
  border-radius: var(--smbk-radius-container-sm);
  background: var(--smbk-color-surface-default);
}

/* The block frames the words it hides for screen readers, so they stay inside it wherever it is placed: without a
   frame they hang from the page itself and stretch it, past the end of an area that scrolls. No weight, so a parent
   that places the block keeps its say. */
:where(.smbk-contrast-pair) {
  position: relative;
}

.smbk-contrast-pair__sample {
  position: relative;
  display: flex;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  width: var(--smbk-size-48);
  height: var(--smbk-size-48);
  overflow: hidden;
  border-radius: var(--smbk-radius-control-md);
  background: var(--smbk-contrast-pair-background);
  color: var(--smbk-contrast-pair-foreground);
  font-family: var(--smbk-typography-heading-xs-font-family);
  font-size: var(--smbk-typography-heading-xs-font-size);
  font-weight: var(--smbk-typography-heading-xs-font-weight);
  line-height: var(--smbk-typography-heading-xs-line-height);
  letter-spacing: var(--smbk-typography-heading-xs-letter-spacing);
  forced-color-adjust: none;
}

/* The sample's edge, drawn inside and over the color, as the color swatch draws its own. */
.smbk-contrast-pair__sample::after {
  content: '';
  position: absolute;
  inset: 0;
  border: var(--smbk-border-width-hairline) solid var(--smbk-color-border-edge-shade-subtle);
  border-radius: inherit;
  pointer-events: none;
}

.smbk-contrast-pair__text {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: var(--smbk-space-stack-xs);
  min-width: 0;
}

.smbk-contrast-pair__result {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--smbk-space-inline-sm);
}

.smbk-contrast-pair__ratio {
  color: var(--smbk-color-text-default);
  font-family: var(--smbk-typography-label-md-font-family);
  font-size: var(--smbk-typography-label-md-font-size);
  font-weight: var(--smbk-typography-label-md-font-weight);
  line-height: var(--smbk-typography-label-md-line-height);
  letter-spacing: var(--smbk-typography-label-md-letter-spacing);
  font-variant-numeric: tabular-nums;
}

.smbk-contrast-pair__names {
  margin: 0;
  color: var(--smbk-color-text-subtle);
  font-family: var(--smbk-typography-body-xs-font-family);
  font-size: var(--smbk-typography-body-xs-font-size);
  font-weight: var(--smbk-typography-body-xs-font-weight);
  line-height: var(--smbk-typography-body-xs-line-height);
  letter-spacing: var(--smbk-typography-body-xs-letter-spacing);
  overflow-wrap: anywhere;
}

/* Words for screen readers only: the ratio said in full. */
/* .smbk-contrast-pair__hidden: hidden from view, kept for screen readers and the keyboard, by the shared rule in visually-hidden/visually-hidden.css. */

/* ---- high contrast (forced colors) ---- */
/* The sample keeps its two colors, since they are what it shows; its edge and the block's take the reader's text color. */
@media (forced-colors: active) {
  .smbk-contrast-pair,
  .smbk-contrast-pair__sample::after {
    border-color: CanvasText;
  }
}
```

## Tokens it reads

```json
{
  "space": {
    "inline": {
      "lg": {
        "$type": "dimension",
        "$value": "{{space.12}}",
        "$description": "Gap between things side by side, large."
      },
      "sm": {
        "$type": "dimension",
        "$value": "{{space.6}}",
        "$description": "Gap between things side by side, small."
      }
    },
    "inset": {
      "md": {
        "$type": "dimension",
        "$value": "{{space.8}}",
        "$description": "Padding inside a component, medium."
      }
    },
    "stack": {
      "xs": {
        "$type": "dimension",
        "$value": "{{space.4}}",
        "$description": "Gap between things stacked one above another, extra small."
      }
    }
  },
  "border-width": {
    "hairline": {
      "$type": "dimension",
      "$value": "{{border-width.1}}",
      "$description": "Thin lines: dividers and almost every edge."
    }
  },
  "color": {
    "border": {
      "subtle": {
        "$type": "color",
        "$value": "{{color.grey.300}}",
        "$description": "Dividers and quiet edges."
      },
      "edge-shade-subtle": {
        "$type": "color",
        "$value": "{{color.alpha.black.10}}",
        "$description": "A faint see-through dark edge inside a colour sample (swatches, colour sliders and areas, contrast samples), so any colour stands apart from the surface behind it."
      }
    },
    "surface": {
      "default": {
        "$type": "color",
        "$value": "{{color.grey.100}}",
        "$description": "Standard surface: cards, panels, the header."
      }
    },
    "text": {
      "default": {
        "$type": "color",
        "$value": "{{color.grey.900}}",
        "$description": "Main text."
      },
      "subtle": {
        "$type": "color",
        "$value": "{{color.grey.700}}",
        "$description": "Secondary text: descriptions, metadata."
      }
    }
  },
  "radius": {
    "container": {
      "sm": {
        "$type": "dimension",
        "$value": "{{radius.8}}",
        "$description": "Corners of cards, panels and dialogs, small."
      }
    },
    "control": {
      "md": {
        "$type": "dimension",
        "$value": "{{radius.6}}",
        "$description": "Corners of controls, medium."
      }
    }
  },
  "size": {
    "48": {
      "$type": "dimension",
      "$value": "48px",
      "$description": "Raw size step, 48 px. Only for fixed sizes inside a component; semantic sizes (size/control, icon, mark) point here."
    }
  },
  "typography": {
    "heading-xs": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Funnel Display, Funnel Display Fallback, system-ui, sans-serif",
        "fontSize": {
          "min": "20px",
          "max": "28px"
        },
        "fontWeight": 400,
        "lineHeight": 1.2,
        "letterSpacing": "0px"
      }
    },
    "label-md": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": "14px",
        "fontWeight": 500,
        "lineHeight": 1.4,
        "letterSpacing": "0px"
      },
      "$description": "Label MD: labels on controls and short UI text (buttons, chips, tabs, menu headings). Geist Medium, sentence case. Code and keys use Code SM and Code MD."
    },
    "body-xs": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": "12px",
        "fontWeight": 400,
        "lineHeight": 1.5,
        "letterSpacing": "0px"
      }
    }
  }
}
```

## Purpose

Shows one text color on one background as they really look, with their contrast ratio and the mark it earns: AAA, AA, AA large or fails. It is for tools and pages that check a color system against the contrast rules. To show a range of colors with their contrast against white, use the color range.

## Anatomy

- The block: a row on the default surface inside a subtle edge.
- The sample part: a 48 px square in the background color with "Aa" in the text color, drawn with the two colors given.
- The text part, a column: the result part (the ratio part in Label MD, then a small soft badge, the badge part, that says the mark in words) over the names part, the two roles in Body XS, such as "Text subtle on surface default".
- The result axis: AAA at 7:1 and up and AA at 4.5:1 take the success badge, AA large at 3:1 the warning badge, and fails the danger badge.

## When to use

- To show whether a text color can be read on a background, in a theme builder, a token editor or a color system's documentation.
- Side by side in a grid, one per pair a system uses.

## When not to use

- To show a whole range of colors. Use the [color range](color-range.md).
- For a color that is not text or a control's edge: the marks are the rules for text and for what identifies a control.

## Behavior

The ratio is worked out from the two colors as WCAG 2.2 counts it, a see-through color laid over what is under it, and written with two decimals, never rounded up past what it is. The mark follows from the ratio. A page that has measured the pair itself can give the ratio, or the mark, and the pair shows them as given.

The sample keeps its two colors whatever the page's theme: a pair is drawn as it is.

## Accessibility

### What it does for you

- The sample is hidden from screen readers. They read the ratio in words ("Contrast 5.03 to 1"), the mark and the two roles.
- The mark is a word in the badge, not a color alone.
- In forced colors mode the sample keeps its two colors, since they are what it shows, and the edges take the reader's text color.

### What you must do

- Name the two roles, so a reader who cannot see the sample still knows which pair the mark belongs to.
- Give every word it says by itself, such as the badge's words and the ratio read aloud, in the page's language; each keeps its English unless the page gives its own.

### How to test

- With a screen reader, the pair is read as the ratio, the mark and the two roles, and the sample is not read.
- The mark shows as a word in the badge, so it reads without color.
- In a forced-color mode, the sample still shows its two colors.
- With text at 200% and the page 320 pixels wide, the pair fills the width it is given and its words stay whole.

## Usage rules

### Do

- Name the two roles, so the reader knows which pair the mark belongs to.
- Let the pair work out its ratio and mark from its colors.

### Don't

- Give a mark the two colors do not earn.
