Says whether a foreground and background pair is readable, as a WCAG 2 contrast ratio with a pass or fail against each requirement the pair is held to. Reach for it wherever a colour is chosen and somebody then has to live with reading it: a design-system or design-token table that lists every pair in use, a theme or appearance editor, the brand and accent colour fields in an admin panel or tenant settings, checking the output of a generated or imported palette, a warning next to a colour input before the value is saved, an accessibility audit or review screen, chart series and status colours, dark-mode review, and a Storybook or docs page that has to show the figure rather than claim it. Common asks it answers: "contrast checker react", "color contrast component", "wcag contrast ratio", "contrast ratio calculator", "check contrast of two colors", "is this text readable on this background", "4.5:1", "3:1", "7:1", "AA vs AAA contrast", "accessible color combination", "accessible text color on background", "relative luminance javascript", "wcag 2 contrast formula", "luminance contrast js", "shadcn contrast checker", "tailwind color contrast check", "design token contrast audit", "contrast ratio for oklch", "contrast of a css variable", "check contrast of theme tokens", "dark mode contrast check", "large text contrast threshold", "non-text contrast 1.4.11", "focus ring contrast", "icon button contrast", "contrast of semi-transparent text", "overlay text contrast", "why does my contrast checker say rgb(0,0,0)", "getComputedStyle color returns oklch", "コントラスト比 計算", "色のコントラスト チェック", "WCAG コントラスト react", "文字が読めるか 判定", "アクセシビリティ 配色 チェック", "背景色と文字色 比率". **It resolves the colour the browser actually paints, which is why it works on a current theme at all.** The usual implementation reads `getComputedStyle(el).color` and pulls three integers out with a regex, and that assumes the computed value is `rgb(...)`. It is not: measured in Chrome 152, `oklch(0.7 0.15 250)` computes to `oklch(0.7 0.15 250)`, `lab(50% 40 -30)` to `lab(50 40 -30)`, `color(display-p3 1 0 0)` unchanged, and `color-mix(in srgb, red 30%, blue)` to `color(srgb 0.3 0 0.7)`. That is not a corner case any more — reading the eight principal theme tokens off shadcn/ui's own documentation site today, every one of them computes to `lab(...)`, and its main stylesheet holds 431 occurrences of `lab(` against one of `rgb(`. So the regex version reports nothing on the exact palettes people most want to check. This paints the value onto a 1×1 canvas and reads the pixel instead, which makes the browser do the conversion and handles every notation it ships, named colours and out-of-gamut values included. **It resolves the colour where you asked about it, not wherever a probe happened to sit.** `var(--primary)` and `currentColor` only mean something at a position in the tree, and a probe parked at the end of `body` does not return an error for them — it returns the inherited text colour. Measured: the same `var(--token)` string resolved to `oklch(0.55 0.2 25)` inside the subtree that declares it and to `rgb(26, 26, 26)` on a body-level probe, with nothing marking the second as unresolved. A detached element is worse still, answering the empty string for all thirteen notations tried, plain hex among them. Validity is settled separately with `CSS.supports`, because `color: notacolor` also computes to the inherited colour and is otherwise indistinguishable from a correct answer. **It re-measures when the theme changes, which is neither a prop change nor a resize.** If the inputs are tokens, both strings are identical before and after a dark-mode toggle while the answer moves: measured, adding the `dark` class to `<html>` took `--foreground` from 0/0/0 to 250/250/250 and `--background` from 255/255/255 to 10/10/10, turning a 21.00:1 pair into an 18.97:1 one with nothing for React to hear. A `MutationObserver` watches the document element's `class`, `style` and `data-theme`, and `prefers-color-scheme` is watched for a page themed by media query alone. A contrast figure that has gone stale is worse than none, because it is the number the reader is trusting. **A translucent colour has no ratio on its own, and this says so instead of assuming white.** `rgba`, `oklch(... / 0.6)` and `color-mix(..., transparent)` are detected by painting over white and over black and comparing, with no notation parsed; when one of them has no opaque colour behind it the status is `indeterminate` rather than a confident number. When there is something behind it, the real ancestor stack is composited in order rather than skipped — a 60%-black panel over a 251/251/250 page is 100/100/100, which is neither of them. Text is composited over the background and the background over the backdrop, in that order, because text sits on the background: collapsing the two reports a ratio against a colour that is nowhere on screen. Alpha is quantised to eight bits first, which is what the compositor does — against a canvas over twelve pairs the float form missed five and the quantised form matched all twelve. **Large text takes the weight as well as the size.** The rule is 18pt at any weight or 14pt when bold, so 18.67px is large at weight 700 and normal-size at weight 400, and a checker handed only a size cannot tell which — the same pair needs 3:1 in one case and 4.5:1 in the other. Sizes are accepted as CSS lengths, so `14pt` and `1.5rem` both work; the relative ones are measured against the document rather than guessed against a 16px root. An unknown size is held to the normal-text requirement rather than excused from one. 1.4.11 Non-text Contrast is reported as 3:1 at AA and AAA both, because it has no AAA counterpart and inventing a stricter one would fail pairs that conform. WCAG 2 only, deliberately. APCA — the perceptual model drafted for WCAG 3, and the fair objection to 2.x's ratio — is not computed here: it is not a Recommendation, its coefficients are versioned, and a number that disagrees with the reference implementation is worse than no number on a screen whose whole job is to be trusted. The figures are rounded the way the published checkers round them so they can be cross-checked, except in the thousandth-wide band where rounding up would read as meeting a requirement the verdict denies. Nothing is said in colour alone, which would be a poor joke here: every verdict is a word, the icons are `aria-hidden`, and the headline is a live region so a reader dragging a colour slider hears the new ratio. `contrastRatio`, `relativeLuminance`, `compositeOver`, `isLargeText`, `requiredRatio`, `gradeContrast`, `measureColorIn`, `findBackdrop` and `measureContrast` are all exported, the last of them synchronous and hook-free so a token table can grade dozens of pairs in one pass instead of one hook per row. Official shadcn/ui has nothing here, and the measurement is not close: fetching every entry in its registry today — 65 listed, 62 fetchable, code-block and questionnaire and trace 404 on both style tracks — and searching all 223,289 bytes of it, the strings contrastRatio, relativeLuminance, luminance, WCAG, APCA, sRGB, getImageData and CSS.supports are every one of them a zero hit. Official never computes a luminance and never reads a pixel. Within pulld it sits beside color-picker, which chooses a colour, and theme-toggle, which switches between sets of them — neither asks whether the result can be read — and it is the only component here that resolves a CSS colour of any notation to the bytes a screen will actually paint.
pnpm dlx shadcn@latest add "https://pulld.pages.dev/r/color-contrast.json"