Skip to main content

Foundations

Foundations are the accessibility rules that aren't tied to a single component — utilities used across patterns (like sr-only), page-level structure (landmarks, headings, page titles), and visual fundamentals (text contrast, focus indicators). The rules below are the cross-cutting requirements every UI implementation should meet.

Rule: Offscreen Text Utility (sr-only)​

Scopecomponent

Must Haves​

  • Where a component's rules dictate the use of "offscreen text", then the snippet below must be included as a CSS class: .sr-only.
  • Offscreen text may be used as an alternative to aria-labelledby or aria-label.

Donts​

  • Do not hide offscreen text using display: none or visibility: hidden when it is needed for an accessible name.
  • Do not add aria-label or aria-labelledby to an element whose accessible name is carried by offscreen text. Both override the element's text content, so the offscreen text is never used.
  • Do not leave focusable content inside a visually hidden container that stays hidden while focused. A keyboard user reaches it with nothing visible on screen, and the focus indicator appears to vanish.
    • A visually hidden control that returns to the visible layout on focus is the acceptable case, such as a skip link. Pair .sr-only with the :focus restoration below.

Snippets​

CSS
.sr-only {
clip: rect(1px,1px,1px,1px);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}

For a visually hidden element that must become visible when it receives focus, such as a skip link. :focus-within covers the case where the focusable element is a descendant of the hidden container:

CSS
.sr-only-focusable:focus,
.sr-only-focusable:focus-within {
clip: auto;
height: auto;
overflow: visible;
position: static;
white-space: normal;
width: auto;
}

Rule: Page Title​

Scopescreen

Must Haves​

  • Each page (route/view/document) sets a descriptive page title that reflects the central topic of the current page.
  • The page title begins with the name of the current page and is followed by the name of the site.
    • Page name and site name should be separated using a clear visual character, such as a hyphen, emdash, or vertical pipe.
    • Page name should be similar to the <h1> text on the page.

Donts​

  • Do not leave the page title as a generic placeholder across routes.

Rule: Landmarks​

Scopescreenlayout

Must Haves​

  • When site or app navigation is present on a page (route/view/document), this is contained inside a <nav> (or role="navigation") landmark, which is contained inside a <header> (or role="banner") landmark.
    • If a single set of navigation is present, then it should be labeled (e.g., <nav aria-label="primary">).
    • If more than one set of navigation is present - e.g., primary and secondary or breadcrumbs - then each navigation landmark must be labeled (e.g., <nav aria-label="primary">, <nav aria-label="secondary">, <nav aria-label="breadcrumbs">).
  • When a footer is present - i.e., a section at the bottom of the page with information relevant to the entire site, such as a sitemap or navigation links - this is contained inside a <footer> (or role="contentinfo") landmark
  • A <main> (or role="main") landmark must always be present.
    • The main landmark contains the dominant content of the page, which directly relates to or expands upon the central topic of the page, or the central functionality of an application.
    • If site or app navigation is present at the top of the page or view (i.e., header), and/or a footer (footer) is present at the bottom of the page, the main landmark should wrap all content between header and footer content
    • The main landmark must be a sibling of the header and/or footer containers.
  • If the page contains complementary content - i.e., content that is only indirectly related to the page's main content - this is contained inside an <aside> (or role="complementary") landmark
    • The aside landmark is infrequently used and does not need to be present.
    • If present, the aside landmark should be a sibling to the main landmark, and to the header and/or footer, if present.

Donts​

  • Do not use multiple main landmarks on the same page.
  • Do not wrap the header, footer, or aside inside the main landmark, or vice-versa: these should all be siblings.

Rule: Heading Structure​

Scopescreenlayout

Must Haves​

  • Each page (route/view/document) has one level 1 heading, or <h1>, that identifies the main topic of the page.
  • Headings are nested in a logical order according to the relationships of the content they introduce.
    • An <h2> introduces a major section of the page.
    • An <h3> introduces a subsection of the preceding <h2>, and so on.
    • When closing a subsection and returning to a higher-level section, it is acceptable to return to a higher heading level.
  • If text visually functions as a heading, it must be marked up as a heading element rather than only styled to look like one.
  • If footer links are grouped under visible group titles (e.g., "Company", "Support", "Legal"), each group title is a level 2 heading (<h2>).
  • Headings are descriptive of the content they introduce.

Customizable​

  • Section headings use native HTML heading elements (<h1> through <h6>) to communicate the structure of the page.
    • If native elements are unavailable, headings can be implemented with role="heading" and aria-level.

