CSE 134B Web Client Languages
  1. Presentation
  2. CSS
  3. For people
  4. CSS for People

Presentation · CSS 08

CSS for People

Every declaration lands in somebody's perception. This last page of the section turns the language around and looks at it from the other side: the readers, listeners, keyboard users, slow connections, other languages and future maintainers that CSS is for.

The earlier pages taught CSS as a language. This page asks what that language does to people: the accessibility work that is CSS's job, performance as usability, styling for other languages, the developers who maintain a style sheet, and where frameworks fit.

1. CSS as a humanistic medium

CSS is easy to dismiss as decoration applied after the real work. The opposite is closer to the truth. HTML decides what a page means; CSS decides whether a particular person can perceive and use it. Light gray text on white, a removed focus ring, an animation that cannot be stopped, a layout that falls apart at 200 percent zoom: each is a CSS decision, and each shuts somebody out. A style sheet has more audiences than its author usually pictures.

WhoWhat they need from the CSS
A sighted reader on a fast connectionA page that looks designed and appears quickly: the reader most authors test as.
A reader with low vision or a color-vision differenceEnough contrast, text that scales, and meaning that never depends on color alone.
A keyboard, switch or voice userA visible focus indicator, a sensible focus order, and targets big enough to hit.
A reader with vestibular sensitivity or attention limitsMotion that respects prefers-reduced-motion, and a calm, predictable layout.
A reader on a slow or metered connectionA page that does not wait on, or pay for, styles it never uses.
A reader in another language or scriptA layout that survives longer words, right-to-left text and vertical writing.
The developer who maintains it, including you in two yearsCSS that reads as clearly as it writes, and changes that stay local.

Serving all of them comes down to four qualities: the style sheet is accessible, fast, translatable and maintainable. Sections 2 to 5 take them in turn.

Start from what the platform already gives

The browser arrives with humane behavior built in: user-agent focus rings, form controls in the reader's language, relative units that follow the reader's font size, media queries that report preferences, and a cascade that lets the reader's own style sheet win when it must. So the first discipline is not to add accessibility on top of CSS but to avoid breaking what is already there. Nearly every CSS accessibility failure is an override that removed something and put nothing in its place: outline: none, text in fixed units, a fixed height that clips enlarged text. The Accessibility quality page traces this idea through every layer of a web client; this page covers the part that lives in the style sheet.

2. Accessibility: what CSS can give and take away

CSS cannot make bad HTML accessible. A div styled to look like a button is a picture of a button: not focusable, no role, deaf to Enter and Space. Everything below assumes real button, a href, label and landmark elements, and asks what CSS then does well or badly.

Focus you can see

For a keyboard user the focus indicator is the only way to know where they are; removing it is like hiding the mouse pointer. The most common accessibility bug in CSS is one line, often added because someone disliked the ring after a mouse click:

*:focus { outline: none; }

The :focus-visible pseudo-class is the proper answer. It matches only when the browser judges an indicator useful: after keyboard navigation or in a text field, but usually not after a mouse click on a button. Browsers' user-agent styles already draw their default ring with it. If the default does not suit the design, restyle it instead of deleting it:

@layer base { :root { --focus: #a3420f; } :focus-visible { outline: 0.2rem solid var(--focus); outline-offset: 0.2rem; } }

Use outline rather than box-shadow: an outline takes no layout space, follows rounded corners, and survives forced-colors mode, which removes box shadows. Measure it in rem so it grows with zoom, and check its contrast: WCAG 2.2 requires focus to be visible at level AA (success criterion 2.4.7), asks that a visual indicator of state meet 3:1 against its surroundings (1.4.11, non-text contrast), and sets a stricter size and contrast target for focus at level AAA (2.4.13).

Contrast, and never color alone

Contrast is the most measurable accessibility decision in a style sheet. WCAG's AA level, the usual legal and professional target, asks for:

  • 4.5:1 between normal text and its background;
  • 3:1 for large text (roughly 24 CSS pixels, or 18.66 bold) and for the parts of controls and graphics a person needs to see, such as input borders, icons and focus rings.

DevTools shows the ratio in its color picker. Separately, color must never be the only carrier of meaning (criterion 1.4.1): an error state needs a message or an icon as well as red, and a link in a paragraph needs an underline or another cue besides its hue. The Color section treats both in depth: Color for people computes contrast and shows what color alone fails to carry, and How color works covers color spaces.

Readers can also ask for more contrast. The prefers-contrast media feature reports that request, and a token-based style sheet can answer it in a few lines:

@media (prefers-contrast: more) { @layer base { :root { --text-muted: var(--text); } a { text-decoration-thickness: 0.15em; } :is(input, select, textarea) { border-color: currentColor; } } }

Motion

Parallax, large slides and zooms can cause dizziness and nausea for people with vestibular disorders. Operating systems offer a "reduce motion" setting, and prefers-reduced-motion reports it to CSS. The responsive page introduces preference queries; here, treat stillness as the baseline and motion as the enhancement:

@layer base { @media (prefers-reduced-motion: no-preference) { html { scroll-behavior: smooth; } dialog[open] { animation: rise 0.25s ease-out; } } @keyframes rise { from { opacity: 0; translate: 0 1rem; } } }

Many sites use the opposite approach, a global switch that cuts every animation short. It is a safety net for animation you do not control, but it relies on !important rather than design:

@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } }

