CSS: Accessibility & Modern Features

Write inclusive CSS and use the latest features that make layouts more powerful and flexible.

Semantic CSS (Meaningful Class Names)

Semantic CSS means naming classes by their purpose, not their visual appearance. A class named .btn-danger is semantic, it describes what the button means. A class named .red-button is not, it describes how it looks, and becomes misleading if the colour ever changes.

Semantic class names

Semantic names make CSS maintainable. If the design changes, you update the CSS rule, not every HTML element that uses the class.

  • .btn-primary, .btn-danger, describes role; works regardless of colour
  • .card-header, .card-body, describes structure, not appearance
  • .is-active, .is-hidden, state classes with a prefix clarify intent
  • Avoid: .red-text, .mt-20, .big-font, these describe appearance, not meaning
  • BEM (Block__Element--Modifier) is a popular convention: .card__title--featured

Semantic class names

The second button uses names that describe its role, not its colour or size.

Focus States and Keyboard Navigation

Keyboard users navigate pages by pressing Tab to move between interactive elements. The focused element must always be visually distinct, this is the focus ring. Removing it without a replacement is one of the most common accessibility failures.

Focus states

Never use outline: none or outline: 0 without providing a visible custom focus indicator. WCAG 2.1 requires focus indicators to be visible and meet a minimum contrast ratio.

  • The default browser focus ring is the minimum, but you can replace it with something better-looking
  • outline: none, removes the focus ring entirely, only do this if you provide a custom one
  • box-shadow: 0 0 0 3px ..., a common custom focus ring that respects border-radius
  • Test your page using only the keyboard: Tab, Shift+Tab, Enter, Space, arrow keys
  • All interactive elements (links, buttons, inputs) must be reachable and operable by keyboard

Custom focus ring

Tab to each element, a visible custom focus indicator replaces the default outline.

Screen Reader Considerations

Some CSS properties affect what screen readers can perceive. Understanding which CSS hides content visually versus hiding it from assistive technology is essential for building inclusive experiences.

CSS and screen readers

display: none and visibility: hidden both hide content from screen readers. To hide something visually but keep it readable by screen readers, use the visually-hidden utility pattern.

  • display: none, hidden from screen readers and layout
  • visibility: hidden, hidden from screen readers, space preserved
  • opacity: 0, invisible visually but still read by screen readers
  • visually-hidden pattern, clip + position: absolute trick to hide visually but keep in the accessibility tree
  • Generated content (::before, ::after) may or may not be read, never put meaningful text there

Visually hidden text

The label text is hidden visually but read by screen readers, the button has a meaningful accessible name.

Color Contrast Requirements (WCAG)

WCAG (Web Content Accessibility Guidelines) defines minimum contrast ratios between text and its background to ensure readability for people with low vision or colour blindness.

WCAG contrast ratios

Contrast ratio measures the difference in luminance between foreground and background. 1:1 is identical (no contrast); 21:1 is black on white (maximum contrast).

  • 4.5:1, AA normal text, minimum for body text under 18pt (or 14pt bold)
  • 3:1, AA large text, minimum for large text (18pt+) and UI components
  • 7:1, AAA normal text, enhanced level for body text
  • Check contrast with browser DevTools, or tools like WebAIM Contrast Checker
  • Colour alone should never be the only way to convey information
  • Never rely on colour to differentiate errors from success, always add an icon or label too

Contrast ratio

The first line passes WCAG AA; the second fails.

Reduced Motion (@prefers-reduced-motion)

Some users experience nausea, dizziness, or seizures from fast-moving animations. Most operating systems have a "Reduce motion" setting. The @prefers-reduced-motion media query detects this preference so you can tone down or remove animations.

@prefers-reduced-motion

Best practice: write animations normally, then add a prefers-reduced-motion block at the end that removes or simplifies them for users who need it.

  • prefers-reduced-motion: reduce, user has enabled the reduce motion OS setting
  • prefers-reduced-motion: no-preference, user has not set a preference (default)
  • You do not need to remove all animation, fades and simple opacity changes are usually acceptable
  • Disable parallax effects, auto-playing carousels, and bouncing loaders for these users
  • Test by enabling 'Reduce Motion' in your OS accessibility settings

@prefers-reduced-motion

The spinner animates normally by default but stops spinning when the user prefers reduced motion.

Dark Mode (@prefers-color-scheme)

@prefers-color-scheme detects whether the user's OS is in dark mode or light mode. Combined with CSS variables, it lets you implement a full dark mode theme without JavaScript.

@prefers-color-scheme