Donts​

  • Do not use heading elements only to make text look larger or bolder.
  • Do not skip heading levels when moving deeper into the hierarchy, such as going from <h2> directly to <h4>, unless closing a subsection and returning to a higher-level section.
  • Do not use non-heading elements as visual-only section titles when they function as headings.
  • Do not render an empty heading element.

Rule: Text Contrast​

Scopecomponent

Must Haves​

  • Text and images of text must have sufficient contrast against their background.
    • Normal-size text must meet a contrast ratio of at least 4.5:1.
    • Large-scale text (18pt regular, or 14pt bold) must meet a contrast ratio of at least 3:1.
  • Text placed over a background image also has a background-color behind it, so it stays legible when Windows High Contrast Mode drops the image. See global.forced-colors.
  • forced-color-adjust: none is not applied to text or its container, which would pin the authored text colors and prevent the user's palette from applying.

Donts​

  • Do not use color combinations for text that fall below 4.5:1 for normal text or 3:1 for large text.
  • Do not rely on a background image alone to establish the contrast behind text.

Rule: Non-text Contrast​

Scopecomponent

Must Haves​

  • Visual information needed to identify a user interface component - such as control boundaries - must have a contrast ratio of at least 3:1 against adjacent colors.
    • In other words, a component's border or background color must contrast at least 3:1 against the page's background color.
  • Meaningful graphical objects must have a contrast ratio of at least 3:1 against adjacent colors when their appearance is needed to understand the content.
    • This includes meaningful icons, simple charts, data marks, indicators, and other non-decorative graphics.
  • A boundary carried by background-color alone is restated under @media (forced-colors: active), since Windows High Contrast Mode replaces author backgrounds and the boundary disappears rather than merely losing contrast. See global.forced-colors.

Donts​

  • Do not use very low-contrast borders, outlines, or icons when they are necessary to identify a control or its current state.
  • Do not verify contrast only in the authored palette. A boundary that passes 3:1 can still vanish entirely under forced colors.

Snippets​

A boundary painted as a background fill needs a real border under forced colors, since the fill and its surroundings collapse to the same system color:

CSS
@media (forced-colors: active) {
.control-boundary {
border: 1px solid CanvasText;
}
}

Rule: Use of Color​

Scopecomponent

Must Haves​

  • Do not use color as the only visual means of conveying information, indicating an action, prompting a response, or distinguishing a visual element.
  • When a component renders a meaningful state visually (e.g., selected, active, current, invalid, pressed), that state is distinguishable by something in addition to color, such as an icon, checkmark, shape, underline, or text.
    • This is the visual counterpart to exposing the state programmatically. A component may satisfy aria-selected and still leave a sighted user unable to tell which item is selected, when selection is shown by a background tint alone.
  • The additional cue is an icon, a shape, a border, or text rather than a second color treatment, so that it survives Windows High Contrast Mode. See global.forced-colors.
    • Forced colors is the reason this rule has teeth beyond grayscale: a state shown by a background tint does not merely lose contrast there, it is repainted with the system color and disappears.

Donts​

  • Do not signal selection, validity, current-ness, or availability by color alone.
  • Do not satisfy this rule with a second color treatment, such as a darker tint or a colored background swap, which fails under forced colors exactly as the first one does.

Rule: Forced Colors (Windows High Contrast Mode)​

Scopecomponent

Windows High Contrast Mode replaces the author's palette with a small set of user-chosen system colors. It is not a dark theme: background-color, border-color, and color are re-mapped, box-shadow is removed, and some background images are dropped. Anything whose meaning rested on those properties alone disappears.

Must Haves​

  • An element distinguished from its surroundings only by background-color also carries a real border, so its boundary survives when backgrounds are flattened.
    • This covers progress fills against their tracks, selected rows, badges, chips, pills, custom form-control indicators, and any painted state layer.
  • Where the authored colors carry meaning the system palette would erase, pair the styling with a @media (forced-colors: active) block that restates the distinction using CSS system color keywords.
    • The keywords are Canvas, CanvasText, LinkText, VisitedText, ActiveText, ButtonFace, ButtonText, ButtonBorder, Field, FieldText, Highlight, HighlightText, SelectedItem, SelectedItemText, Mark, MarkText, GrayText, AccentColor, and AccentColorText.
  • box-shadow is not the only carrier of a boundary, a state, or an elevation cue that the user needs, since shadows are removed under forced colors.
  • Icons drawn as a CSS background-image are paired with a forced-colors fallback, such as an inline SVG or a text alternative.
    • Inline SVG painted with fill: currentColor or stroke: currentColor inherits the system text color and needs no override.
  • forced-color-adjust: none is applied only to elements whose authored colors are themselves the information, such as a color-picker swatch, a chart series key, or a brand preview.
  • Focus indicators follow global.focus-states, which carries its own required forced-colors override.