Reduced motion does not mean no change: a fade still signals that something happened. The Animation topic (coming) covers motion in full.

Zoom, text size and reflow

Low-vision readers zoom the page or raise the browser's default font size, and a style sheet must survive both. Three WCAG criteria at level AA say what that means:

CriterionRequirementWhat breaks it in CSS
1.4.4 Resize textText can be enlarged to 200 percent without loss of content or function.Text in px, which ignores the font-size setting; boxes with a fixed height that clip enlarged text.
1.4.10 ReflowContent works at a width of 320 CSS pixels (a 1280-pixel window at 400 percent zoom) without scrolling in two directions.Fixed-width layouts and columns that never stack; wide content that is not allowed to scroll on its own.
1.4.12 Text spacingNothing is lost when the reader raises line height to 1.5, paragraph spacing to 2em, letter spacing to 0.12em and word spacing to 0.16em.Fixed heights and widths around text, and overflow: hidden on text containers.

The 320-pixel figure is a fact about the standard, not a unit to design in. A layout built from relative units, fluid tracks and normal flow meets all three without special effort, because it never assumed a size in the first place:

.card { width: 320px; height: 180px; font-size: 14px; overflow: hidden; } @layer layout { main { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); gap: var(--space); > article { min-block-size: 10rem; padding: var(--space); } } :is(pre, table) { overflow: auto; } }

The min() inside minmax() lets a track shrink below 18rem on a narrow viewport, which is exactly the 400 percent case. min-block-size sets a floor without a ceiling, so a card grows with its text. Code and wide tables scroll inside their own box, which WCAG allows. To test, zoom to 400 percent, then separately set the browser's default font size to its largest value.

Forced colors

Windows contrast themes apply forced colors: the browser replaces the page's colors with a small palette the reader chose, such as yellow text on black. Backgrounds, borders and text all take the reader's colors, and box shadows are dropped. CSS can detect the mode with @media (forced-colors: active) and can name the reader's palette with system color keywords such as Canvas, CanvasText, LinkText, ButtonText and Highlight.

Well-built pages need almost nothing. Failures come from meaning drawn only with a background or a shadow, which forced colors erase:

@layer base { button { background: var(--accent); color: var(--on-accent); border: 1px solid transparent; } } @media (forced-colors: active) { @layer base { nav [aria-current="page"] { text-decoration: underline 0.15em; } svg { fill: CanvasText; } } }

The transparent border is invisible normally, but forced colors redraw it in the reader's color, so the button keeps its edge. The query restores a cue the design carried in color alone and points icons at the reader's text color. The property forced-color-adjust: none opts an element out of forced colors entirely; keep it for the rare element, such as a color swatch, whose color is the content.

Hiding content properly

"Hidden" means different things to different readers. Choose by asking who should still perceive the content:

TechniqueSeenRead by a screen readerFocusableTakes space
display: none, or the hidden attributeNoNoNoNo
visibility: hiddenNoNoNoYes
opacity: 0NoYesYesYes
The visually hidden patternNoYesYesNo
aria-hidden="true" (HTML, not CSS)YesNoYes, which is a trapYes

Use display: none or hidden for content hidden from everyone, such as a closed panel; the Properties page covers both. Use the visually hidden pattern for text a screen reader should announce but the design does not show, such as the label of an icon-only button. opacity: 0 alone is usually a mistake: a keyboard user can still tab to the invisible element and land on nothing.

@layer utilities { .visually-hidden { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } } @layer components { body > a:first-child { position: absolute; inset-block-start: 0.5rem; inset-inline-start: 0.5rem; translate: 0 -200%; &:focus-visible { translate: 0; } } }

