Color and Visual Design Guidelines

1. Color Contrast Calculations and Tools

Element Type WCAG Level AA WCAG Level AAA Calculation Method
Normal Text 4.5:1 minimum 7:1 minimum Relative luminance ratio
Large Text (>18pt or 14pt bold) 3:1 minimum 4.5:1 minimum Relative luminance ratio
UI Components (buttons, form borders) 3:1 minimum N/A Against adjacent colors
Graphical Objects (icons, chart elements) 3:1 minimum N/A Against background
Focus Indicators WCAG 2.2 3:1 minimum N/A Against background + unfocused state
Disabled Elements No requirement No requirement Best practice: maintain 3:1

Example: Color contrast calculations and fixes

<!-- Poor contrast (FAIL) -->
<p style="color: #777; background: #fff;">
  This text has 4.47:1 contrast - FAILS AA for normal text
</p>

<!-- Fixed contrast (PASS AA) -->
<p style="color: #595959; background: #fff;">
  This text has 7.0:1 contrast - PASSES AA and AAA
</p>

<!-- Large text (18pt+) with lower contrast OK -->
<h2 style="color: #767676; background: #fff; font-size: 24px;">
  Large text with 3.01:1 contrast - PASSES AA for large text
</h2>

<!-- Button with sufficient contrast -->
<button style="
  background: #0066cc; 
  color: #fff; 
  border: 2px solid #004499;">
  Text: 8.59:1 (PASS), Border: 5.14:1 against background (PASS)
</button>

<!-- Focus indicator with sufficient contrast -->
<style>
.accessible-link:focus {
  outline: 3px solid #0066cc; /* 3.02:1 against white background */
  outline-offset: 2px;
}
</style>
<a href="#" class="accessible-link">Accessible focus indicator</a>

<!-- Icon with sufficient contrast -->
<svg style="fill: #595959;" role="img" aria-label="Settings">
  <!-- Icon with 7:1 contrast against white background -->
  <path d="M..."></path>
</svg>
Contrast Ratio Passes For Visual Example
21:1 Maximum possible (black on white) Perfect contrast
7:1+ AAA normal text, AA large text Excellent readability
4.5:1 - 7:1 AA normal text Good readability
3:1 - 4.5:1 AA large text, UI components Acceptable for large text/UI
<3:1 Fails all requirements Poor contrast

Example: JavaScript contrast checker

// Calculate contrast ratio between two colors
function getContrastRatio(color1, color2) {
  const l1 = getRelativeLuminance(color1);
  const l2 = getRelativeLuminance(color2);
  
  const lighter = Math.max(l1, l2);
  const darker = Math.min(l1, l2);
  
  return (lighter + 0.05) / (darker + 0.05);
}

// Calculate relative luminance
function getRelativeLuminance(hexColor) {
  // Convert hex to RGB
  const rgb = hexToRgb(hexColor);
  
  // Convert RGB to sRGB
  const srgb = rgb.map(val => {
    val = val / 255;
    return val <= 0.03928 
      ? val / 12.92 
      : Math.pow((val + 0.055) / 1.055, 2.4);
  });
  
  // Calculate luminance
  return 0.2126 * srgb[0] + 0.7152 * srgb[1] + 0.0722 * srgb[2];
}

function hexToRgb(hex) {
  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
  return result ? [
    parseInt(result[1], 16),
    parseInt(result[2], 16),
    parseInt(result[3], 16)
  ] : null;
}

// Check WCAG compliance
function checkWCAG(foreground, background, fontSize, isBold) {
  const ratio = getContrastRatio(foreground, background);
  const isLargeText = fontSize >= 18 || (fontSize >= 14 && isBold);
  
  const aaThreshold = isLargeText ? 3 : 4.5;
  const aaaThreshold = isLargeText ? 4.5 : 7;
  
  return {
    ratio: ratio.toFixed(2),
    passAA: ratio >= aaThreshold,
    passAAA: ratio >= aaaThreshold,
    isLargeText
  };
}