Donts​

  • Do not convey state, selection, or a boundary with background-color alone.
  • Do not rely on box-shadow for a border, a ring, or any cue that carries meaning.
  • Do not apply forced-color-adjust: none broadly to preserve a design's ordinary palette. It overrides the colors the user chose and defeats the mode.
  • Do not hard-code a color value inside a @media (forced-colors: active) block. The system keywords follow the user's chosen theme; a literal does not.
  • Do not treat a component as verified because it reads correctly in a dark theme. Forced colors replaces the palette outright and removes shadows.

Snippets​

An element distinguished only by background-color vanishes when the palette is replaced. Restate the distinction with system colors, and give the container a border so its bounds survive:

CSS
@media (forced-colors: active) {
.indicator-track {
border: 1px solid CanvasText;
}

.indicator-fill {
background: Highlight;
}
}

Opting out, for the narrow case where the authored color is the content itself:

CSS
.swatch {
forced-color-adjust: none;
}

Rule: Focus Not Obscured​

Scopelayoutcomponent

Must Haves​

  • When a component receives keyboard focus, the focused element is not entirely hidden by author-created content such as sticky headers, sticky footers, or the component's own overlapping chrome.
  • Components with their own sticky or floating sub-regions (e.g., a frozen header row or column) keep the focused element scrolled or offset into a visible area.

Donts​

  • Do not let a sticky or floating region overlap and fully conceal the element that currently has keyboard focus.

Rule: Focus States​

Scopecomponent

Must Haves​

  • Each keyboard-focusable user interface component must have a visible focus indicator when it receives keyboard focus, and for as long as it retains focus.
  • The focus indicator must have a contrast ratio of at least 3:1 against adjacent colors.

Customizable​

  • The focus indicator may take different forms, such as an outline, border, background change, underline, or other visible treatment, provided that it clearly indicates which element currently has keyboard focus.
  • Prefer styling keyboard focus with :focus-visible so the indicator is shown when users navigate by keyboard, not on mouse click.
  • Strongly recommended: a two-layer focus style — a 2px solid ring floated 2px outside the element with a white box-shadow behind it — so the indicator remains visible against any surrounding surface (light, dark, image, or gradient). See the first snippet below.
  • A simple solid outline offset from the element remains acceptable when the surrounding surfaces are known and the ring's 3:1 contrast against them is verified.
    • Starting pattern: outline: 2px solid ...
    • Starting offset: outline-offset: 2px (1px may be used where spacing is tighter).
  • Windows High Contrast Mode (forced-colors) support is required for any focus style. Pair the primary style with a @media (forced-colors: active) override that uses the Highlight CSS system color and drops the box-shadow. See the shared override snippet below.

Donts​

  • Do not use a focus indicator whose color blends into adjacent colors below the required 3:1 contrast ratio.
  • Do not make the focus indicator so subtle that users cannot quickly identify which element currently has focus.
  • Do not rely on hover-only styles as the only visible indicator of focus.
  • Do not ship focus styles without a forced-colors override — Windows High Contrast Mode users will lose the custom colors and be left with browser defaults that may not match the design's contrast intent.

Snippets​

Strongly recommended two-layer focus ring. The box-shadow extends 4px in white around the element; the outline floats 2px outside the element edge, so the white halo sits between the element and the colored ring for surface-independent visibility:

