FullStack Course LogoFullStack Course
Module: CSS
CSS·040·9 MIN READ

040: CSS Accessibility, Performance, and Compatibility

TOPICS COVERED: CSS Accessibility, Performance, and Compatibility

Learning outcomes

By the end of this lesson, you should be able to audit CSS for keyboard focus, contrast, reflow, motion, forced colors, and touch usability. You should also be able to spot visual and layout patterns that may be expensive, reduce blocking or unused CSS when the evidence supports it, use progressive enhancement and feature queries, and test compatibility without filling a stylesheet with speculative hacks.

Prerequisites and retrieval

Retrieve the HTML accessibility lesson, 021 color, 022 typography, 030 responsive design, 031 media queries, and 036 motion. Those lessons provide the HTML, color, type, responsive, media-query, and motion details this lesson builds on.

Accessibility: CSS can help or harm semantics

CSS cannot substitute for semantic HTML. It can, however, determine whether semantic controls remain usable: focus can disappear, text can become unreadable, content can be clipped, and interaction can depend on a device feature that a user does not have.

Focus visibility

Keyboard users need a clear indication of where focus is. :focus-visible lets the browser show that treatment when focus was reached in a way that calls for it, without forcing the same outline on every pointer interaction.

css
:where(a, button, input, select, textarea):focus-visible {
  outline: 3px solid #2563eb;
  outline-offset: 3px;
}

Avoid this broad reset:

css
*:focus {
  outline: none;
}

It is only defensible when you provide an equivalent focus treatment that remains obvious and usable.

Contrast and state

A control needs sufficient contrast in every state users can encounter, not just its default appearance:

css
.button {
  color: white;
  background: #2563eb;
}

.button:hover {
  background: #1d4ed8;
}

.button:disabled {
  color: #475569;
  background: #e2e8f0;
}

Check text contrast, non-text UI boundaries where the applicable requirement calls for them, focus indicators, and error states. A hover or disabled color that looks acceptable in isolation can still become hard to distinguish in context.

Color should not be the only signal for meaning:

css
.error {
  color: #b91c1c;
  border-inline-start: 4px solid currentColor;
}

.error::before {
  content: "Error: ";
  font-weight: 700;
}

If the word “Error” carries essential information, putting it in the HTML is better than relying on generated content. The message then exists in the document structure rather than depending on this particular CSS presentation.

Reflow and zoom

Fixed-height text containers are fragile. A larger text size, a longer translation, or a narrower viewport can make their content overflow or disappear:

css
/* fragile */
.card {
  height: 180px;
  overflow: hidden;
}

Prefer a size that establishes a minimum without preventing the content from growing:

css
.card {
  min-block-size: 11rem;
}

Test the page at 200% zoom and at narrow widths. Look for clipped text, unexpected horizontal scrolling, and controls that are no longer reachable or understandable.

Touch targets

Do not size an interactive control only around a tiny icon or a few pixels of text. Give the control a usable hit area:

css
.icon-button {
  min-inline-size: 2.75rem;
  min-block-size: 2.75rem;
  display: inline-grid;
  place-items: center;
}

The exact target guidance depends on the standard or design requirement your product follows. In practice, a generous hit area also helps users with imprecise pointing, not only users on touch screens.

Motion

Respect a user's reduced-motion preference for decorative movement:

css
@media (prefers-reduced-motion: reduce) {
  .decorative-motion {
    animation: none;
    transition: none;
  }
}

Removing decoration must not remove the meaning of a state change. Essential feedback still needs to be understandable without animation.

Color scheme and forced colors

If the page supports both light and dark color schemes, declare that expectation:

css
:root {
  color-scheme: light dark;
}

In Windows or another forced-color environment, the browser or operating system may replace authored colors. That is a problem when a decorative border, shadow, or background is the only visible boundary between controls and their surroundings.

Here is a targeted enhancement for a button boundary:

css
@media (forced-colors: active) {
  .button {
    border: 1px solid ButtonText;
  }
}

Use system colors when you are specifically adapting to forced-color mode. Do not try to recreate the entire system palette with custom paint.

Performance mental model

CSS performance is not one number and not one property. A stylesheet can affect several stages of the browser's work:

  • download/parse cost;
  • style calculation;
  • layout;
  • paint;
  • compositing.

That is why optimization by myth is unreliable. Measure the actual bottleneck before changing a rule.