Here a class earns its place: the markup cannot say "hide this visually" on its own. The second rule styles the first link in the body, by convention a "skip to main content" link, which stays off-screen until a keyboard user reaches it.

Source order and visual order

Flexbox order, grid placement, row-reverse and absolute positioning can all show boxes in a different order from the HTML. The eye follows the visual order; the keyboard and screen readers follow the source. When they disagree, focus jumps around the screen.

Source order versus visual orderOn the left, four elements in source order: main is 1, aside is 2, nav is 3, footer is 4. On the right, a grid has placed nav at the top, aside on the left, main on the right and footer at the bottom, so the eye reads nav, aside, main, footer. A focus path drawn over the grid starts in main, jumps left to aside, then up to nav, then down past main to footer, because keyboard focus and screen readers follow the source order.Source order1 · main2 · aside3 · nav4 · footergridWhat the grid shows3 · nav2 · aside1 · main4 · footerfocus startswhat the keyboard and screen reader followwhat the eye follows: nav, aside, main, footer
Grid moved the boxes; it did not move the focus order. The focus path starts in the middle of the page and jumps around it.

WCAG names the requirement: when the sequence affects meaning, it must survive in the source (1.3.2, meaningful sequence), and focus must move in an order that preserves meaning and operability (2.4.3, focus order). The fix is almost never more CSS. Put the HTML in the order a reader should meet it, then use layout to arrange that order on the screen:

body { display: flex; flex-direction: column; } body > nav { order: -1; } @layer layout { body { display: grid; grid-template-areas: "nav" "main" "aside" "footer"; @media (width >= 60rem) { grid-template-columns: 1fr 3fr; grid-template-areas: "nav nav" "aside main" "footer footer"; } > nav { grid-area: nav; } > main { grid-area: main; } > aside { grid-area: aside; } > footer { grid-area: footer; } } }

The HTML lists nav, main, aside and footer in the order a reader needs. A narrow screen stacks them so; a wide one puts the aside beside main, a small and sensible difference. Rearranging is fine when visual and reading order agree, such as a wrapping card grid, and a bug when they disagree. A newer property, reading-flow, lets focus follow the visual order of flex and grid items, but it has shipped only in Chromium-based browsers and is not Baseline, so source order remains the fix.

