-
+
{gradient}
@@ -370,63 +337,244 @@ const GradientSection: React.FC = () => {
)
}
+// Anchors are the References list at the bottom; cvdPalette.tsx has a matching copy.
+const Ref: React.FC<{ n: number }> = ({ n }) => (
+
[{n}]
+);
+
+// Scoping the real style objects per column means the preview can't drift from the tokens.
+// -mark = chart marks, -text = foreground. Air quality alone has no -text — its mark doubles
+// as text, and the audit checks it at the text floors. Ink is per value: glucose and air
+// quality need white in light where the other fills take near-black.
+const DARK_INK = '#12151b';
+const LIGHT_INK = '#fff';
+type SignalSwatch = { value: string; ink: string };
+const SIGNALS: { name: string; mark: string; text: string; light: SignalSwatch; dark: SignalSwatch }[] = [
+ { name: 'Glucose', mark: '--mdhui-color-glucose-mark', text: '--mdhui-color-glucose-text', light: { value: '#c4291c', ink: LIGHT_INK }, dark: { value: 'red-orange-40', ink: DARK_INK } },
+ { name: 'Sleep', mark: '--mdhui-color-sleep-mark', text: '--mdhui-color-sleep-text', light: { value: '#7b88c6', ink: DARK_INK }, dark: { value: 'indigo-40', ink: DARK_INK } },
+ { name: 'Heart rate', mark: '--mdhui-color-heart-rate-mark', text: '--mdhui-color-heart-rate-text', light: { value: '#e35c33', ink: DARK_INK }, dark: { value: 'red-orange-40', ink: DARK_INK } },
+ { name: 'Activity', mark: '--mdhui-color-activity-mark', text: '--mdhui-color-activity-text', light: { value: '#f5b722', ink: DARK_INK }, dark: { value: 'gold-20', ink: DARK_INK } },
+ { name: 'Air quality', mark: '--mdhui-color-air-quality-mark', text: '--mdhui-color-air-quality-mark', light: { value: 'teal-55', ink: LIGHT_INK }, dark: { value: 'teal-35', ink: DARK_INK } },
+];
+
+// Every sample sits in one column and its token name in the gutter beside it, so the
+// card reads as plausible content on the left and a token index on the right.
+const Line: React.FC<{ token: string; children: React.ReactNode }> = ({ token, children }) => (
+
+);
+const Divider: React.FC<{ token: string; color: string }> = ({ token, color }) => (
+
+);
+const Caption: React.FC<{ children: React.ReactNode }> = ({ children }) => (
+
{children}
+);
+
+const TokenContextCard: React.FC<{ scheme: 'light' | 'dark' }> = ({ scheme }) => (
+
+
+
text
+
118 mg/dL
+
Blood glucose, last 7 days
+
Your readings stayed in range.
+
Updated 2 hours ago
+
Sync unavailable
+
+
accents — as text, then as fill
+
View details
+
In range
+
Running low
+
Overdue
+
Log reading
+
3 due
+
+
+
signals — name in its text color, mark shows its value
+ {SIGNALS.map(signal => {
+ const swatch = scheme === 'light' ? signal.light : signal.dark;
+ return (
+
+
{signal.name}
+
{swatch.value}
+
+ );
+ })}
+
+
surface background-color-1 · card background-color-0 · card edge border-color-0
+
+);
+
+const TokensInContext: React.FC = () => (
+
+);
+
const ColorDocs: React.FC = () => {
return (
-
The "Magic Number" Color System
-
In design systems compliant with WCAG 2.0 (and Section 508), the term "magic number" refers to the specific luminance difference required
- between two colors to ensure they meet minimum contrast standards for accessibility.
- The higher the magic number, the greater the contrast and accessibility.
-
-
- - Magic number of 40+: Meets WCAG 2.0 AA Large Text (18pt/14pt bold or larger) contrast. For example, grey-90 and indigo-50.
- - Magic number of 50+: Meets WCAG 2.0 AA contrast or AAA Large Text contrast. For example, grey-90 and red-40. We recommend this level of contrast for most web content.
- - Magic number of 70+: Meets WCAG 2.0 AAA contrast. For example, grey-10 and red-80.
-
-
Additionally, a grade of 50 will meet the Section 508 AA contrast requirement against both pure white (#000) and pure black (#fff).
+
The "magic number" color system
+
The MyDataHelps color system makes accessible color the default, in light mode and dark. It
+ takes the U.S. Web Design System's approach, gives it more vibrant hues, and adds
+ APCA: a newer way of measuring contrast that dark mode needs, and the one WCAG 3
+ is expected to adopt.
+
Each color's name carries a number, and that number tells you how light or dark the color is.
+ The gap between two of those numbers is the "magic number": the wider the gap,
+ the more contrast the pair is guaranteed to have, whichever two hues you picked. Choosing a
+ readable pair comes down to subtraction, so nobody has to run colors through a contrast checker
+ to know the answer.
+
Usage
+
color: var(--mdh-[hue]-[grade]) Example: color: var(--mdh-blue-40)
+
The palette is 25 hues plus a grey, each in 19 grades from 1
+ (near white) to 99 (near black), published as --mdh-* CSS custom properties. Ten
+ gradients cover decorative surfaces, and the color-blind-safe palettes cover charts.
+
In component code, always go through the semantic --mdhui-* tokens rather than the
+ raw palette — the raw --mdh-* vars exist to define them, and the semantic layer is
+ what keeps light and dark mode both correct.
+
Which magic number to use
+
Find the row for what you're coloring. WCAG is the legal floor that Section 508
+ requires; APCA is the perceptual model drafted for WCAG 3, which asks for a bigger
+ gap to reach the same readability. Both apply in either color scheme, so the APCA number is the one
+ to reach for when you want a single figure to remember — dark mode is audited against it, and light
+ pairs that only meet the WCAG minimum can still land short of it.
+
+
+
+
+ | What you're coloring |
+ WCAGminimum |
+ APCArecommended, especially in dark mode |
+
+
+
+
+ | Body text16–18px regular — the default for most content |
+ 50+ | 80+ |
+
+
+ | Bold or emphasized text16px bold — too small to count as large text |
+ 50+ | 70+ |
+
+
+ | Large text24px regular (18pt) — WCAG's large-text tier starts here |
+ 40+ | 70+ |
+
+
+ | Headlines36px regular, or 24px bold |
+ 40+ | 60+ |
+
+
+ | Highest-contrast textWCAG AAA at body size |
+ 70+ | 80+ |
+
+
+ Functional elementsinput borders, focus rings, checkbox and toggle boundaries, icons, chart marks |
+ 40+ | 40+ |
+
+
+ | Structural elementscard edges, separators, dividers — 10 and 20 in light, 70 and 60 in dark |
+ no minimum |
+
+
+ | Backgroundsthe ends of the ramp — in dark: cards grey-85, app background grey-95, wells grey-99 |
+ 1-5 light · 85-99 dark |
+
+
+
+
+
Why dark mode needs the bigger gaps. The same gap buys less perceptual contrast
+ at the dark end of the ramp: a 50-gap measures Lc 67 near the light end but only Lc 42 near the dark
+ end, and WCAG rates that dark pair higher (5.5:1 vs 4.6:1) — the discrepancy APCA exists to
+ catch. Dark answers don't mirror light ones either: text on tinted backgrounds needs 55 on the light
+ end but 40 on the dark end — a band further out than symmetry would suggest — and status accents
+ ride the 35 half-step on the grade-85 cards.
- More about A, AA, and AAA
+ The standards behind the numbers — WCAG and APCA
-
Always aim for AA. In some cases, A may be acceptable:
+
WCAG levels. Level A doesn't say anything about contrast; the minimums start at AA:
- - A: Minimum level. Text must have a contrast ratio of at least 3:1 for large text (18pt/14pt bold or larger) and 4.5:1 for normal text against its background. This level is the basic requirement for accessibility, ensuring that text is readable for most users with mild visual impairments.
- - AA: Mid-range level. Text must have a contrast ratio of at least 4.5:1 for normal text and 3:1 for large text. This is the recommended level for most web content and is required for legal compliance in many regions (such as Section 508 in the US).
- - AAA: Highest level. Text must have a contrast ratio of at least 7:1 for normal text and 4.5:1 for large text. This level is intended for content that needs to be accessible to the widest possible audience, including users with significant visual impairments. AAA is not required for most content, but is encouraged where possible.
+ - AA (SC 1.4.3
): at least 4.5:1 for normal text and 3:1 for large text (18pt, or 14pt bold). This is the level to hit for most web content, and it's what Section 508 requires in the US.
+ - AAA (SC 1.4.6
): at least 7:1 for normal text and 4.5:1 for large. Aim here when you want the widest possible audience; it's encouraged rather than required.
+ - Non-text elements (SC 1.4.11, Level AA
): anything functional — input borders, focus rings, checkbox and toggle boundaries, icons, chart marks — needs at least 3:1 against what's next to it. Purely structural pieces (card edges, separators, dividers) are exempt.
-
- Source: W3C: Understanding WCAG 2.1 Contrast (Minimum)
-
-
-
-
- Will this change with WCAG 3.0?
-
-
While WCAG 2.0 uses relative luminance to determine color contrast, WCAG 3.0 (potentially Dec 2025) is expected to use APCA (Accessible Perceptual Contrast Algorithm).
- APCA is a new method for evaluating color contrast that takes into account human perception more effectively than previous methods.
-
The MyDataHelps color palette will be updated as necessary to comply with WCAG 3.0.
+
APCA. WCAG 2.x measures contrast as a ratio of relative luminance; APCA
+ (Accessible Perceptual Contrast Algorithm), drafted for WCAG 3, models how contrast actually
+ looks to people. The difference shows up most in dark mode, where the WCAG 2.x
+ math is too optimistic about dark-on-dark pairs. Every pairing here is measured under both,
+ taking the weaker of the two text/background directions, and APCA's text tiers need bigger gaps:
+
+ - Gap 60+ clears Lc 45 — APCA's minimum for large, heavy text such as headlines: 36px regular or 24px bold (ramp floor 49.3).
+ - Gap 70+ clears Lc 60 — its minimum for content text that is not body, column, or block text (floor 70.9).
+ - Gap 80+ clears Lc 75 — its minimum for columns of body text, at fonts no smaller than 24px/300, 18px/400, 16px/500 or 14px/700 (floor 81.5).
+
+
Those are APCA's minimums, and the text rows of the table above derive from them — read
+ that column as a floor, never as headroom. One caveat the table understates: its body row spans
+ “16–18px regular”, but Lc 75 covers 18px regular and 16px medium;
+ 16px regular falls into APCA's Lc 90 tier, which no gap in the table reaches. Set body copy at
+ 18px, or carry 16px at medium weight.
+
Lc 90 is APCA's preferred level for running text, and widening the gap is not a reliable way
+ to reach it: the guarantee is not monotone in gap size. Gap 82 clears Lc 90, but 85 and 89 do
+ not (both 84.0). Individual pairs go well past it — white on a dark card measures
+ Lc 102 in the weaker direction — so measure the pair you are actually using rather than
+ reasoning from the gap. node scripts/color-audit.mjs ramp --json prints the
+ guaranteed floor at every gap.
+
WCAG 3 is still a draft and APCA isn't legally required yet, so the palette conforms to both:
+ WCAG 2.x ratios stay the hard floor, the APCA measurements add the perceptual picture on top,
+ and the dark-mode semantic tokens are built at the APCA gaps.
-
Usage
-
color: var(--ce-[hue]-[grade]) Example: color: var(--ce-blue-40)
-
UI Use Cases
-
- - The 5 lightest (1-5) and darkest (95-99) grades are typically used as background colors.
- - The 55 grade is the lightest legible grade against backgrounds 1-5.
- - The 10 and 20 are typically used for border colors.
-
-
Data Visualization Use Cases
-
- - Air Quality: Cyan
- - Sleep: Indigo
- - Heart rate: Red-Orange
- - Activity: Yellow
-
+
Tokens in context
+
The same card under each scheme. Read each row left to right: the sample on the left is drawn with
+ the token named on the right. Every color here is a semantic --mdhui-* token, so one set
+ of rules produces both cards; in dark mode those tokens map onto the grey ramp and brighter accent
+ grades, and npm run audit:colors checks every pairing against the role floors.
+
+
The signal rows are the data-visualization colors: glucose and heart rate on red-orange,
+ activity on gold, sleep on indigo, air quality on teal. Glucose and heart rate resolve to the
+ same grade in dark for both duties, and in light they sit only Lc 12 apart — under the Lc 15
+ this page asks of chart marks. Don't separate them by color alone in either scheme; use shape,
+ position, or a label.
+
The full ramp
+
Every hue, lightest to darkest. Hover a swatch and the ramp marks the closest
+ grade on either side that's far enough away to clear each band — so you can read a safe pairing
+ straight off the row:
+
+ 40+ · AA large text
+ 50+ · AA body text
+ 70+ · AAA
+
+
The same hover shows what that color measures against white and black — the WCAG 2.x ratio and
+ the APCA Lc in both directions, each with the strongest role it supports. Click a
+ swatch to copy its variable.
{colors.map((hue) => (
))}
+
References
+
+ - U.S. Web Design System: color tokens — the grade-and-magic-number approach this system follows.
+ - W3C: Understanding WCAG 2.1 — Contrast (Minimum) — the AA and AAA text floors.
+ - W3C: Understanding WCAG 2.1 — Non-text Contrast — the 3:1 functional floor and the structural exemption.
+ - APCA in a Nutshell — the perceptual contrast model and its Lc tiers.
+
);
};