Selector complexity

Modern browsers match selectors quickly, but extremely broad or repeatedly evaluated relational selectors can add matching work and make ownership harder to understand.

Prefer a clear component selector:

css
.order-card__total {}

over a remote dependency chain:

css
#app main .orders section article.order div.footer span.total {}

The primary benefit of the shorter selector is maintainability and predictable ownership. Any performance improvement is secondary, so do not rewrite selectors solely because a shorter-looking selector sounds faster.

Layout and paint

Repeated layout changes can become expensive when JavaScript reads geometry and then writes new geometry. CSS by itself can also create costly rendering work, particularly when a large area must be repainted for an elaborate effect.

Potential hotspots include:

  • huge blurred shadows;
  • filter/backdrop-filter over large regions;
  • very large fixed backgrounds;
  • animating layout dimensions on complex pages;
  • thousands of DOM elements with complex styling.

These are places to investigate, not automatic reasons to remove a design. Confirm the cost on the pages and devices that matter.

Critical and unused CSS concepts

At application scale, it can make sense to split CSS so a route does not load every style in the product. Build tooling can also extract and minify the code that is actually shipped.

Do not hand-inline a huge block of “critical CSS” without measuring its benefit and planning how it will be invalidated when styles change. Performance work needs real metrics, not a larger collection of special cases.

Font performance

font-display controls what happens while a web font is loading:

css
@font-face {
  font-family: "SiteSans";
  src: url("/fonts/site-sans.woff2") format("woff2");
  font-display: swap;
}

Limit font variants that the interface does not use, and test the fallback layout. Different font metrics can move text and controls when the final font arrives.

Image/background performance

CSS backgrounds do not provide HTML responsive-image selection features such as srcset in the same way. When an image is content and choosing an appropriate responsive source matters, use <picture> or <img> instead.

Progressive enhancement and @supports

Start with a usable fallback, then enhance it when the browser supports the desired feature:

css
.card-grid {
  display: flex;
  flex-wrap: wrap;
}

@supports (display: grid) {
  .card-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  }
}

Only write a fallback when your actual browser support matrix requires it. Supporting every historical browser indefinitely makes the stylesheet harder to reason about and can preserve code nobody needs.

A negated feature query can express a fallback explicitly:

css
@supports not (container-type: inline-size) {
  .component {
    /* fallback only if genuinely needed */
  }
}

Compatibility workflow

Use a support decision based on users and product requirements rather than on habit:

  1. Define target browsers from actual users/product requirements.
  2. Check current support for features you plan to use.
  3. Prefer progressive enhancement.
  4. Use automated prefixing/tooling when appropriate rather than hand-copying stale vendor-prefix recipes.
  5. Test actual browsers/devices for critical flows.
  6. Document intentional unsupported enhancements.

This process makes an unsupported enhancement an explicit, reviewable decision. It also keeps a fallback focused on browsers that the product truly supports.

Worked example: accessible/performance-safe card hover

This card uses hover as an optional enhancement, limits it to devices that can meaningfully hover, keeps focus visible, and removes the movement when reduced motion is requested:

css
.card {
  border: 1px solid #cbd5e1;
  transition:
    transform 140ms ease,
    box-shadow 140ms ease;
}

@media (hover: hover) and (pointer: fine) {
  .card:hover {
    transform: translateY(-2px);
    box-shadow: 0 6px 18px rgb(15 23 42 / 0.12);
  }
}

.card:focus-within {
  outline: 3px solid #2563eb;
  outline-offset: 3px;
}

@media (prefers-reduced-motion: reduce) {
  .card {
    transition: none;
  }

  .card:hover {
    transform: none;
  }
}

The hover treatment is optional, focus remains visible for keyboard interaction, and the motion can be reduced without losing the card's essential behavior.

Deep dive: reflow, user overrides, and system colors

A usable page must survive more than its default viewport. Users may enlarge text, zoom the page, increase text spacing, or apply system accessibility settings. Test those changes as real usage conditions rather than treating them as unusual edge cases.

Avoid fixed-height text containers:

css
/* fragile */
.alert {
  height: 3rem;
  overflow: hidden;
}

Prefer content-driven block size:

css
.alert {
  min-block-size: 3rem;
  padding: 0.75rem 1rem;
}

Users may override text spacing. Check increased line height, letter spacing, word spacing, and paragraph spacing without allowing content to clip.