// Usage
const result = checkWCAG('#595959', '#ffffff', 16, false);
console.log(`Contrast: ${result.ratio}:1`);
console.log(`AA: ${result.passAA ? 'PASS' : 'FAIL'}`);
console.log(`AAA: ${result.passAAA ? 'PASS' : 'FAIL'}`);
Tool Type Features URL
WebAIM Contrast Checker Online Simple, accurate, shows pass/fail webaim.org/resources/contrastchecker/
Contrast Ratio (Lea Verou) Online Real-time, color picker contrast-ratio.com
Colour Contrast Analyser Desktop app Eyedropper, simulations TPGi CCA (free)
Chrome DevTools Browser Built-in, suggests colors Chrome DevTools > Elements > Color picker
WAVE Extension Browser Full page analysis wave.webaim.org/extension/
Stark Plugin Design tool Figma/Sketch integration getstark.co
Note: Always test contrast on actual backgrounds. Overlays, gradients, and images can affect perceived contrast even if colors pass calculation.

2. Color Blind Accessibility Patterns

Color Vision Deficiency Prevalence Colors Affected Design Impact
Protanopia (Red-blind) ~1% males, 0.01% females Red appears dark, confused with green Red/green combinations problematic
Deuteranopia (Green-blind) ~1% males, 0.01% females Green appears beige, confused with red Red/green combinations problematic
Tritanopia (Blue-blind) ~0.001% all Blue/yellow confusion Blue/yellow combinations problematic
Achromatopsia (Total color blindness) ~0.003% all No color perception (grayscale) All color-only indicators fail

Example: Color blind friendly patterns

<!-- Bad: Color only for status -->
<span style="color: red;">Error</span>
<span style="color: green;">Success</span>

<!-- Good: Color + icon + text -->
<span class="status-error">
  <svg aria-hidden="true" class="icon">...error icon...</svg>
  Error: File not found
</span>

<span class="status-success">
  <svg aria-hidden="true" class="icon">...checkmark...</svg>
  Success: File uploaded
</span>

<!-- Bad: Color-only link distinction -->
<p style="color: #000;">
  Visit our <a href="#" style="color: #00f;">website</a> for more info.
</p>

<!-- Good: Underline + color -->
<p>
  Visit our <a href="#" style="color: #0066cc; text-decoration: underline;">
    website
  </a> for more info.
</p>

<!-- Good: Form validation with multiple cues -->
<style>
.input-error {
  border: 2px solid #d32f2f; /* Color */
  border-left: 5px solid #d32f2f; /* Extra visual cue */
  background: #ffebee; /* Background tint */
  background-image: url('error-icon.svg'); /* Icon */
  background-position: right 8px center;
  background-repeat: no-repeat;
}
</style>

<input 
  type="email" 
  class="input-error"
  aria-invalid="true"
  aria-describedby="email-error">
<span id="email-error" class="error-text">
  ⚠ Please enter a valid email address
</span>

<!-- Chart with patterns + colors -->
<style>
.chart-bar-1 { 
  fill: #0066cc; 
  stroke-dasharray: none; 
}
.chart-bar-2 { 
  fill: #00897b; 
  stroke-dasharray: 5,5; /* Stripe pattern */
}
.chart-bar-3 { 
  fill: #f57c00; 
  stroke-dasharray: 2,2; /* Dot pattern */
}
</style>
Design Strategy Implementation Example Use
Use Patterns/Textures Add stripes, dots, hatching to colored areas Chart bars, map regions
Add Icons/Symbols Include visual symbols with color Status indicators (✓, ✗, ⚠)
Use Shape Differences Different shapes for categories Chart markers (●, ■, ▲)
Add Text Labels Label all colored elements Chart legends, graph labels
Underline Links Don't rely on color alone for links Body text links
Sufficient Brightness Contrast Ensure luminance difference even if hue fails All color combinations
Position/Order Cues Use consistent positioning for status Traffic lights (red=top, green=bottom)

Example: Color blind friendly color palettes

<!-- Safe color combinations (work for most CVD types) -->
<style>
:root {
  /* Blue-Orange palette (deuteranopia/protanopia safe) */
  --color-primary: #0077bb;    /* Blue */
  --color-secondary: #ee7733;  /* Orange */
  --color-accent: #009988;     /* Teal */
  
  /* High contrast palette */
  --color-dark: #332288;       /* Dark purple */
  --color-medium: #44aa99;     /* Teal */
  --color-light: #ddcc77;      /* Tan */
  
  /* Status colors with sufficient difference */
  --color-success: #117733;    /* Dark green */
  --color-warning: #ddaa33;    /* Gold */
  --color-error: #cc3311;      /* Red */
  --color-info: #0077bb;       /* Blue */
  
  /* Avoid these combinations */
  /* Red + Green (common CVD issue) */
  /* Light green + Yellow */
  /* Blue + Purple (for some types) */
}

