I've spent years building and breaking design systems, and if there's one thing I've learnt, it's that accessibility isn't a 'nice-to-have'—it's the foundation. When we audit a system, we aren't just ticking boxes; we're ensuring our components work for everyone, regardless of how they navigate the web.

Automating Colour Contrast Audits

I love using CSS custom properties to manage my palette, but we need a way to flag when combinations fall below WCAG AA standards. Here is a trick I use to create a 'debug mode' for contrast using CSS variables.

/* CSS Debugger for Contrast */
[data-a11y-debug="true"] .btn-primary {
  outline: 5px solid red !important;
  position: relative;
}

[data-a11y-debug="true"] .btn-primary::after {
  content: "Check Contrast!";
  background: red;
  color: white;
  font-size: 10px;
  position: absolute;
  top: -20px;
}

This simple CSS overlay helps me visually identify components that haven't been vetted yet. For a more programmatic approach, I've found that using a JS-based contrast checker during the design token build step is much more reliable.

import { colorContrast } from 'some-a11y-lib';

const auditTokens = (tokens) => {
  tokens.forEach(token => {
    const ratio = colorContrast(token.background, token.foreground);
    if (ratio < 4.5) {
      console.error(`🚨 Contrast failure: ${token.name} ratio is ${ratio}`);
    }
  });
};

const theme = { background: '#FFFFFF', foreground: '#767676' }; // Fails AA
auditTokens([{ name: 'Body Text', ...theme }]);

Enforcing Semantic HTML in React Components

In my experience, developers often reach for a `div` when a `button` or `section` is required. I prefer building 'Polymorphic' components that force a sensible default but allow flexibility.

/* React Polymorphic Box Component */
const Box = ({ as: Component = 'div', children, ...props }) => {
  return <Component {...props}>{children}</Component>;
};

// Usage in Audit
const MyComponent = () => (
  <Box as="main">
    <Box as="header">Accessible Heading</Box>
    <Box as="nav">Navigation items...</Box>
  </Box>
);

What I find fascinating is how often we forget to validate the presence of ARIA labels. I use TypeScript to make certain accessibility props mandatory for specific components.

interface IconButtonProps {
  icon: React.ReactNode;
  'aria-label': string; // Mandatory for accessibility
  onClick: () => void;
}

const IconButton = ({ icon, 'aria-label': ariaLabel, onClick }: IconButtonProps) => (
  <button onClick={onClick} aria-label={ariaLabel} type="button">
    {icon}
  </button>
);

Visible Focus States and Keyboard Navigation

Never, ever use `outline: none`. Instead, I like to use `:focus-visible` to ensure keyboard users see a clear highlight while mouse users don't see a 'distracting' ring.

/* Global Focus Ring Audit */
:focus {
  outline: none; /* Only if replaced by focus-visible */
}

:focus-visible {
  outline: 3px solid var(--focus-ring-color, #005fcc);
  outline-offset: 2px;
  box-shadow: 0 0 0 4px rgba(0, 95, 204, 0.3);
}

For complex components like Modals, we must handle 'Focus Trapping'. Here's a pattern I've found effective for auditing whether a component correctly captures the tab key.

const useFocusTrap = (ref) => {
  useEffect(() => {
    const handleKeyDown = (e) => {
      if (e.key !== 'Tab') return;
      
      const focusables = ref.current.querySelectorAll('button, [href], input');
      const first = focusables[0];
      const last = focusables[focusables.length - 1];

      if (e.shiftKey && document.activeElement === first) {
        last.focus();
        e.preventDefault();
      } else if (!e.shiftKey && document.activeElement === last) {
        first.focus();
        e.preventDefault();
      }
    };

    document.addEventListener('keydown', handleKeyDown);
    return () => document.removeEventListener('keydown', handleKeyDown);
  }, [ref]);
};

Testing for Reduced Motion

We often forget that animation can cause physical discomfort for some users. I always audit my design system's motion tokens to respect the user's system preferences.

/* Motion Audit Utility */
@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;
  }
}

In React, I use a custom hook to conditionally apply animations based on this preference. It's a much cleaner way to handle it than scattered CSS overrides.

const usePrefersReducedMotion = () => {
  const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);

  useEffect(() => {
    const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
    setPrefersReducedMotion(mediaQuery.matches);

    const handler = () => setPrefersReducedMotion(mediaQuery.matches);
    mediaQuery.addEventListener('change', handler);
    return () => mediaQuery.removeEventListener('change', handler);
  }, []);

  return prefersReducedMotion;
};

Wrapping Up

Auditing for accessibility isn't a one-time event; it's a continuous process of refinement. By baking these checks directly into your CSS and React components, you make the 'right way' the 'easy way' for your entire team.

  • Use CSS variables and debug modes to visually audit contrast and focus states.
  • Leverage TypeScript to enforce mandatory ARIA attributes in your component library.
  • Always respect system-level preferences like reduced motion to ensure a comfortable experience for all.

I'd love to see how you're auditing your own systems! If you found this helpful, let's connect. Follow me on Twitter/X or LinkedIn for more CSS and design system tips.