For type in particular (weight and contrast together, all capitals, forced colors and readers who replace a site's fonts), see Type for people.

3. Performance as usability

A page that takes eight seconds to appear is unusable for the person who waited, however well it was designed. Speed is part of usability, and it matters most to readers on slow phones and metered data. The Performance quality page sets out the course's numbers and the whole delivery picture. This section covers what is specific to CSS.

CSS blocks rendering, on purpose

A browser will not paint a page until it has the style sheets that apply to it. That is deliberate: painting unstyled content and restyling it a moment later is worse than a short wait. The cost is that every style sheet in the head sits on the critical path to the first paint.

The render-blocking path for CSSA timeline. The browser downloads and parses the HTML and builds the DOM along the whole length of the timeline. Early on it finds a link to a style sheet and starts fetching the CSS, which takes most of the time, then parses it into the CSSOM. Until that parse finishes, the screen row is blank: rendering is blocked. Only when the styles are ready does the first paint happen.HTMLdownload and parse HTML, build the DOM<link rel=stylesheet>CSSfetch CSS over the networkparseScreenrender blocked: nothing painted yetfirst paintstyles readytime
HTML parsing carries on while the CSS downloads, but nothing is painted until the style sheet has arrived and been parsed.

Two more facts follow. A classic script in the head waits for style sheets above it, because the script might ask for computed styles, so a slow style sheet can also delay JavaScript. And a style sheet that pulls in another with @import cannot reveal the second file until the first has arrived, which stretches the critical path; Basics covers that case. A sheet limited with media="print", or to any media query that does not currently match, is still downloaded but does not block rendering.

Critical CSS, and the simpler answer

The classic optimization is critical CSS: extract the rules the first screenful needs, inline them in a style element in the head so the first paint needs no extra request, and load the rest of the style sheet without blocking. Tools such as Critical and Beasties automate the extraction.

It is a trade-off. Inlined CSS is downloaded again with every page because it cannot be cached separately, and the extraction can drift as the design changes. For small CSS, as this section's style encourages, one compact, compressed, cached style sheet usually wins. The fastest CSS is CSS never sent: open the DevTools Coverage panel, use the site, and delete the rules that never matched.

Selector cost is rarely the problem

Older advice warned against "expensive" selectors such as the universal selector or descendant combinators. Engines have long since optimized matching, and on real pages it is small next to network, layout and script time. Choose selectors for readability. If the Performance panel shows long "Recalculate Style" work, the usual cause is a very large DOM or a script repeatedly changing classes high in the tree, not any one selector.

Where a change enters the pipeline

After the first paint, the browser keeps rendering as things change. A change runs through some or all of four stages: style (work out each element's computed values), layout (work out sizes and positions), paint (draw pixels into layers) and composite (combine the layers on screen). How much of that work a change causes depends on which property changed.

Where a property change enters the rendering pipelineFour stages in a row: style, layout, paint, composite. Changing width, margin or inset runs layout, paint and composite. Changing color or background skips layout and runs paint and composite. Changing transform, translate or opacity can skip both and run on the compositor alone, which is why those are the properties to animate.StyleLayoutPaintCompositewidth, margin, insetlayout, paint and composite, every framecolor, backgroundpaint and compositetransform, opacitycomposite onlythe later a change enters, the less work each frame costs
Animate the properties that enter late. Geometry changes force layout, and layout can spread to the rest of the page.

An animation produces a frame about sixty times a second. Animating width, margin or inset-inline-start forces layout on every frame, often spreading to neighboring boxes, and on a slow phone the motion stutters. Moving the box with translate and fading it with opacity leaves layout alone, and the browser can usually hand the animation to the compositor:

@keyframes slide-in { from { left: -20rem; } to { left: 0; } } @layer components { @keyframes slide-in { from { translate: -100% 0; opacity: 0; } } @media (prefers-reduced-motion: no-preference) { dialog[open] { animation: slide-in 0.3s ease-out; } } }

The same pipeline explains layout thrashing in scripts: writing a style and then reading a size, in a loop, forces a fresh layout on every read.

Skip work for content off screen

On a long page, most content starts out of view. content-visibility: auto lets the browser skip layout and paint for an element while it is far off screen, and do the work just before it scrolls into view. The content stays in the DOM and the accessibility tree, and find-in-page still reaches it. contain-intrinsic-size gives skipped sections a placeholder size so the scrollbar does not jump; the auto keyword makes the browser remember the real size once it has rendered the section.

@layer layout { main > section { content-visibility: auto; contain-intrinsic-size: auto 40rem; } }

content-visibility is Baseline newly available, and a browser without it simply renders everything, as before. It pays off on long pages of independent sections, not on short ones.

Layout shift

The most hostile failure is content that moves after the reader starts using it: an image arrives, pushes a button down, and the tap lands elsewhere. Core Web Vitals measure it as Cumulative Layout Shift. CSS prevents most of it by reserving space: aspect-ratio, or width and height attributes in the HTML, for images and video; min-block-size for regions filled late; and translate rather than geometry for anything that moves. Web fonts that swap in with different metrics are the other big cause; loading fonts belongs to the Fonts topic (coming).

4. Internationalization

Most style sheets carry English assumptions: text runs left to right, words are as long as their English versions, quotation marks look like these ones. Arabic, Hebrew, Persian and Urdu run right to left. Japanese, Chinese and Mongolian can be set vertically. German often runs a third longer than English, and browser translation can rewrite any page after it loads.

Logical properties as the default

Basics introduced the logical directions: inline is the direction lines run, block is the direction they stack, and each has a start and an end. Logical properties are named for those directions, so they follow the writing mode and the direction of the content instead of the screen:

PhysicalLogical
margin-left, margin-rightmargin-inline-start, margin-inline-end, or margin-inline for both
padding-top, padding-bottompadding-block-start, padding-block-end, or padding-block
border-leftborder-inline-start
width, max-width, heightinline-size, max-inline-size, block-size
top, leftinset-block-start, inset-inline-start
text-align: lefttext-align: start

Logical properties are Baseline widely available and cost nothing to adopt from the start; converting a physical style sheet when an Arabic edition arrives costs a great deal. Keep physical properties for things that are truly physical, such as a shadow cast by light from above.

Direction and writing mode

Direction belongs in the HTML, because it is a property of the content. The dir attribute sets it for a document or a passage:

<html lang="ar" dir="rtl"> … <p>The word for book is <span lang="ar" dir="rtl">كتاب</span>.</p>

With dir="rtl", every logical property flips with no further work. The few things that must mirror explicitly, such as a "forward" arrow icon, can use the :dir() pseudo-class, which matches the direction an element actually has.

Vertical scripts change the axes themselves. With writing-mode: vertical-rl, lines run top to bottom and stack right to left. Logical properties keep their meaning; physical ones now point the wrong way.

@layer components { button > svg:dir(rtl) { scale: -1 1; } figure:lang(ja) > blockquote { writing-mode: vertical-rl; text-orientation: mixed; max-block-size: 30rem; } }

Language-aware typography: :lang(), quotes and hyphens

The :lang() pseudo-class matches an element by the language it inherits from lang, so CSS can adjust typography per language without a class. Quotation marks are the classic case. The q element generates its marks from the quotes property; its initial value, auto, asks the browser to choose marks suited to the language, and :lang() sets them explicitly where a design needs control or a browser's choice falls short:

@layer base { :lang(de) { quotes: "\201E" "\201C" "\201A" "\2018"; } :lang(fr) { quotes: "\00AB\00A0" "\00A0\00BB"; } :lang(ja) { quotes: "\300C" "\300D" "\300E" "\300F"; } article p { hyphens: auto; } }

German gets low opening quotes, French gets guillemets with no-break spaces, and Japanese gets corner brackets, all from the same <q> markup. hyphens: auto breaks long words at syllable boundaries, which keeps narrow columns of German from leaving ragged gaps. It works only when the content declares its language, because the browser must pick a hyphenation dictionary, and not every language has one. One more reason every page starts with <html lang="…">.

Room for longer words

A "Save" button becomes "Speichern" in German and "保存" in Chinese; a box sized for the English word overflows in one and gapes in the other. The cure is the same as for zoom: min-inline-size instead of fixed widths, ch and em for measures tied to text, and flexbox or grid to absorb the variation. A pseudo-translation that stretches every string by a third catches most failures early.

Type is where many of these needs meet. The Fonts section's Type for people shows what joined scripts, tall marks and Han unification ask of fonts and CSS, with a sampler of eight scripts.

5. Developer experience

The audience most often forgotten is the maintainer. CSS is read far more often than it is written, and a style sheet that frightens its readers gets worse over time: afraid to change a rule, they add a more specific one on top, and eventually nobody understands the whole.

Four qualities make CSS pleasant to maintain:

  • Readable. A person can tell what a rule does by reading it. Selectors name things the way the HTML does, and token names say what a value is for, such as --space or --measure, not what it happens to be.
  • Local. A person can find the rules that affect an element, and a change to one component does not leak into another.
  • Predictable. A person can tell which rule wins without trial and error: specificity stays low, layers are declared in one place, and !important is rare.
  • Small. Dead rules are deleted. Less CSS is easier to read, faster to load, and has fewer ways to surprise anyone.

Every habit in this section's house style serves that reader. Compare a selector that describes a path through the DOM with one that describes a thing:

#header .nav-container .nav-list .nav-item.is-active .nav-link > .nav-icon { fill: red !important; } @layer reset, base, layout, components, utilities; @layer base { :where(h1, h2, h3) { line-height: 1.2; text-wrap: balance; } } @layer components { nav { a[aria-current="page"] { font-weight: 700; } svg { fill: currentColor; } } }

The first rule breaks whenever the markup changes and can only be overridden by something more specific. The second set states the cascade's order in its first line, gives shared rules zero specificity with :where(), and finds the current page's link by the attribute that already tells assistive technology which page it is. The cascade page explains layers and :where() in full, and the selectors page explains specificity.

6. Frameworks and the platform

Faced with all of this, many teams reach for a CSS framework to make the complexity go away. Each major framework solved a real problem of its time, and deserves a fair account.

Bootstrap: consistency when browsers disagreed

Bootstrap came out of Twitter in 2011, when browsers differed in their defaults, layout meant floats, and every team rebuilt the same grid, buttons and forms. It offered a normalized base, a twelve-column responsive grid, and ready-made components such as btn btn-primary. Teams got consistent, responsive interfaces in days, and a generation of sites looked alike. Later versions followed the platform: Bootstrap 4 moved its grid to flexbox, and Bootstrap 5 dropped jQuery and exposed its values as custom properties.

Tailwind: naming nothing

Tailwind, first released in 2017, took the opposite approach. Instead of named components it offers small single-purpose utility classes, and an element's look is composed in the markup:

<button class="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 focus-visible:ring-2">Save</button> <button class="btn btn-primary">Save</button>

The case for it is real. Naming is hard, and many one-off styles have no meaningful name. Utilities skip naming, keep styling beside the element, leave no dead CSS when markup is deleted, and keep the output small because only used classes are emitted. Specificity fights mostly disappear. Its current version is itself built on modern CSS, including cascade layers and custom properties.

The costs are real too. A maintainer must combine ten classes in their head to see the button, and the intent, "the primary action", appears nowhere. The HTML gets heavier, and styling is tied to a build tool and a vocabulary learned on top of CSS. A team that knows px-4 but not padding-inline is stuck the moment it needs something the framework lacks.

ApproachOptimizes forCostsFits best
Utility classesWriting speed, no naming, small output, local editsReading effort, heavier HTML, intent hidden, a tool to learnPrototypes, and teams fluent in the vocabulary
Named componentsReading speed, visible intent, a coherent design systemNaming effort, cascade discipline, CSS to pruneLong-lived code maintained by many people
Structure-first platform CSS (this course)Semantic HTML, no build step, low specificity, longevityRequires knowing CSS well; good markup is a prerequisiteContent sites, and anywhere the HTML is in your control

What native CSS now does

Much of what frameworks and preprocessors such as Sass were adopted for is now part of the language, and Baseline widely available:

The needThe framework or preprocessor answerThe platform answer
Consistent starting stylesNormalize, Reboot, PreflightConsistent user-agent styles and a small reset layer
Design tokensSass variables, a theme configurationCustom properties, which also change at run time and per element
NestingSass and LessNative nesting
Controlling who winsNaming conventions, single-class utilities, !important@layer and :where()
A grid systemTwelve-column classesGrid and subgrid
Components that adapt to their spaceBreakpoint classes such as md:Container queries
Color variationsSass darken(), fixed palettescolor-mix() and the color functions

The structure-first equivalent of the two buttons above needs no class at all, because the HTML already says which button is the primary action of a form:

@layer components { button { display: inline-flex; align-items: center; gap: 0.5em; padding: 0.5em 1em; border: 1px solid transparent; border-radius: var(--radius); font: inherit; } form button:where([type="submit"], :not([type])) { background: var(--accent); color: var(--on-accent); } }

The course's position

This course is platform first. That is not a judgment that frameworks are bad; you will use them in jobs. It is a judgment about order. Abstractions do not remove complexity, they move it into a build step, a vocabulary or a dependency, and when the abstraction leaks only someone who knows the layer underneath can fix it. Learn the platform first and a framework becomes a convenience you can evaluate.

Longevity points the same way. Frameworks change conventions every few years, while CSS grows by addition: plain, semantic CSS written in 2010 still works in 2026. And the defaults, preference queries, logical properties and :focus-visible exist because standards authors argued on users' behalf. Work with those decisions and your pages inherit them.

7. Summary

ConceptKey points
A humanistic mediumCSS decides whether people can perceive and use a page. Its audiences include disabled readers, slow connections, other languages and future maintainers. Start from the platform's defaults and avoid breaking them.
FocusNever remove outlines without a replacement. Style :focus-visible with an outline in relative units, with at least 3:1 contrast.
Contrast and colorWCAG AA: 4.5:1 for normal text, 3:1 for large text and interface parts. Never let color alone carry meaning. Answer prefers-contrast.
MotionMake stillness the baseline and add motion inside prefers-reduced-motion: no-preference.
Zoom and reflowRelative units, fluid tracks and minimum sizes satisfy resize text, reflow at 400 percent zoom, and text spacing. No fixed heights around text.
Forced colorsColors are replaced and shadows dropped. Use transparent borders and outlines, and system colors where needed.
Hiding contentdisplay: none hides from everyone; the visually hidden pattern hides only from the eye; opacity: 0 alone leaves a focusable ghost.
Source and visual orderKeyboard and screen readers follow the source. Put HTML in reading order and let grid place it; do not reorder against meaning.
PerformanceCSS blocks first paint. Ship less of it, cache it, inline critical CSS only when it pays. Selector cost rarely matters; animate transform and opacity, not geometry; use content-visibility on long pages; reserve space to prevent layout shift.
InternationalizationLogical properties by default; direction in HTML with dir; writing-mode for vertical scripts; :lang() for quotes; hyphens needs lang; leave room for longer words.
Developer experienceReadable, local, predictable and small. Declare layers once, keep specificity low with :where(), and delete dead rules.
FrameworksBootstrap gave consistency and a grid; Tailwind removes naming at a cost to reading. Custom properties, layers, nesting and container queries now cover much of their ground. Learn the platform first.