/* Sequential data with brightness steps */
.data-1 { background: #f7fbff; } /* Lightest */
.data-2 { background: #deebf7; }
.data-3 { background: #c6dbef; }
.data-4 { background: #9ecae1; }
.data-5 { background: #6baed6; }
.data-6 { background: #4292c6; }
.data-7 { background: #2171b5; }
.data-8 { background: #08519c; }
.data-9 { background: #08306b; } /* Darkest */
</style>
Warning: Never use color as the only visual means of conveying information. Always provide additional cues like text, icons, patterns, or positional differences.

3. High Contrast Mode Support

High Contrast Feature OS/Browser CSS Detection Design Impact
Windows High Contrast Windows 10/11 @media (prefers-contrast: high) Overrides all colors with system palette
Increased Contrast macOS, iOS @media (prefers-contrast: more) Suggests stronger contrast
Forced Colors Windows, browsers @media (forced-colors: active) Limited color palette enforced
Inverted Colors macOS, iOS @media (inverted-colors: inverted) All colors inverted

Example: High contrast mode support

<!-- Default styles -->
<style>
.button {
  background: #0066cc;
  color: white;
  border: 2px solid #0066cc;
  padding: 12px 24px;
}

/* High contrast mode adjustments */
@media (prefers-contrast: high) {
  .button {
    border-width: 3px; /* Thicker borders */
    font-weight: 600; /* Bolder text */
  }
  
  /* Ensure focus indicators are extra visible */
  .button:focus {
    outline: 4px solid;
    outline-offset: 4px;
  }
}

/* Forced colors mode (Windows High Contrast) */
@media (forced-colors: active) {
  .button {
    /* Use system colors instead of custom colors */
    background: ButtonFace;
    color: ButtonText;
    border-color: ButtonText;
  }
  
  .button:hover {
    background: Highlight;
    color: HighlightText;
    border-color: Highlight;
  }
  
  .button:disabled {
    color: GrayText;
    border-color: GrayText;
  }
  
  /* Preserve important visual distinctions */
  .icon {
    /* Force icon to be visible */
    forced-color-adjust: auto;
  }
  
  /* Custom focus indicators */
  .button:focus {
    outline: 2px solid ButtonText;
    outline-offset: 2px;
  }
}

/* Inverted colors support */
@media (inverted-colors: inverted) {
  /* Re-invert images/media so they appear normal */
  img, video {
    filter: invert(1);
  }
}

/* Increased contrast (macOS/iOS) */
@media (prefers-contrast: more) {
  body {
    /* Slightly increase base contrast */
  }
  
  .text-secondary {
    /* Make secondary text darker */
    color: #444; /* Instead of #666 */
  }
}
</style>
System Color Keyword Use Case Example
ButtonFace Button background background: ButtonFace;
ButtonText Button text, borders color: ButtonText;
Canvas Page background background: Canvas;
CanvasText Body text color: CanvasText;
LinkText Hyperlinks color: LinkText;
Highlight Selected/active background background: Highlight;
HighlightText Selected text color: HighlightText;
GrayText Disabled text color: GrayText;
Field Input background background: Field;
FieldText Input text color: FieldText;
Note: In forced colors mode, custom colors are overridden. Use forced-color-adjust: none sparingly and only when necessary to preserve critical visual information.

4. Visual Indicators Beyond Color

Indicator Type Visual Cue Use Case Implementation
Icons ✓ ✗ ⚠ ℹ ★ Status, ratings, actions SVG or icon fonts with ARIA labels
Text Labels "Success", "Error", "Required" All important states Explicit text with semantic markup
Underlines Solid, dashed, dotted, double Links, emphasis, errors text-decoration
Borders Thickness, style variations Focus, active state, errors border with different widths/styles
Patterns/Textures Stripes, dots, crosshatch Charts, graphs, maps SVG patterns or CSS gradients
Size/Weight Font size/weight changes Hierarchy, emphasis font-size, font-weight
Position/Layout Spatial arrangement Navigation state, progress Flexbox/Grid positioning
Shape Circle, square, triangle Chart markers, bullets SVG or CSS shapes

Example: Multi-sensory status indicators

<!-- Status with icon + color + text -->
<div class="alert alert-success">
  <svg aria-hidden="true" class="alert-icon">
    <use href="#checkmark-icon"></use>
  </svg>
  <strong>Success:</strong> Your changes have been saved.
</div>

<div class="alert alert-error">
  <svg aria-hidden="true" class="alert-icon">
    <use href="#error-icon"></use>
  </svg>
  <strong>Error:</strong> Unable to connect to server.
</div>

<div class="alert alert-warning">
  <svg aria-hidden="true" class="alert-icon">
    <use href="#warning-icon"></use>
  </svg>
  <strong>Warning:</strong> Your session will expire in 5 minutes.
</div>

<style>
.alert {
  padding: 12px 16px;
  border-left: 5px solid; /* Thick left border */
  display: flex;
  align-items: flex-start;
  gap: 12px;
}

.alert-icon {
  flex-shrink: 0;
  width: 24px;
  height: 24px;
}

.alert-success {
  background: #e8f5e9;
  border-color: #2e7d32;
  color: #1b5e20;
}

.alert-success .alert-icon {
  fill: #2e7d32;
}

.alert-error {
  background: #ffebee;
  border-color: #c62828;
  color: #b71c1c;
}

.alert-error .alert-icon {
  fill: #c62828;
}

.alert-warning {
  background: #fff3e0;
  border-color: #f57c00;
  color: #e65100;
}

.alert-warning .alert-icon {
  fill: #f57c00;
}
</style>

<!-- Required field indicators -->
<label for="email">
  Email Address
  <abbr 
    title="required" 
    aria-label="required"
    style="color: #d32f2f; text-decoration: none; font-weight: bold;">
    *
  </abbr>
  <span class="sr-only">(required)</span>
</label>

<!-- Link with underline (not just color) -->
<p>
  Read our <a href="/privacy" style="
    color: #0066cc;
    text-decoration: underline;
    text-decoration-thickness: 2px;
    text-underline-offset: 2px;">privacy policy</a> for details.
</p>

<!-- Progress indicator with multiple cues -->
<div class="progress-steps">
  <div class="step step-completed">
    <span class="step-number">1</span>
    <svg class="step-icon" aria-hidden="true">✓</svg>
    <span class="step-label">Personal Info</span>
  </div>
  <div class="step step-active">
    <span class="step-number">2</span>
    <span class="step-label">Payment</span>
  </div>
  <div class="step step-upcoming">
    <span class="step-number">3</span>
    <span class="step-label">Confirmation</span>
  </div>
</div>
Warning: WCAG Success Criterion 1.4.1 requires that color is not used as the only visual means of conveying information. Always combine color with at least one other visual differentiator.

5. CSS Custom Properties for Theming

Theme Aspect Custom Property Pattern Use Case
Colors --color-primary, --color-text Brand colors, semantic colors
Spacing --space-sm, --space-md Consistent spacing scale
Typography --font-size-base, --line-height Font sizes, line heights
Shadows --shadow-sm, --shadow-md Elevation levels
Borders --border-radius, --border-width Consistent borders
Transitions --transition-fast, --transition-slow Animation timing

Example: Accessible theming system with CSS custom properties

<!-- Theme system with light/dark modes -->
<style>
:root {
  /* Light theme (default) */
  --color-background: #ffffff;
  --color-surface: #f5f5f5;
  --color-text: #212121;
  --color-text-secondary: #757575;
  --color-primary: #1976d2;
  --color-primary-dark: #1565c0;
  --color-error: #d32f2f;
  --color-success: #388e3c;
  --color-warning: #f57c00;
  
  /* Ensure minimum contrast ratios */
  --color-link: #0066cc; /* 4.5:1 on white */
  --color-border: #e0e0e0;
  
  /* Typography */
  --font-size-base: 16px;
  --font-size-lg: 18px;
  --font-size-sm: 14px;
  --line-height-base: 1.5;
  --line-height-heading: 1.2;
  
  /* Spacing scale */
  --space-xs: 4px;
  --space-sm: 8px;
  --space-md: 16px;
  --space-lg: 24px;
  --space-xl: 32px;
  
  /* Accessible focus indicator */
  --focus-ring: 2px solid var(--color-primary);
  --focus-offset: 2px;
}

/* Dark theme */
@media (prefers-color-scheme: dark) {
  :root {
    --color-background: #121212;
    --color-surface: #1e1e1e;
    --color-text: #e0e0e0;
    --color-text-secondary: #b0b0b0;
    --color-primary: #90caf9;
    --color-primary-dark: #64b5f6;
    --color-error: #ef5350;
    --color-success: #66bb6a;
    --color-warning: #ffa726;
    --color-link: #90caf9; /* 7:1 on dark background */
    --color-border: #333333;
  }
}

/* High contrast mode adjustments */
@media (prefers-contrast: high) {
  :root {
    --color-text: #000000;
    --color-background: #ffffff;
    --color-border: #000000;
    --focus-ring: 3px solid;
    --focus-offset: 3px;
  }
}

/* Manual theme toggle support */
[data-theme="dark"] {
  --color-background: #121212;
  --color-surface: #1e1e1e;
  --color-text: #e0e0e0;
  --color-text-secondary: #b0b0b0;
  /* ... dark theme colors ... */
}

[data-theme="high-contrast"] {
  --color-background: #000000;
  --color-text: #ffffff;
  --color-primary: #ffff00;
  --color-link: #00ffff;
  /* Maximum contrast colors */
}

/* Apply theme variables */
body {
  background: var(--color-background);
  color: var(--color-text);
  font-size: var(--font-size-base);
  line-height: var(--line-height-base);
}

a {
  color: var(--color-link);
  text-decoration: underline;
}

button {
  background: var(--color-primary);
  color: var(--color-background);
  border: none;
  padding: var(--space-sm) var(--space-md);
}

button:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

.card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--border-radius, 8px);
  padding: var(--space-md);
}
</style>

<!-- Theme toggle -->
<button id="theme-toggle" aria-label="Toggle dark mode">
  <svg aria-hidden="true">...moon/sun icon...</svg>
</button>

<script>
const themeToggle = document.getElementById('theme-toggle');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');

// Get stored theme or use system preference
function getTheme() {
  const stored = localStorage.getItem('theme');
  if (stored) return stored;
  return prefersDark.matches ? 'dark' : 'light';
}

// Apply theme
function setTheme(theme) {
  document.documentElement.setAttribute('data-theme', theme);
  localStorage.setItem('theme', theme);
  themeToggle.setAttribute('aria-label', 
    theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'
  );
}

// Initialize theme
setTheme(getTheme());

// Toggle theme
themeToggle.addEventListener('click', () => {
  const current = document.documentElement.getAttribute('data-theme') || 'light';
  setTheme(current === 'dark' ? 'light' : 'dark');
});

// Listen for system preference changes
prefersDark.addEventListener('change', (e) => {
  if (!localStorage.getItem('theme')) {
    setTheme(e.matches ? 'dark' : 'light');
  }
});
</script>
Theming Best Practice Why Implementation
Test Contrast in All Themes Dark mode can fail contrast if not tested Verify 4.5:1 ratio in light AND dark
Respect System Preferences Users set preferences for a reason prefers-color-scheme media query
Allow Manual Override Some users want different than system Theme toggle with localStorage
Smooth Transitions Jarring changes can trigger issues transition: background 0.3s, color 0.3s
Announce Theme Changes Screen reader users need notification aria-live announcement
Preserve Focus Visibility Focus must be visible in all themes Test focus indicators in each theme
Note: CSS custom properties can't be used in media queries, but can be changed within them. Define theme variations inside @media blocks to adapt to user preferences.

Color and Visual Design Quick Reference

  • Normal text needs 4.5:1 contrast (AA), 7:1 (AAA); large text needs 3:1 (AA), 4.5:1 (AAA)
  • UI components and graphical objects need minimum 3:1 contrast against adjacent colors
  • Never use color as the only visual means - add icons, text labels, patterns, or shapes
  • Avoid red/green combinations - use blue/orange or add patterns/icons for colorblind users
  • Support high contrast mode with system color keywords (ButtonFace, CanvasText, etc.)
  • Links must be underlined or have 3:1 contrast with surrounding text
  • Test designs with color blindness simulators and actual users
  • Use CSS custom properties for themable, maintainable color systems
  • Respect prefers-color-scheme and provide manual theme toggle
  • Ensure focus indicators have 3:1 contrast in all themes (WCAG 2.2)