Note:Never remove focus indicators without replacement. Use
:focus-visible to show focus only for keyboard users. Minimum 3:1 contrast ratio for focus
indicators (WCAG 2.4.11). Test with keyboard-only navigation.
2. Color Contrast and WCAG Compliance
WCAG Level
Normal Text
Large Text
Graphics/UI
AA (Minimum)
4.5:1 contrast
3:1 contrast (18px+)
3:1 contrast
AAA (Enhanced)
7:1 contrast
4.5:1 contrast (18px+)
3:1 contrast
Large Text
18px+ or 14px+ bold
Non-text Contrast
3:1 for icons, borders, focus indicators
Example: WCAG compliant color schemes
/* WCAG AA compliant color combinations *//* ✅ PASS AA: 4.52:1 contrast */.text-on-light { color: #595959; /* Dark gray */ background: #ffffff; /* White */}/* ✅ PASS AAA: 7.03:1 contrast */.text-high-contrast { color: #3d3d3d; /* Darker gray */ background: #ffffff; /* White */}/* ✅ PASS AA for large text: 3.01:1 */.large-text { font-size: 18px; color: #767676; background: #ffffff;}/* ❌ FAIL AA: 2.85:1 contrast */.insufficient-contrast { color: #999999; /* Too light */ background: #ffffff;}/* Color palette with contrast ratios */:root { /* Primary colors with AA compliance */ --primary: #007acc; /* 4.54:1 on white */ --primary-dark: #005a9e; /* 6.59:1 on white - AAA */ --primary-light: #3399dd; /* 3.02:1 on white - AA large */ /* Text colors */ --text-primary: #1a1a1a; /* 16.59:1 on white - AAA */ --text-secondary: #4d4d4d; /* 8.59:1 on white - AAA */ --text-tertiary: #666666; /* 5.74:1 on white - AA */ /* Background colors */ --bg-primary: #ffffff; --bg-secondary: #f5f5f5; --bg-tertiary: #e0e0e0; /* Status colors (AA compliant) */ --success: #0f7b0f; /* 4.51:1 on white */ --warning: #856404; /* 5.51:1 on white */ --error: #c41e3a; /* 5.14:1 on white */ --info: #006699; /* 5.52:1 on white */}/* High contrast theme */.high-contrast { --text-primary: #000000; --bg-primary: #ffffff; --link-color: #0000ff; --link-visited: #800080;}/* Accessible link colors */a { color: #0066cc; /* 5.54:1 on white - AA */ text-decoration: underline; /* Don't rely on color alone */}a:visited { color: #663399; /* 5.04:1 on white - AA */}a:hover,a:focus { color: #004080; /* 7.52:1 on white - AAA */ text-decoration: underline;}/* Button contrast */.button-primary { background: #0066cc; /* 5.54:1 on white */ color: #ffffff; /* 5.54:1 on #0066cc */ border: 2px solid #0066cc;}.button-primary:hover { background: #0052a3; /* Higher contrast */}/* Disabled state must still meet 3:1 for UI components */.button:disabled { background: #cccccc; color: #666666; /* 3.13:1 - passes 3:1 for graphics */ opacity: 1; /* Don't use opacity alone for disabled state */}/* Status indicators with patterns (not color alone) */.status-success { color: #0f7b0f; background: #e6f4e6;}.status-success::before { content: '✓ '; /* Icon reinforces status */}.status-error { color: #c41e3a; background: #fce8eb; border-left: 4px solid currentColor; /* Additional indicator */}.status-error::before { content: '✗ ';}/* Form validation */.input-error { border-color: #c41e3a; /* 5.14:1 */ border-width: 2px;}.error-message { color: #c41e3a; font-weight: 500;}.error-message::before { content: '⚠ '; /* Icon supplements color */}/* Testing contrast with CSS *//*To test, use browser DevTools or online tools:- WebAIM Contrast Checker- Chrome DevTools (Accessibility panel)- Firefox Accessibility Inspector- https://contrast-ratio.com*/
Example: Dynamic contrast adjustment
/* Automatic contrast adjustment with color-contrast() (experimental) */.adaptive-text { /* Choose text color with best contrast */ color: color-contrast(var(--bg-color) vs white, black);}/* Relative color syntax for guaranteed contrast */.button { --bg: #007acc; background: var(--bg); /* Ensure text contrast by darkening/lightening */ color: oklch(from var(--bg) calc(l - 50%) c h);}/* Manual contrast function (CSS custom property) */:root { --bg-lightness: 95%; /* Light background */}.auto-contrast { background: hsl(0, 0%, var(--bg-lightness)); /* If background is light (>50%), use dark text, else light text */ color: hsl(0, 0%, calc((var(--bg-lightness) - 50%) * -100%));}/* Prefers-contrast media query */@media (prefers-contrast: more) { :root { --text-primary: #000000; --bg-primary: #ffffff; --link-color: #0000ff; } .button { border-width: 2px; font-weight: 600; } a { text-decoration: underline; text-decoration-thickness: 2px; }}@media (prefers-contrast: less) { /* Some users prefer lower contrast */ :root { --text-primary: #4d4d4d; --bg-primary: #fafafa; }}/* Force colors mode (Windows High Contrast) */@media (forced-colors: active) { .button { border: 1px solid currentColor; } .card { border: 1px solid CanvasText; } /* System colors in forced-colors mode: Canvas, CanvasText, LinkText, VisitedText, ActiveText, ButtonFace, ButtonText, Field, FieldText, Highlight, HighlightText, GrayText */}/* Contrast checker utility class */.contrast-check { /* For development: show contrast issues */ outline: 3px solid red;}.contrast-check[data-contrast="pass-aaa"] { outline-color: green;}.contrast-check[data-contrast="pass-aa"] { outline-color: yellow;}.contrast-check[data-contrast="fail"] { outline-color: red;}
Warning: Don't use color as the only means of conveying
information. Add icons, patterns, or text. Test with color blindness simulators. Aim for WCAG AA minimum (4.5:1
normal text, 3:1 large text).
/* Global reduced motion reset */@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } /* Exceptions for critical animations */ .loading-spinner, .progress-indicator, [role="progressbar"] { animation-duration: revert !important; transition-duration: revert !important; }}/* Motion safe wrapper */@media (prefers-reduced-motion: no-preference) { .motion-safe-fade { animation: fadeIn 0.5s ease; } .motion-safe-slide { animation: slideUp 0.3s ease; } .motion-safe-scale { transition: transform 0.3s ease; } .motion-safe-scale:hover { transform: scale(1.05); }}/* Provide alternative feedback for reduced motion */@media (prefers-reduced-motion: reduce) { .interactive-card:hover { /* Instead of animation, use border/outline */ outline: 3px solid #007acc; outline-offset: 2px; } .notification { /* Instead of slide-in, just appear with border */ border-left: 4px solid #007acc; } .loading-indicator { /* Instead of spinner, use pulsing dot */ animation: pulse 2s ease-in-out infinite; } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }}/* JavaScript detection *//*const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');if (prefersReducedMotion.matches) { // Disable complex animations document.body.classList.add('reduce-motion');}// Listen for changesprefersReducedMotion.addEventListener('change', (e) => { if (e.matches) { document.body.classList.add('reduce-motion'); } else { document.body.classList.remove('reduce-motion'); }});*//* CSS for JS-controlled animations */.reduce-motion .animated-element { animation: none !important; transition: none !important;}/* Best practices: 1. Respect user preference always 2. Keep functional animations (loading, progress) 3. Provide alternative feedback (borders, colors) 4. Test with reduced motion enabled 5. Consider duration reduction vs complete removal 6. Make smooth scrolling optional 7. Pause auto-playing content*/
Note:Always respect prefers-reduced-motion. Keep functional
animations (loading spinners, progress bars). Provide alternative feedback with borders, colors, or static
states. Test with system settings enabled.
Warning: Set color-scheme: light dark to enable browser dark mode for form
controls. Test both themes for contrast compliance. Provide user override option. Don't forget to adjust images
and icons for dark mode.
5. prefers-contrast and High Contrast Mode
Media Query
Value
Description
Action
prefers-contrast
no-preference
No contrast preference
Use default styles
prefers-contrast
more
User wants higher contrast
Increase contrast, borders
prefers-contrast
less
User wants lower contrast
Reduce contrast, soften
forced-colors
active
High contrast mode enabled
Use system colors
Example: High contrast mode support
/* Increased contrast preference */@media (prefers-contrast: more) { :root { /* Higher contrast colors */ --text-primary: #000000; --bg-primary: #ffffff; --link-color: #0000ff; --border-color: #000000; } /* Thicker borders */ .card, .button, input, textarea { border-width: 2px; } /* Stronger shadows */ .elevated { box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); } /* Bolder text */ body { font-weight: 500; } h1, h2, h3, h4, h5, h6 { font-weight: 700; } /* Remove transparency */ .translucent { opacity: 1; } /* Underline all links */ a { text-decoration: underline; text-decoration-thickness: 2px; }}/* Reduced contrast preference */@media (prefers-contrast: less) { :root { --text-primary: #4d4d4d; --bg-primary: #fafafa; --border-color: #e0e0e0; } /* Softer borders */ .card { border-color: #f0f0f0; } /* Lighter shadows */ .elevated { box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); }}/* Windows High Contrast Mode (forced-colors) */@media (forced-colors: active) { /* System will override most colors */ /* Use system color keywords */ body { background: Canvas; color: CanvasText; } a { color: LinkText; } a:visited { color: VisitedText; } .button { background: ButtonFace; color: ButtonText; border: 1px solid ButtonText; } .button:hover, .button:focus { background: Highlight; color: HighlightText; border-color: HighlightText; forced-color-adjust: none; /* Opt out of adjustment */ } /* Add borders where they don't exist */ .card, .section { border: 1px solid CanvasText; } /* Show focus indicators */ :focus-visible { outline: 3px solid Highlight; outline-offset: 2px; } /* Icons and decorative images */ .icon { /* Force icons to be visible */ forced-color-adjust: auto; } img { /* Preserve image colors */ forced-color-adjust: none; } /* Backplate for text over images */ .text-over-image { background: Canvas; color: CanvasText; padding: 0.5rem; } /* System color keywords: Canvas - Background CanvasText - Text on Canvas LinkText - Links VisitedText - Visited links ActiveText - Active link ButtonFace - Button background ButtonText - Button text ButtonBorder - Button border Field - Input background FieldText - Input text Highlight - Selected background HighlightText - Selected text GrayText - Disabled text Mark - Highlighted background MarkText - Highlighted text */}/* Combine prefers-contrast and forced-colors */@media (prefers-contrast: more) { /* Additional enhancements even without forced-colors */ .button { border-width: 2px; font-weight: 600; }}@media (prefers-contrast: more) and (forced-colors: active) { /* Extra emphasis in high contrast mode */ .important { border: 3px solid HighlightText; font-weight: bold; }}
Example: Comprehensive high contrast implementation
Note:forced-colors: active indicates Windows High Contrast Mode. Use system
colors (Canvas, CanvasText, LinkText, etc.). Add borders to borderless elements. Test with Windows High Contrast
Mode enabled.
6. Accessible Typography and Reading Flow
Aspect
Guideline
WCAG
Recommendation
Font Size
Minimum 16px body text
Best practice
Use rem for scalability
Line Height
1.5 for body text minimum
WCAG 1.4.8 (AAA)
1.5-1.8 for readability
Line Length
45-75 characters optimal
WCAG 1.4.8 (AAA)
Max 80ch recommended
Paragraph Spacing
1.5x font size minimum
WCAG 1.4.12 (AA)
Use margin-bottom
Letter Spacing
0.12em minimum
WCAG 1.4.12 (AA)
Adjustable by user
Word Spacing
0.16em minimum
WCAG 1.4.12 (AA)
Avoid justified text
Example: Accessible typography
/* WCAG compliant typography */:root { /* Base font size */ font-size: 16px; /* Never below 16px */}body { font-family: system-ui, -apple-system, sans-serif; font-size: 1rem; /* 16px, user scalable */ line-height: 1.6; /* WCAG 1.4.8: minimum 1.5 */ color: #1a1a1a; /* Text spacing requirements (WCAG 1.4.12) */ letter-spacing: 0.05em; word-spacing: 0.1em;}/* Headings */h1 { font-size: 2.5rem; line-height: 1.2; margin-bottom: 1rem;}h2 { font-size: 2rem; line-height: 1.3; margin-bottom: 0.875rem;}h3 { font-size: 1.5rem; line-height: 1.4; margin-bottom: 0.75rem;}/* Paragraph spacing: 1.5x font size minimum */p { margin-bottom: 1.5em; /* WCAG 1.4.8 */ max-width: 70ch; /* Optimal line length */}/* Line length control */.readable-content { max-width: 70ch; /* 45-75 characters optimal */ margin-left: auto; margin-right: auto;}/* Prevent long lines */.article { max-width: min(70ch, 100% - 2rem);}/* Adjustable text spacing */.user-adjustable-spacing { /* Allow user to override */ letter-spacing: var(--user-letter-spacing, 0.05em); word-spacing: var(--user-word-spacing, 0.1em); line-height: var(--user-line-height, 1.6);}/* Avoid justified text (creates rivers) */p { text-align: left; /* Not justify */}/* Small text (captions, footnotes) */.small-text { font-size: 0.875rem; /* 14px minimum */ line-height: 1.6;}/* Large text for better readability */.large-text { font-size: 1.125rem; /* 18px */ line-height: 1.7;}/* Avoid narrow columns */.column { min-width: 20ch; /* Prevent narrow text */}/* Lists */ul, ol { margin-bottom: 1.5em; padding-left: 2em;}li { margin-bottom: 0.5em; line-height: 1.6;}/* Code blocks */code, pre { font-family: 'Monaco', 'Courier New', monospace; font-size: 0.875em; line-height: 1.6;}pre { padding: 1rem; overflow-x: auto; border-radius: 4px;}/* Blockquotes */blockquote { margin: 1.5em 0; padding-left: 1.5em; border-left: 4px solid #007acc; font-style: italic; line-height: 1.7;}/* Links in text */a { color: #0066cc; text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 2px;}a:hover,a:focus { text-decoration-thickness: 2px;}/* User preference: larger text spacing */@media (prefers-contrast: more) { body { letter-spacing: 0.1em; word-spacing: 0.16em; line-height: 1.8; } p { margin-bottom: 2em; }}
Example: Responsive and accessible typography
/* Fluid typography with clamp() */:root { --font-size-base: clamp(1rem, 0.9rem + 0.5vw, 1.125rem); --font-size-lg: clamp(1.125rem, 1rem + 0.625vw, 1.25rem); --font-size-xl: clamp(1.25rem, 1.1rem + 0.75vw, 1.5rem); --font-size-2xl: clamp(1.5rem, 1.3rem + 1vw, 2rem); --font-size-3xl: clamp(2rem, 1.6rem + 2vw, 3rem);}body { font-size: var(--font-size-base); line-height: 1.6;}h1 { font-size: var(--font-size-3xl); line-height: 1.2;}/* Maintain readability at all sizes */.content { /* Prevent text from getting too wide */ max-width: min(70ch, 90vw); margin-inline: auto; padding-inline: 1rem;}/* Dyslexia-friendly options */.dyslexia-friendly { font-family: 'OpenDyslexic', 'Comic Sans MS', sans-serif; font-size: 1.125rem; line-height: 1.8; letter-spacing: 0.1em; word-spacing: 0.2em;}/* Text that wraps nicely */h1, h2, h3 { text-wrap: balance; /* Balance lines */ max-width: 30ch;}p { text-wrap: pretty; /* Prevent orphans */}/* Avoid ALL CAPS */.avoid-caps { /* Don't use text-transform: uppercase for long text */ /* If necessary, use title case instead */}/* Underlining without cutting descenders */a { text-decoration-skip-ink: auto;}/* Focus on readability, not aesthetics */.readable-first { /* Prioritize these properties */ font-size: 1.125rem; line-height: 1.7; letter-spacing: 0.02em; max-width: 65ch;}/* Tables */table { font-size: 0.9375rem; line-height: 1.6;}th, td { padding: 0.75rem 1rem; text-align: left;}/* Prevent orphans and widows */p { orphans: 2; widows: 2;}/* User zoom support */@media (min-width: 768px) { /* Don't prevent user zoom */ /* Never use: maximum-scale=1, user-scalable=no */}/* Font loading performance */@font-face { font-family: 'CustomFont'; src: url('/fonts/custom.woff2') format('woff2'); font-display: swap; /* Show fallback immediately */}/* WCAG 1.4.12: Text spacing must be customizable *//* Users should be able to override these without breaking layout */* { /* Line height: at least 1.5x font size */ /* Paragraph spacing: at least 2x font size */ /* Letter spacing: at least 0.12x font size */ /* Word spacing: at least 0.16x font size */}/* Test with browser zoom at 200% *//* Test with large text accessibility settings */
CSS Accessibility Best Practices
Never remove focus outlines without providing alternatives (use
:focus-visible)
Maintain minimum 4.5:1 contrast for normal text, 3:1 for large text (WCAG AA)