Accessibility
Sveltopia Colors uses the Radix 12-step scale system, where each step has a semantic purpose that guides accessible color usage. Critical text/background pairings are APCA-validated and auto-corrected during generation.
Step semantics
Each step in the 12-step scale has a designed purpose and an accessibility guideline. Using steps for their intended purpose ensures accessible results:
| Steps | Tailwind | Purpose | Accessibility guideline |
|---|---|---|---|
| 1–2 | 50–100 | Backgrounds | Safe as backgrounds for steps 11–12 text * |
| 3–5 | 200–400 | Interactive surfaces | Safe as backgrounds for step 12 text (by design) |
| 6–7 | 500–600 | Borders | Sufficient contrast against backgrounds (by design) |
| 8–10 | 700–850 | Solid fills | White/contrast text at large font sizes ** |
| 11–12 | 900–950 | Text | Accessible on background steps 1–2 * |
* Enforced: the generator auto-corrects steps 11–12 against steps 1–2 if contrast falls below APCA thresholds.
** Validated as warning: step 9 is checked for white text readability but not auto-corrected, since saturated hues (yellow, lime) intentionally use dark text instead.
Guidelines marked "by design" follow from the Radix step system's lightness progression — each step group is spaced to maintain perceptual contrast with its intended pairings.
Safe combinations
These pairings are designed to meet APCA contrast thresholds:
text-orange-950 on bg-orange-50)text-blue-950 on bg-blue-200)bg-orange-800 — some saturated hues may need dark text instead)border-blue-600 on bg-blue-50)Avoid these pairings
The palette gives you the building blocks for accessible interfaces — but which combinations you use is up to you. Follow the step semantics above and you'll produce accessible results without needing to think about contrast ratios.
Why APCA over WCAG 2.x?
WCAG 2.x's contrast ratio algorithm has known issues:
- It treats dark-on-light and light-on-dark identically, but human perception doesn't
- It doesn't account for font size — a huge heading needs less contrast than 12px body text
- It produces false positives (passing combinations that are hard to read) and false negatives (failing combinations that are perfectly readable)
APCA (Accessible Perceptual Contrast Algorithm) addresses all of these. It's being developed for WCAG 3 and produces results that better match how humans actually perceive contrast.
A concrete example: white text on a saturated orange button (step 800) fails WCAG 2.x with a ratio of 2.87:1 — well below the 4.5:1 threshold. But the same pairing passes APCA at every practical button font size, because APCA accounts for the fact that large, bold text on a vivid background is perfectly readable. This is the kind of false negative that makes WCAG 2.x frustrating for design systems with saturated brand colors.
How validation works
During palette generation, the generator validates and enforces contrast for the most critical pairings:
- Text steps (11–12) are checked against background steps (1–2). Step 12 must meet body text contrast (APCA ≥ 75); step 11 must meet large text contrast (APCA ≥ 60). If either falls short, the generator automatically adjusts lightness until the threshold is met.
- Solid fill step (9) is checked for white text readability (APCA ≥ 60). This is flagged as a warning rather than auto-corrected, since some saturated hues (yellow, lime) intentionally use dark text on solid backgrounds.
Other step relationships (borders against backgrounds, text on interactive surfaces) follow from the Radix scale's perceptual lightness curve — each step group is spaced to maintain usable contrast with its intended pairings. You can verify any palette using the programmatic API below.
Automatic remediation: You don't need to manually check text contrast. If a brand color would produce text steps (11–12) that fall below APCA thresholds against background steps (1–2), the generator automatically adjusts lightness while preserving hue and chroma as closely as possible.
Testing your implementation
Use the programmatic API to validate palettes in your CI pipeline:
import { generatePalette, validatePaletteContrast, formatContrastReport } from '@sveltopia/colors';
// generatePalette returns a single mode — generate both
const light = generatePalette({ brandColors: ['#FF4F00'] });
const dark = generatePalette({ brandColors: ['#FF4F00'], mode: 'dark' });
const palette = {
light: light.scales,
dark: dark.scales,
_meta: {
tuningProfile: light.meta.tuningProfile,
inputColors: ['#FF4F00'],
generatedAt: new Date().toISOString()
}
};
const report = validatePaletteContrast(palette);
if (!report.passed) {
console.error(formatContrastReport(report));
process.exit(1);
}
console.log(`All ${report.totalChecks} checks passed.`);