The cleanest approach is to define your theme colours as CSS variables in :root, then override them inside the dark mode media query. Every element using var() updates automatically.

  • prefers-color-scheme: dark, user's OS is in dark mode
  • prefers-color-scheme: light, user's OS is in light mode (default)
  • Define light-mode variables in :root, then override in the dark media query
  • CSS variables are the most maintainable approach, update the token, every element updates
  • You can also let the browser handle it with color-scheme: light dark on :root

@prefers-color-scheme

The card changes colours based on the OS dark mode setting.

:focus-visible for Better UX

:focus-visible is a smarter version of :focus. It shows the focus ring only when it is actually useful for keyboard navigation and hides it for mouse clicks where it looks out of place.

:focus-visible

:focus-visible lets you remove the focus ring on mouse-clicked buttons while keeping it for keyboard users. It is the modern best practice for all interactive elements.

  • :focus, fires for both mouse clicks and keyboard focus
  • :focus-visible, fires only when the browser determines a visible indicator is needed (keyboard, not mouse)
  • Use :focus-visible for buttons and links; use :focus for inputs (always show for text fields)
  • Browser support is now excellent, all modern browsers support it

:focus-visible

Click the button with your mouse, no ring. Tab to it, the ring appears.

Logical Properties

Logical properties replace physical directions (left, right, top, bottom) with writing-mode-aware equivalents. They work correctly in right-to-left languages like Arabic and Hebrew without needing extra CSS overrides.

Logical properties

Logical properties use inline (horizontal in LTR) and block (vertical in LTR) axes instead of physical directions. In RTL layouts they automatically flip.

  • margin-inline-start, left margin in LTR; right margin in RTL (replaces margin-left)
  • margin-inline-end, right margin in LTR; left margin in RTL (replaces margin-right)
  • padding-block-start, top padding in horizontal writing modes (replaces padding-top)
  • padding-block-end, bottom padding (replaces padding-bottom)
  • inset-inline-start, replaces left in positioned elements
  • border-inline, border-block, margin-inline, padding-block, all have logical equivalents

Logical properties

margin-inline and padding-block instead of left/right/top/bottom, works in both LTR and RTL.

aspect-ratio Property

aspect-ratio locks the width-to-height ratio of an element. When you set one dimension, the other is calculated automatically. It replaces the old padding-top percentage hack.

aspect-ratio

aspect-ratio is perfect for video embeds, image placeholders, and any element that must maintain a specific shape regardless of its width.

  • aspect-ratio: 16 / 9, standard widescreen video ratio
  • aspect-ratio: 1, square (same as 1 / 1)
  • aspect-ratio: 4 / 3, classic photo/screen ratio
  • If both width and height are explicitly set, aspect-ratio is ignored
  • Replaces the padding-top: 56.25% hack previously used for responsive video embeds

aspect-ratio

Both elements maintain their ratio regardless of the container width.

Filter Effects

The filter property applies graphical effects to an element and its contents, blurring, colour adjustment, brightness, and more. Multiple filters can be stacked in one declaration.

filter

filter applies to the entire element including its children. For background blurring only, use backdrop-filter instead, it applies the effect to everything behind the element.

  • blur(4px), blurs the element
  • brightness(0.7), dims the element (1 = normal, 0 = black)
  • contrast(1.5), increases contrast (1 = normal)
  • grayscale(1), fully desaturates (0 = colour, 1 = grey)
  • saturate(2), boosts colour saturation
  • sepia(0.8), applies a warm sepia tone
  • drop-shadow(2px 4px 8px rgba(0,0,0,0.3)), shadow that follows the element's shape (not the box)

filter effects

Four copies of the same image with different filter values applied.

Browser Compatibility and Feature Detection

Not every CSS feature is available in every browser. The @supports rule lets you check whether a browser supports a property before applying styles that depend on it.

@supports and feature detection

@supports applies a block of CSS only when the browser supports the given property and value. It is the CSS equivalent of a feature detection check in JavaScript.

  • @supports (display: grid), styles inside only apply if grid is supported
  • @supports not (display: grid), fallback styles for browsers without grid
  • @supports (container-type: inline-size), check for container query support
  • caniuse.com, the definitive resource for checking browser support of CSS features
  • Most modern CSS features (grid, flexbox, custom properties, clamp) are safe to use in all current browsers
  • Progressive enhancement: build a working baseline first, then layer in modern features

@supports

Grid layout is used when supported; flexbox is the fallback for older browsers.

Knowledge Check

1. What is the minimum contrast ratio required by WCAG AA for normal body text?

2. What does @prefers-reduced-motion: reduce signal?

3. What is the difference between :focus and :focus-visible?

4. Which CSS property should you use instead of margin-left for international/logical layouts?

5. What does filter: grayscale(1) do?

6. Which media query detects the user's dark mode preference?