CSS
:focus-visible {
outline: 2px solid var(--focus-ring-color, #1a73e8);
outline-offset: 2px;
box-shadow: 0 0 0 4px #fff;
}

Acceptable simpler alternative when the surrounding surfaces are known and contrast is verified:

CSS
:focus-visible {
outline: 2px solid var(--focus-ring-color, #1a73e8);
outline-offset: 2px;
}

Required forced-colors override — pair with either primary style above. Highlight maps to the OS-defined focus/selection color under Windows High Contrast Mode; box-shadow: none prevents doubled visuals under the system-managed palette:

CSS
@media (forced-colors: active) {
:focus-visible {
outline: 2px solid Highlight;
outline-offset: 2px;
box-shadow: none;
}
}

Rule: Motion and Unmounting​

Scopecomponent

Animation and content removal belong to one rule because they fail together. An element animating out is still in the DOM while it is visually gone, and that window is where keyboard focus is lost and the accessibility tree stops matching the screen.

Must Haves​

  • Under prefers-reduced-motion: reduce, no animation repeats, travels an element across the screen, or scales it.
    • Changes to color, opacity, or blur that do not alter an element's perceived size, shape, or position are not motion animation and may continue.
    • Movement driven by a value the user is waiting on, such as a progress fill advancing, is not decorative motion and continues.
  • Animation that starts automatically, lasts more than 5 seconds, and runs alongside other content provides a mechanism to pause, stop, or hide it, unless the movement is essential to an activity in progress.
  • Motion is not the only means of conveying that something changed. A change communicated by movement is also communicated by text, by a change in the accessibility tree, or by a live region.
  • Before an element holding keyboard focus is removed from the DOM or made unfocusable, focus is moved to a stable element that remains present, such as the control that opened it or the container that replaces it.
  • aria-hidden="true" is not applied to a focusable element, nor to any ancestor of one.
  • An element being animated out leaves the accessibility tree when it stops being available to the user, not when its exit animation finishes.

Donts​

  • Do not remove or disable an element that holds focus without first moving focus somewhere stable. Focus falls to the document body and the user loses their place in the page.
  • Do not apply aria-hidden="true" to a container that holds a focusable control. A keyboard user reaches a control that assistive technology cannot report.
  • Do not rely on an exit animation to communicate that something was removed.
  • Do not treat prefers-reduced-motion: reduce as a request to remove the element. It asks for less movement, not less information.
  • Do not hold content out of the accessibility tree for the duration of an entrance animation. It is available to a screen reader as soon as it is available at all.

Snippets​

Disable decorative motion under the reduced-motion preference. Scope it to the elements that animate decoratively rather than applying it to everything, so movement the user is waiting on still runs:

CSS
@media (prefers-reduced-motion: reduce) {
.marquee,
.spinner,
.slide-in {
animation: none;
transition: none;
}
}

Reading the preference in script, for components that need to render differently rather than merely stop animating:

JS
const query = window.matchMedia("(prefers-reduced-motion: reduce)");
const apply = () => setReduceMotion(query.matches);
apply();
query.addEventListener("change", apply);

Moving focus before removal, deferred so the replacement is focusable by the time the call lands:

JS
requestAnimationFrame(() => returnTarget.current?.focus());

Rule: Icons​

Scopecomponent

An icon is meaningful when the user needs it to understand or operate the interface and no text beside it carries the same meaning. Everything else is decorative, which is most icons. The two are marked up in opposite ways, so answer this first.

Must Haves​

  • Hide a decorative icon from assistive technology. Inline <svg> and icon-font elements take aria-hidden="true"; an <img> takes an empty alt="".
  • Expose a meaningful icon one of two ways: name the graphic, or supply its meaning as text.
    • To name it, give the graphic role="img" and aria-label or aria-labelledby. ARIA requires a label on role="img".
    • To supply text, place the equivalent text beside the icon, visually hidden when it should not be seen (see global.sr-only), and hide the icon.
  • For an icon-only control, put the accessible name on the <button> or <a> and leave the graphic decorative.
  • Name what the control does or what the icon means, not what it depicts (e.g., "Add to my list", not "Plus sign").
  • Controls that differ only by their icon have different accessible names (e.g., "Edit profile" and "Edit cover photo", not "Edit" on both).
  • A meaningful icon carries the non-text contrast requirement in global.non-text-contrast. A decorative one does not.

Donts​

  • Do not name both a control and the icon inside it. The control is announced with its name repeated (e.g., "Delete Delete button").
  • Do not hide an inline <svg> with role="presentation" or role="none". Unlike aria-hidden="true", they drop the element's own role but leave its <title> and text descendants in the accessibility tree.
  • Do not give a decorative graphic a <title> child, which names it.
  • Do not use an emoji or an icon-font glyph as a meaningful icon without a text alternative. Emoji are announced by their Unicode name (a flame reads as "fire" where "trending" was meant), and icon-font glyphs read as private use area characters or as nothing.

Snippets​

Decorative, because the adjacent text already carries the meaning:

JSX
<button type="button">
<span aria-hidden="true">[download-icon]</span>
Download
</button>

Icon-only control. The name lives on the control, and the graphic stays decorative:

JSX
<button type="button" aria-label="Add to my list">
<span aria-hidden="true">[plus-icon]</span>
</button>

Meaningful standalone icon, where no text nearby carries the status. Either route is correct, and the second one hides the graphic because the text has taken over the meaning:

JSX
{/* Name the graphic. */}
<svg role="img" aria-label="Downloaded" width="16" height="16" viewBox="0 0 16 16">
<path d="M2 8l4 4 8-8" fill="none" stroke="currentColor" strokeWidth="2" />
</svg>

{/* Or supply the meaning as text, which makes the graphic decorative. */}
<span>
<svg aria-hidden="true" width="16" height="16" viewBox="0 0 16 16">
<path d="M2 8l4 4 8-8" fill="none" stroke="currentColor" strokeWidth="2" />
</svg>
<span className="sr-only">Downloaded</span>
</span>