Forced colors

When forced colors are active, a small targeted rule can restore a boundary that authored decoration would otherwise provide:

css
@media (forced-colors: active) {
  .button {
    border: 1px solid ButtonText;
  }
}

Do not unnecessarily fight system colors. Native controls and system color keywords often adapt better than a layer of custom decorative paint.

Contrast preferences

Preference queries can improve an already usable baseline:

css
@media (prefers-contrast: more) {
  .muted {
    color: CanvasText;
  }
}

Treat preference queries as enhancements. The baseline should not make content low-contrast and depend on a user's preference setting to become readable.

Deep dive: rendering performance

Rendering problems usually come from the total workload, not from one universally “slow property.” Consider the size and complexity of the DOM, how much layout and paint work changes, what effects cover, and when fonts and styles arrive.

Potential costs include:

  • large DOM + broad selectors;
  • layout triggered by size/geometry changes;
  • large paint areas;
  • blur/filter/backdrop-filter effects;
  • huge offscreen content;
  • web-font delays and layout shifts;
  • unused blocking CSS.

content-visibility

For a large section that starts offscreen, content-visibility: auto may let the browser skip some rendering work until that section is needed:

css
.long-report-section {
  content-visibility: auto;
  contain-intrinsic-size: auto 600px;
}

Use this only after measuring a real benefit. Skipped rendering can affect APIs that expect layout information, so test accessibility behavior and fragment navigation as well.

Containment

Containment can isolate some layout and paint work:

css
.widget {
  contain: layout paint;
}

It also changes layout and paint relationships. Container queries already establish some containment behavior, so do not add contain as a generic optimization without checking what relationship the component needs.

will-change

will-change is a hint for a specific upcoming change, not a general performance label:

css
.drawer {
  will-change: transform;
}

Overusing it can consume extra memory and create unnecessary compositing layers. Add it only when profiling shows that a repeatedly animated property benefits from the hint.

Font performance: loading strategy

Good typography and performance meet at font loading. Review the whole loading path, not just the font declaration:

  • limiting families/weights;
  • subsetting when appropriate;
  • font-display behavior;
  • fallback metric differences;
  • preload only for genuinely critical fonts;
  • avoiding a chain of CSS imports for fonts.

A visually perfect custom font is not a success if text remains invisible while it loads or the layout shifts severely when it replaces the fallback.

Measurement workflow

Use browser DevTools to isolate the stage that is actually slow:

  1. Is the problem loading, layout, paint, compositing, or JavaScript?
  2. Which element/property is involved?
  3. Does the problem reproduce with throttling?
  4. Does the proposed change improve the measured result?
  5. Did the optimization introduce accessibility or visual regressions?

The final question matters because a faster render that clips content or removes focus visibility is not a successful optimization. Performance work should produce evidence, not folklore.

CSS audit checklist

Accessibility

  • visible keyboard focus;
  • no hover-only essential controls;
  • contrast checked in all states;
  • no color-only meaning;
  • content reflows at zoom;
  • text is not clipped by fixed heights;
  • reduced-motion respected;
  • controls remain usable in forced colors.

Performance

  • no unused giant image effects;
  • font set is intentional;
  • CSS bundles are route/component appropriate for application scale;
  • no accidental expensive animation;
  • selector ownership is clear;
  • repeated visual effects are measured on target devices.

Compatibility

  • support matrix documented;
  • modern features checked before production use;
  • fallback is usable;
  • feature detection used where appropriate;
  • no stale vendor-prefix cargo cult.

Practice set

Apply the checks to a real page rather than only reading the rules:

  1. Audit an existing page at 200% zoom.
  2. Navigate entirely by keyboard.
  3. Enable reduced motion and forced colors.
  4. Throttle network and observe font/image fallback.
  5. Remove one expensive blur/shadow and compare rendering.
  6. Add a feature query around a progressive enhancement.

For each change, record what you observed and whether the result improved without creating an accessibility or compatibility regression.

Recap

CSS quality is broader than appearance. Accessible CSS preserves focus, contrast, reflow, user preferences, and operability. Performant CSS avoids unnecessary work and relies on measurement. Compatible CSS starts with a support policy and progressive enhancement, not with fear of modern features or a pile of copied hacks.

Official references

Reader page: /css/lesson/040/css-accessibility-performance-and-compatibility