
Modern Web Guidance
- 14.4k installs
- 1.7k repo stars
- Updated August 3, 2026
- googlechrome/modern-web-guidance
modern-web-guidance is an agent skill documented in googlechrome/modern-web-guidance.
About
# Accessibility Coding Guidelines This guide provides actionable DOs and DON'Ts for AI coding agents to ensure web applications are accessible to all users, including those using assistive technologies. Keep these principles in mind throughout: - **Accessibility is the minimum, not the ceiling.** Conformance to standards is the floor; aim for genuine usability. - **Patterns are use-case specific.** No checklist replaces real testing - including testing with disabled users - to confirm a given implementation is actually accessible in context. Content Navigability and Structure ### Actionable Guidelines #### DOs - **Place all content within landmarks**: Wrap the page in `<header>`, `<nav>`, `<main>`, `<aside>`, and `<footer>` so assistive-tech users can jump between regions. - **Structure main content with headings**: Use `<h1>` - `<h6>` sequentially (no jumping `<h1>` → `<h4>`) so screen-reader users get a navigable outline. - **Use lists for repeated, contiguous content**: `<ul>`/`<ol>` give assistive tech a count up front and let users skip the entire group.
- Accessibility Coding Guidelines
- **Accessibility is the minimum, not the ceiling.** Conformance to standards is the floor; aim for genuine usability.
- **Place all content within landmarks**: Wrap the page in `<header>`, `<nav>`, `<main>`, `<aside>`, and `<footer>` so ass
- **Structure main content with headings**: Use `<h1>` - `<h6>` sequentially (no jumping `<h1>` → `<h4>`) so screen-reader
- **Use lists for repeated, contiguous content**: `<ul>`/`<ol>` give assistive tech a count up front and let users skip th
Modern Web Guidance by the numbers
- 14,402 all-time installs (skills.sh)
- +764 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #57 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
modern-web-guidance capabilities & compatibility
- Capabilities
- accessibility coding guidelines · **accessibility is the minimum, not the ceiling. · **place all content within landmarks**: wrap the · **structure main content with headings**: use `< · **use lists for repeated, contiguous content**:
- Use cases
- documentation
What modern-web-guidance says it does
Keep these principles in mind throughout: - **Accessibility is the minimum, not the ceiling.** Conformance to standards is the floor; aim for genuine usability.
- **Patterns are use-case specific.** No checklist replaces real testing — including testing with disabled users — to confirm a given implementation is actually accessible in context.
- **Structure main content with headings**: Use `<h1>`–`<h6>` sequentially (no jumping `<h1>` → `<h4>`) so screen-reader users get a navigable outline.
- **Use lists for repeated, contiguous content**: `<ul>`/`<ol>` give assistive tech a count up front and let users skip the entire group.
npx skills add https://github.com/googlechrome/modern-web-guidance --skill modern-web-guidanceAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 14.4k |
|---|---|
| repo stars | ★ 1.7k |
| Security audit | 2 / 3 scanners passed |
| Last updated | August 3, 2026 |
| Repository | googlechrome/modern-web-guidance ↗ |
What problem does modern-web-guidance solve for developers using this skill?
# Accessibility Coding Guidelines This guide provides actionable DOs and DON'Ts for AI coding agents to ensure web applications are accessible to all users, including those using assistive technologie
Who is it for?
Developers who need modern-web-guidance patterns described in the cached skill documentation.
Skip if: Skip when docs are empty or the task is outside the skill's documented scope.
When should I use this skill?
User asks about modern-web-guidance or triggers the skill by name.
What you get
Actionable workflows and conventions from SKILL.md for modern-web-guidance.
- current web pattern guidance
- CWV optimization recommendations
Files
Modern Web Guidance
A skill to search for specific web development use cases and retrieve their corresponding best practice guides.
When to use
Must use this skill:
- At the start of implementing any web feature.
- Before creating a new component, to check if a standardized pattern already exists.
- To avoid implementing ad-hoc solutions or loading large dependencies unnecessarily.
Usage Instructions
Step 1. Search Use Cases
Search with an action-oriented query summarizing what you want to achieve using the search command. Run modern-web-guidance directly with npx.
npx -y modern-web-guidance@latest search "<query>" --skill-version 2026_05_16-c5e7870Example Output:
[
{
"id": "optimize-image-priority",
"description": "Optimize the loading priority of Largest Contentful Paint (LCP) candidate images.",
"category": "performance",
"featuresUsed": [ "Fetch priority" ],
"tokenCount": 985,
"similarity": 0.7289
},
{
"id": "defer-rendering-heavy-content",
"description": "Reduce rendering times in content-heavy web pages by deferring rendering for offscreen content.",
"category": "performance",
"featuresUsed": [ "content-visibility", "hidden=\"until-found\"" ],
"tokenCount": 1250,
"similarity": 0.6961
}
]Note: If search results are vague, return no matches, or show low similarity scores, run the list command to browse all guides:```sh
npx -y modern-web-guidance@latest list
```
---
Step 2. Retrieve Best Practices
Once you have a relevant id from the search results, call this script using the retrieve command to get the full guide. You can pass multiple IDs separated by commas.
npx -y modern-web-guidance@latest retrieve "<id>"Example Output: The markdown content of the guide describing implementation steps...
Using npx
- IMPORTANT: on Windows, using
npxmay fail. Usenpx.cmd ...instead. - Network access is required for fetching npm packages needed by the task.
- If the
npx -y modern-web-guidance…command hangs, you may be offline. Try running again in offline
mode: npx --offline ….
- The
--skill-versionflag is used to determine if this SKILL.md is out of date. If it is, a warning
message is logged to stderr.
Guidelines
- Always search first to find the most relevant guides.
- These guides are usually framework-agnostic; adapt them correctly to your setup.
- Do not hallucinate guides or ignore them; they represent the preferred local standard for the user's project.
Interpreting Browser Support & Fallbacks
- Default Behavior: All guides assume Baseline Widely available features are safe to use without fallbacks. For features that are not Baseline widely available, you MUST follow the fallback recommendations in the guide, unless the user has specified a custom browser support policy.
- **Custom
Accessibility Coding Guidelines
This guide provides actionable DOs and DON'Ts for AI coding agents to ensure web applications are accessible to all users, including those using assistive technologies.
Keep these principles in mind throughout:
- Accessibility is the minimum, not the ceiling. Conformance to standards is the floor; aim for genuine usability.
- Patterns are use-case specific. No checklist replaces real testing — including testing with disabled users — to confirm a given implementation is actually accessible in context.
1. Content Navigability and Structure
Actionable Guidelines
DOs
- Place all content within landmarks: Wrap the page in
<header>,<nav>,<main>,<aside>, and<footer>so assistive-tech users can jump between regions. - Structure main content with headings: Use
<h1>–<h6>sequentially (no jumping<h1>→<h4>) so screen-reader users get a navigable outline. - Use lists for repeated, contiguous content:
<ul>/<ol>give assistive tech a count up front and let users skip the entire group. - Provide skip links prior to repeated content like site headers with navigation or long/infinite lists, so that keyboard users can easily bypass them. Make sure the target is focusable (e.g.
<main id="content" tabindex="-1">). - Semantic Tables: Use
<caption>and<th scope="col">(or<th scope="row">) for data tables.
DON'Ts
- Don't use fake headings: Never style
<div>or<span>to look like headings without standard<h1>–<h6>tags. - Don't place headings inside `<summary>`, and avoid relying on headings inside `<details>` content: Headings inside
<summary>may be hidden from screen-reader heading lists and heading-navigation shortcuts entirely; headings inside<details>content are only reachable via heading navigation when the disclosure is open. - Caveat: If a heading must act as a disclosure trigger, use a more robust alternative to
<details>/<summary>instead, e.g. an accordion or a disclosure implemented with ARIA where the heading wraps the button. - Don't use tables for layout: Use CSS Grid/Flexbox for visual layouts.
- Don't overuse landmarks: Too many landmarks dilute their value. In particular, avoid labeling a
<section>(which turns it into aregionlandmark) —regionshould be a last resort when no other landmark fits.
Code Examples
<!-- Good: Semantic landmarks, heading hierarchy, skip link -->
<header>
<a href="#content" class="skip-link visually-hidden">Skip to content</a>
<nav aria-label="Primary">
<ul>
<li><a href="/">Home</a></li>
</ul>
</nav>
</header>
<main id="content" tabindex="-1">
<h1>Platform Dashboard</h1>
<section>
<h2>User Statistics</h2>
<table>
<caption>Monthly active users</caption>
<tr>
<th scope="col">Month</th>
<th scope="col">Users</th>
</tr>
<tr>
<td>January</td>
<td>12,000</td>
</tr>
</table>
</section>
</main>2. Semantic HTML and ARIA
Actionable Guidelines
DOs
- Prefer HTML elements and attributes to ARIA: A native element comes with the right role and behavior.
<button>already impliesrole="button";requiredalready impliesaria-required. - Match ARIA implementations to actual behavior: If you set
role="tab", the element must behave like a tab — including keyboard interactions. Many ARIA patterns can't be implemented in CSS alone and need JavaScript. - Be deliberate about `disabled` vs `aria-disabled`:
disabledremoves the element from the focus order entirely (andtabindex="0"won't bring it back), which is often wrong for toolbar buttons or links.aria-disabled="true"keeps the element focusable so users can land on it and learn it's disabled.
DON'Ts
- Don't use ARIA when native HTML exists: Avoid
<div role="button">or<a role="button">if<button>works. - Don't add redundant ARIA roles or properties: Avoid
<ul role="list">,<nav role="navigation">, or<input required aria-required="true">. - Caveat: Safari removes list semantics from
<ul>/<ol>outside<nav>whenlist-style: noneordisplay: flex/gridis applied. In that caserole="list"is required to restore them. - Don't assume custom elements have no ARIA: Custom elements can attach ARIA via
ElementInternals, which some automated test tools can't see — so the absence ofrole/aria-*attributes in markup doesn't prove the element has no semantics. Verify with the browser's accessibility-tree inspector.
3. Accessible Names and Descriptions
Every interactive element and some landmarks need an accessible name, and many benefit from an accessible description. Names are short and identify the element; descriptions add context.
Actionable Guidelines
DOs
- Prefer native naming mechanisms:
<label>for form controls,<caption>for<table>,<legend>for<fieldset>,<figcaption>for<figure>. - Explicitly associate `<label>` with its control via `for`/`id`, even when nesting the input inside the label — explicit association improves assistive-tech support.
- Prefer `aria-labelledby` over `aria-label` when a visible label exists: avoids duplication, improves maintainability, and translates better.
- Prefer to reuse the same accessible name for hyperlinks that share an `href`.
- Use visually hidden text to disambiguate controls that look identical visually but do different things (e.g. multiple "Edit" buttons in a list).
DON'Ts
- Don't put `aria-label`/`aria-labelledby` on elements that shouldn't be named — e.g. plain
<div>,<span>, or custom elements without a role. Custom elements may have an implicit role set viaElementInternals, so the absence of aroleattribute isn't conclusive. - Don't reuse an accessible name across controls with different effects in the same view (close buttons for two different open dialogs are fine because only one is reachable at a time; multiple “Edit” buttons for different content is not).
- Don't reuse an accessible name across hyperlinks pointing to different `href`s.
- Don't pack descriptions, error messages, or instructions into the label.
- Don't repeat state already exposed via ARIA (
aria-expanded,aria-checked,aria-selected,aria-pressed) inside the accessible name — it creates redundancy and ambiguity. - Don't include the role name in the label:
<nav aria-label="Primary navigation">reads as "Primary navigation navigation." - Don't use `title` or `placeholder` as a naming mechanism.
- Don't include interactive elements in an `aria-describedby` target unless their text content reads sensibly as a description on its own (e.g. if a link’s text is the same as how it’s labelled elsewhere, it can be included within a description).
Code Example: Visually Hidden Utility
A .visually-hidden utility lets you provide text for screen readers without rendering it visually. It's commonly used for skip links, additional context on icon-only buttons, and supplementary labels.
/* Hides content visually but keeps it in the accessibility tree.
:focus-within / :active opt elements out — useful for skip links and
any focusable content wrapped in this class. */
.visually-hidden:where(:not(:focus-within, :active)) {
position: absolute !important;
clip-path: inset(50%) !important;
overflow: hidden !important;
width: 1px !important;
height: 1px !important;
margin: -1px !important;
padding: 0 !important;
border: 0 !important;
white-space: nowrap !important;
}When the hidden content is focusable (skip links, focus-receiving wrappers), the :focus-within/:active exception lets it become visible. Style the visible state per situation, e.g. a skip link to the main content typically wants fixed positioning at the top-left of the viewport so the rest of the page doesn't shift.
4. Document Metadata and Language
Actionable Guidelines
DOs
- Declare Visual Language: Always set
<html lang="en">(or appropriate code). - Unique Page Titles: Front-load unique context in
<title>(e.g.,Page Topic | Site Name). - Inline Language Switches: Use
lang="..."for block quotes or text in different languages. - IFrame Titles: Always provide a descriptive
title="..."for<iframe>elements. - Update document title on Page Transitions in SPAs: Shift focus to updated titles.
DON'Ts
- Don't Disable iframe Scrolling: Avoid
scrolling="no"(deprecated) oroverflow: hiddenon iframes. Users who zoom in or enlarge text need to scroll to reach content that overflows.
Code Examples
<!-- Good: Distinct title and language declaration -->
<html lang="en">
<head>
<title>Analytics Reports | Guidance Platform</title>
</head>
<body>
<p>The motto is <span lang="la">"Carpe diem"</span>.</p>
<iframe title="Interactive Sales Chart" src="/chart"></iframe>
</body>
</html>5. Keyboard and Focus Management
Actionable Guidelines
DOs
- Logical Tab Order: Ensure tab order matches visual layouts (top-to-bottom).
- Visible Focus Indicators: Always style
:focus-visiblestates explicitly. If disabling defaults, provide overrides with sufficient contrast. - Custom Trigger Keyboards: Attach Enter/Space handlers for custom simulated interactive elements. When implementing a custom keyboard handler for button-like elements,
Entershould be akeydownhandler andSpaceshould be akeyuphandler (matching native<button>behavior whereEnterrepeats andSpacetriggers on release). - Use `tabindex` deliberately: Anything focusable — by keyboard or programmatically — should have an implicit or explicit ARIA role, so don't make every element focusable. When focus is needed, choose
tabindex="0"to add the element to the tab order ortabindex="-1"to make it programmatically focusable only (e.g., a skip-link target). - Manage Toggle States: Utilize
aria-expandedandaria-pressedto communicate toggle states for custom controls.
DON'Ts
- Don't disable outlines without replacements: Avoid
outline: nonewithout styling alternatives. - Don't use Positive Tabindex values: Never use
tabindex="1"or greater. - Don't hide interactive elements from screen readers: Avoid
aria-hidden="true"orrole="presentation"on elements that can receive focus.
Code Examples
/* Good: High contrast focus border */
:where(a:any-link, button):focus-visible {
outline: 3px solid #ff0055;
outline-offset: 3px;
}<!-- Good: Skip to main content -->
<a href="#content" class="skip-link">Skip to main content</a>
<main id="content" tabindex="-1">...</main>// Good: Keyboard handlers for complex custom widgets (e.g., Tree items, tabs).
// NOTE: This pattern applies ONLY to non-standard UI where no native HTML tag exists.
// Always prioritize native <button> or <input> elements for standard interactions.
// Elements MUST have the appropriate ARIA role (e.g., role="treeitem" or role="tab").
customWidget.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
toggleWidgetState();
}
if (e.key === ' ') {
e.preventDefault(); // Prevent page scrolling on Spacebar keydown
}
});
customWidget.addEventListener('keyup', (e) => {
if (e.key === ' ') {
toggleWidgetState();
}
});
function toggleWidgetState() {
// E.g., Manage toggle/expanded states for custom controls
const isExpanded = customWidget.getAttribute('aria-expanded') === 'true';
customWidget.setAttribute('aria-expanded', !isExpanded);
}6. Alternate Text and Media
Actionable Guidelines
DOs
- Informative Visual Descriptions: Describe the purpose of the image (e.g., "Search", not "Magnifying glass").
- Empty Alt properties for decorative visuals: Use
alt=""to remove decorative images from the accessibility tree so they aren't announced. - Synchronous Captions for videos: Supply WebVTT captions for video tracks.
- Transcripts for audio: Provide text transcripts for purely audio podcasts.
- Informative View Descriptions for inline SVGs: Apply
role="img"and a nested<title>tag for informative visuals. - Decorative SVGs removal: Apply
aria-hidden="true"to remove decorative SVGs from reading flows. - Long descriptions for complex images: Use
<figure>/<figcaption>oraria-describedbyfor charts and infographics. - Provide data tables as alternatives: Consider providing semantic data tables as accessible alternatives for charts and other complex data visualizations.
DON'Ts
- Don't use clichéd prefixes: Avoid "Image of..." or "Picture of...".
- Don't use underscores in filenames: Use dashes if the filename might be announced as fallback.
Code Examples
<!-- Decorative -->
<img src="divider.png" alt="">
<!-- Inline Decorative SVG (remove from tab flow) -->
<svg aria-hidden="true" viewBox="0 0 24 24">
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>
</svg>
<!-- Informative (Functional) -->
<a href="/search">
<img src="glass.png" alt="Search the platform">
</a>
<!-- Video with Captions tracks -->
<video controls>
<source src="intro.mp4" type="video/mp4">
<track src="caps.vtt" kind="captions" srclang="en" label="English">
</video>
<!-- Complex graph with figcaption -->
<figure>
<img src="chart.png" alt="Sales growth graph 2024.">
<figcaption>Sales grew 20% in Q3 due to new platform launch.</figcaption>
</figure>
<!-- Audio with expandable transcript details -->
<audio controls src="podcast.mp3" aria-details="podcast-transcript"></audio>
<details id="podcast-transcript">
<summary>View Transcript</summary>
<div class="transcript-content">
Welcome to the show...
</div>
</details>Content Visibility Decision Matrix
| Intent | Visual | Screen Reader | Focusable | Structural Pattern |
|---|---|---|---|---|
| Visible to all | Yes | Yes | Yes | Standard rendering |
| Screen Reader only | No | Yes | Yes (if interactive) | Visually hidden utility (e.g. .visually-hidden) |
| Visual only | Yes | No | No | aria-hidden="true" / role="presentation" |
| Hidden for all | No | No | No | hidden attribute / display: none |
Heuristic Rule: If an element can receive keyboard focus, it must not be hidden via aria-hidden="true".
7. Forms and Input Controls
Actionable Guidelines
DOs
- Connect Labels Programmatically: Use
<label for="id">linked to<input id="id">. - Use Autocomplete: Set valid standard
autocompleteoptions (e.g.,"email"or"given-name") for user profiles. - Link hints to inputs via `aria-describedby`: Associate help text with inputs, and place the hint above the input so autocomplete popovers don't cover it during editing.
- Announce dynamic errors via live regions: Use
aria-liveor shift focus to error lists. - Provide form validation constraints: Use
required(oraria-required="true"only whenrequiredisn't applicable) to signal mandatory inputs.
DON'Ts
- Don't use placeholders as labels: Placeholders are not persistent labels.
- Don't trigger context shifts on focus changes: Avoid auto-submitting forms or jumping pages on focus change events alone.
Code Examples
<!-- Good: Semantic forms with hints for passwords -->
<form>
<label for="pwd">Password:</label>
<span id="pwd-hint">Must contain at least 8 characters.</span>
<input id="pwd" type="password" aria-describedby="pwd-hint" autocomplete="current-password" required>
</form>8. Live Regions
Live regions let assistive tech announce content updates that aren't tied to navigation or focus changes. They're easy to misuse — too many regions, or noisy ones, quickly become spam for screen-reader users.
Live Region Urgency Table
| Urgency | Visual Analogue | aria-live Value | Behavioral Impact | Example |
|---|---|---|---|---|
| Critical | Modal / Alert | assertive (or role="alert") | Interrupts immediately, clears speech queue | Session timeout, API failure |
| Standard | Toast / Banner | polite | Announces at next graceful break | Search results, "Saved" status |
| Passive | Silent text | off | Only if user navigates to it | Live character count |
Heuristic Rule: Use assertive only for critical, time-sensitive updates that require immediate attention or prevent safe continuation (e.g., data loss, session timeouts, or network drops).
Actionable Guidelines
DOs
- Centralize live regions for non-visible announcements: A single
politeregion and a singleassertiveregion per page (with whateveraria-atomicconfiguration you need) keeps announcements consistent and easier to maintain. Many frameworks ship their own announcer abstraction — use it. - Debounce frequently-changing regions: If a region can update many times per second (e.g. a combobox's result count as the user types), debounce so users aren't spammed.
- Delay slightly when other announcements may collide: When the user is typing or focus is being managed, a small delay before announcing keeps live-region updates from overlapping other speech.
DON'Ts
- Don't use live regions for interstitial states like "Loading…" or "Updating…" unless they're meaningfully informative — they usually just create noise.
- Don't add live-region updates to inert DOM: When dialogs open or sections become
inert, queued or debounced messages can end up unannounced — or announced from DOM the user can't reach. Coordinate live-region updates with dialog/inert state changes.
Code Example
<!-- Session Timeout Warning with controls -->
<div role="alert" class="timeout-warning">
Your session will expire in 2 minutes.
<button type="button" onclick="extendSession()">Extend Session</button>
</div>9. Color, Contrast, and Typography
Actionable Guidelines
DOs
- Minimum contrast standards: Maintain 4.5:1 for normal text and 3:1 for large text or icons.
- Ensure non-text contrast standards: Maintain a minimum contrast ratio of 3:1 for user interface component boundaries and states.
- This includes visual elements (borders, backgrounds, box-shadows, underlines) that form the boundary or indicate the presence of a UI component (e.g., input field borders).
- This also includes visual elements indicating active states within a component (e.g., checkbox checkmarks or switch thumbs).
- Caveat: Meeting 3:1 non-text contrast can challenge minimalistic designs. Soft gradients or subtle inset/outset shadows can soften visual boundaries while satisfying accessibility requirements.
- Use multiple state indicators: Do not denote success/errors ONLY with color. Use icons or text.
- Relative font size units: Use
remoremfor font sizes instead ofpx. - Consistent or Start alignment: Avoid
justifyalignment as it can be more difficult to read. - Avoid long lines of text: Cap paragraph blocks to a maximum of 80 characters width.
- Support user zoom preferences: Allow users to resize text up to 200% without loss of content or functionality.
- Support light and dark color schemes: Honor
@media (prefers-color-scheme: dark)and pair it with thecolor-schemeCSS property so form controls, scrollbars, and other UA-rendered surfaces match. - Use `prefers-contrast` only when warranted: Reach for
@media (prefers-contrast: more)when the design uses low-contrast accents (e.g., subtle borders, muted secondary text) that need to be reinforced; most sites that already meet baseline contrast won't need it.
DON'Ts
- Don't use color alone to indicate the presence of a user interface component or its state: Use iconography and/or shape to help differentiate.
- Don't use Justified Text Alignment: Avoid
text-align: justify. - Don't use Ornate fonts: Omit cursive typefaces for main reading content.
- Don't rely on all-caps for emphasis: Prefer bolding for visual emphasis, and use
<em>/<strong>when the emphasis is semantic. - Limit emphasis overall: Emphasis loses meaning when it's everywhere — apply it only where it changes how the content should be read.
Code Examples
/* Good: Relative sizing and line caps */
body {
line-height: 1.5;
text-align: start; /* Supports LTR and RTL */
}
article {
max-width: 80ch; /* Caps line length to ~80 characters for readability */
}<!-- Good: Denotes state without colors alone -->
<div class="error-msg">
<span aria-hidden="true">❌</span>
<span>The password entered was invalid.</span>
</div>/* Dark Mode support variables */
:root {
--bg-color: #ffffff;
--text-color: #212529;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #121212;
--text-color: #f8f9fa;
}
}10. Motions and Preferences
Actionable Guidelines
DOs
- Support Reduced Motion media queries: Support
@media (prefers-reduced-motion: reduce)media queries. - Provide Pause mechanism: Allow users to stop auto-running carousels banners or other persistent animations.
- Default to static views: Consider defaulting to static states and allowing users to opt-in to motion.
DON'Ts
- Don't exceed flash limits (three per second): Never include rapid light-to-dark flashing. Such effects can cause seizures.
Code Examples
/* Good: Dampen spin states for reduced motion queries */
@media (prefers-reduced-motion: reduce) {
.spinner {
animation: none;
opacity: 0.5;
}
}11. Modals and Native Dialogs
Modern browsers provide native solutions for creating modal dialogs which avoid the need for focus traps, managing the accessibility of outside content, ensuring the content is on top, and dimming the background content — all of which can be error prone and require heavy JavaScript event tracking to maintain.
Actionable Guidelines
DOs
- Use the Native `<dialog>` Element: Invoke the dialog using the
.showModal()method to open it in a modal state. When in a modal state, the browser sets outside content as inert (i.e. the outside content is hidden from the accessibility tree and cannot be interacted with nor be focused). - Use the `inert` Attribute for Custom Overlays: When
<dialog>cannot be used (e.g., some non-modal overlays, framework constraints, or layouts where<dialog>'s top-layer/positioning behavior conflicts with the design), applyinertto outside content to ensure it cannot be interacted with by keyboard, pointer, or assistive technology. This requires structuring elements in such a way that the custom overlay is not a descendant of the element withinertset on it.
DON'Ts
- Don't implement focus traps for native modal dialogs: When a
<dialog>element is opened in a modal state, browsers set outside content as inert which is sufficient for ensuring only the dialog’s content can be focused.
Code Examples
HTML & JS: Native `<dialog>` with standard close events
<!-- Dialog opens natively with showModal() and locks focus -->
<button id="open-btn">Open Dialog</button>
<dialog id="accessible-modal" aria-labelledby="title-id">
<h2 id="title-id">Account Settings</h2>
<p>Update your details here.</p>
<button onclick="this.closest('dialog').close()">Close Dialog</button>
</dialog>
<script>
document.getElementById('open-btn').addEventListener('click', () => {
document.getElementById('accessible-modal').showModal();
});
</script>12. Testing Validations
Actionable Guidelines
DOs
- Run Automated checks via axe-core or Lighthouse audits: Catch missing alt texts or low contrasts (e.g., via Lighthouse in Chrome DevTools MCP).
- Validate Sequential Navigations using keyboards alone: Using only keyboard shortcuts, such as Tab/Shift+Tab, arrow keys, Enter, Space, and Esc, confirm every interactive element is reachable and operable, and that focus never gets stuck.
- Test on Screen Readers with calibrated browsers: Rely on standard bindings (e.g., JAWS with Chrome, NVDA with Firefox, Narrator with Edge, VoiceOver with Safari on macOS and iOS, TalkBack with Chrome for Android).
DON'Ts
- Don't rely purely on scores: A 100% score does not guarantee real usability.
Accessible Error Announcement
The Problem
Standard HTML5 validation provides visual feedback (via :invalid or :user-invalid), but it doesn't automatically synchronize with accessibility attributes like aria-invalid.
If you use standard :invalid styling, screen readers might announce "Invalid entry" the moment a user tabs into a required field that is currently empty. This creates a disruptive experience for users using assistive technologies, as the error is announced before interaction has occurred.
The Solution
We want the programmatic state (aria-invalid="true") to be applied at the exact same moment the visual state (:user-invalid) applies. Since :user-invalid relies on the browser's internal "user-interacted" flag, we can use JavaScript to check that this selector matches during standard interaction events.
See MDN aria-invalid for more details.
Implementation Strategy
1. Visual Layer: Use CSS :user-invalid to show borders/icons. 2. Accessibility Layer: Use aria-invalid and aria-errormessage to communicate state to Assistive Technology (AT). 3. Bridge Visual & Accessibility Layer: Create a lightweight JavaScript utility that listens for blur and input events, checks if the element matches :user-invalid, and updates the ARIA attributes accordingly.
Implementation Guide
1. HTML Structure
Link your input to its error message using aria-errormessage (or aria-describedby for broader support).
<form>
<div class="field">
<label for="email">Email</label>
<input
type="email"
id="email"
required
aria-errormessage="email-error"
>
<span id="email-error" class="error-msg">
Please enter a valid email address.
</span>
</div>
</form>2. CSS
Control the visibility of the error message using the native pseudo-class :user-invalid.
.error-msg {
display: none;
color: #d93025;
}
/* Show error message when input is user-invalid */
input:user-invalid ~ .error-msg {
display: block;
}
/* Optional: Visual cues on the input itself */
input:user-invalid {
border-color: #d93025;
}3. JavaScript
Since there is no "UserInvalidChanged" event, hook into standard form events to check the state.
const updateAriaState = (event) => {
const input = event.target;
if (!input.matches?.('input, textarea, select')) return;
// Check if the browser currently considers this input "user-invalid"
const isUserInvalid = input.matches(':user-invalid');
if (isUserInvalid) {
input.setAttribute('aria-invalid', 'true');
} else {
input.removeAttribute('aria-invalid');
}
};
// Listen on the document to handle dynamically added fields.
// 'blur' and 'focus' do not bubble, so we must use the capture phase (true).
document.addEventListener('blur', updateAriaState, true);
document.addEventListener('focus', updateAriaState, true);
// Also update on input if we've already shown the error,
// so the error clears immediately when fixed.
document.addEventListener('input', (event) => {
const input = event.target;
if (!input.matches?.('input, textarea, select')) return;
const hasAriaInvalid = input.hasAttribute('aria-invalid');
const ariaInvalid = input.getAttribute('aria-invalid');
if (hasAriaInvalid && ariaInvalid === 'true') {
updateAriaState(event);
}
});Fallbacking & Browser Support
The :user-invalid pseudo-class is widely supported (Baseline 2023), but older browsers need a fallback.
Feature Detection
You can check for support in CSS and JavaScript.
JavaScript Check:
if (!CSS.supports('selector(:user-invalid)')) {
// Fallback logic here
}CSS for Fallback
To ensure your fallback logic is visually indistinguishable from the native behavior, you must apply your error styles to both the pseudo-class and your fallback class.
/* Apply error styles to both native selector and fallback class */
input:user-invalid,
input.user-invalid-fallback {
border-color: #d93025;
background-color: #fce8e6;
}
/* Show error message for both cases */
input:user-invalid ~ .error-msg,
input.user-invalid-fallback ~ .error-msg {
display: block;
}Fallback Logic
If :user-invalid is missing manually track the interaction state using a WeakMap.
const UserInvalidFallback = (() => {
const dirtyState = new WeakMap();
const updateState = (input) => {
const isValid = input.checkValidity();
// Update both visual and ARIA state
input.classList.toggle('user-invalid-fallback', !isValid);
input.classList.toggle('user-valid-fallback', isValid);
if (!isValid) {
input.setAttribute('aria-invalid', 'true');
} else {
input.removeAttribute('aria-invalid');
}
};
const handleEvent = (event) => {
const input = event.target;
if (event.type === 'reset' && input.matches?.('form')) {
const controls = input.elements || [];
for (const control of controls) {
dirtyState.delete(control);
control.classList.remove('user-invalid-fallback');
control.classList.remove('user-valid-fallback');
control.removeAttribute('aria-invalid');
}
return;
}
if (!input.matches?.('input, textarea, select')) return;
if (event.type === 'input' || event.type === 'change') {
const state = dirtyState.get(input) || { hasInteracted: false, hasBlurred: false };
state.hasInteracted = true;
dirtyState.set(input, state);
if (state.hasBlurred) {
updateState(input);
}
} else if (event.type === 'blur') {
const state = dirtyState.get(input) || { hasInteracted: false, hasBlurred: false };
state.hasBlurred = true;
dirtyState.set(input, state);
if (state.hasInteracted) {
updateState(input);
}
}
};
const init = () => {
if (CSS.supports('selector(:user-invalid)')) return;
document.addEventListener('blur', handleEvent, true); // Capture phase required
document.addEventListener('input', handleEvent, true);
document.addEventListener('change', handleEvent, true);
document.addEventListener('reset', handleEvent, true); // Capture resets
};
return { init };
})();
// Initialize globally
UserInvalidFallback.init();Other Considerations
1. `aria-live` vs. `aria-errormessage`:
-
aria-errormessageconnects the input to the text, but screen readers might not announce it immediately upon appearance (only when focusing the input). - If you need immediate announcement when the error appears (e.g., on blur), consider adding
role="alert"oraria-live="polite"to the error message container, but test thoroughly to avoid "double announcement" when the user focuses the field to fix it.
2. Internationalization:
- Ensure the text content of your error message (
#email-error) is translated. The logic remains the same.
The Language Detector API is a client-side web API designed to identify the language of a given text string. By performing detection locally in the browser, it enhances user privacy and reduces the need for heavy external libraries or costly server-side calls.
Key Use Cases
- Translation Prep: Identifying the source language before sending text to a translator.
- Safety & Filtering: Loading specific models for tasks like toxicity detection.
- Accessibility: Labeling content with the correct
langattribute for screen readers. - UI Localization: Adjusting application interfaces based on the user's input language.
Hardware & System Requirements
- OS: Windows 10/11, macOS 13+, Linux, or Chromebook Plus.
- Storage: 22 GB free space (model is removed if space drops below 10 GB).
- RAM/CPU: 16 GB RAM and 4+ CPU cores.
- VRAM: 4 GB+ if using a GPU.
Implementation Guide
1. Model Management & User Activation
Check model availability before attempting to instantiate the detector or trigger download.
MANDATORY: Instantiating the language detector or triggering a model download with LanguageDetector.create() MUST be initiated by a user gesture (such as a button click) to prevent a NotAllowedError when the model is in a downloadable or downloading state.
// Check if the model is available or downloadable
const availability = await LanguageDetector.availability();
if (availability !== 'unavailable') {
button.addEventListener('click', async () => {
const detector = await LanguageDetector.create({
monitor(m) {
m.addEventListener('downloadprogress', (e) => {
console.log(`Downloaded ${e.loaded * 100}%`);
});
},
});
});
}2. Running Detection
The API returns a ranked list of potential languages with a confidence score between 0.0 and 1.0.
const someUserText = 'Hallo und herzlich willkommen!';
const results = await detector.detect(someUserText);
for (const result of results) {
// result.detectedLanguage (e.g., 'de')
// result.confidence (e.g., 0.999)
console.log(result.detectedLanguage, result.confidence);
}Avoid using the detector on very short phrases or single words, as accuracy drops significantly.
Security and Environment
- Iframes: Cross-origin iframes require an explicit Permissions Policy to access the API.
<iframe
src="https://cross-origin.example.com/"
allow="language-detector"
></iframe>- Web Workers: The API is not currently available in Web Workers due to Permission Policy complexities.
- Privacy: No data is sent to Google or third parties during the detection process.
Fallback Strategy
Language detector has limited availability. Supported by: Chrome 138 (Jun 2025). Unsupported in: Edge, Firefox, and Safari.
Before use, check if the LanguageDetector object is available in the global scope:
if ('LanguageDetector' in self) {
// The Language Detector API is supported.
} else {
// Execute fallback strategy
}If the LanguageDetector API is unsupported or availability checks return 'unavailable', you must gracefully fall back: 1. Remote API Fallback: Redirect the detection request to a server endpoint or a cloud API (such as the Vertex AI Gemini API) to identify the language. 2. Graceful Degradation: Disable language detection elements/buttons and inform the user that client-side detection is currently unsupported in this browser, preventing any unhandled exceptions or crashes.
The Prompt API allows developers to run natural language processing tasks directly in the browser using Gemini Nano. This built-in AI approach ensures user privacy, reduces server costs, and enables offline functionality.
1. Getting Started and Hardware Requirements
The Prompt API is currently available in Chrome as of version 148 (Desktop) for Windows, macOS, Linux, and Chromebook Plus.
Hardware Prerequisites
- Storage: 22 GB free space (for the initial profile and model).
- Memory/CPU: 16 GB RAM and 4+ CPU cores.
- GPU: 4 GB VRAM or more (Required for audio input).
- Network: Required only for the initial model download.
Initializing the API
Check model availability before triggering a download:
const availability = await LanguageModel.availability();
// Do not call create() when unavailable — the model cannot run on this device.
if (availability !== 'unavailable') {
const session = await LanguageModel.create({
monitor(m) {
// Inform the user while the model downloads so the UI doesn't appear frozen.
m.addEventListener('downloadprogress', (e) => {
console.log(`Downloaded ${e.loaded * 100}%`);
});
},
});
}2. Core Prompting Capabilities
Session examples in this section omit session.destroy() for brevity. Always call session.destroy() when a session is no longer needed to free device memory (see Section 5).
Basic and Streamed Output
For short responses, use prompt(). For longer content, use promptStreaming() to provide a more responsive UI.
MANDATORY: Never assign model output to innerHTML. Model output is untrusted and can contain injected markup. Always use textContent or a sanitizer.
const session = await LanguageModel.create();
// prompt() accumulates the full response before resolving — use for short, one-shot output.
const result = await session.prompt('Write a haiku about coding.');
// textContent, not innerHTML — model output is untrusted and must not be parsed as markup.
outputEl.textContent = result;
// promptStreaming() yields independent chunks that must be concatenated;
// use for longer content so each chunk can be rendered progressively.
const stream = session.promptStreaming('Write a long story about a robot.');
let completeResult = '';
for await (const chunk of stream) {
completeResult += chunk;
outputEl.append(chunk);
}
console.log('Full story:', completeResult);Multimodal Input
The Prompt API supports text, audio, and visual inputs (images, canvas, video frames).
const session = await LanguageModel.create({
// Declaring expected input types lets the browser optimize model loading.
expectedInputs: [{ type: 'text' }, { type: 'image' }],
expectedOutputs: [{ type: 'text' }],
});
const response = await session.prompt([
{
role: 'user',
content: [
{ type: 'text', value: 'What is in this image?' },
{ type: 'image', value: document.querySelector('canvas') },
],
},
]);3. Advanced Session Management
Sessions allow the model to maintain context across multiple interactions.
Context and Quota
Each session has a maximum token limit. You can monitor usage via session.contextUsage and session.contextWindow. If the window overflows, the oldest messages (except the system prompt) are dropped.
Cloning Sessions
Cloning is efficient for starting parallel conversations that share the same initial context (like a "system" personality) without re-initializing.
const mainSession = await LanguageModel.create({
initialPrompts: [{ role: 'system', content: 'You speak like a pirate.' }],
});
const branchA = await mainSession.clone();
const branchB = await mainSession.clone();
// Destroy the base after cloning — the clones own their own context from here.
mainSession.destroy();Restoring Past Sessions
While a native "restore" feature is in development, you can recreate a session by feeding previous history into initialPrompts.
Note: localStorage is unencrypted and persistent. Stored conversation history may include user PII — consider the privacy implications before persisting chat history.
// || '[]' ensures JSON.parse never receives null when the key doesn't exist yet.
const history = JSON.parse(localStorage.getItem('chat_history') || '[]');
const session = await LanguageModel.create({
initialPrompts: history, // Array of {role, content} objects
});4. Structured Output with JSON Schema
To prevent the model from adding "chatter" (e.g., "Sure, here is your JSON:"), use a JSON Schema via the responseConstraint field. This ensures the output is valid JSON that can be parsed immediately.
Example: Sentiment Classification
// Pass the schema as a plain object — do not JSON.stringify() it first.
const schema = {
type: 'object',
properties: {
rating: { type: 'number', minimum: 1, maximum: 5 },
is_positive: { type: 'boolean' },
},
required: ['rating', 'is_positive'],
};
const result = await session.prompt(
"Rate the following feedback: 'The food was great!'",
{ responseConstraint: schema },
);
const data = JSON.parse(result);
console.log(data.rating); // 5Constraints and Prefixes
You can guide the model further by prefilling the assistant's response using prefix: true.
```javascript const character = await session.prompt([ { role: 'user', content: 'Create a character sheet' }, { role: 'assistant', content: '`json\n', prefix: true }, ]); ```
5. Best Practices and Safety
- Resource Cleanup: Always call
session.destroy()when a conversation is finished to free up memory. - Output Safety: Model output is untrusted. Always write results to
textContent, notinnerHTML, to prevent XSS injection from malicious model output. - Use a sanitizer like the native Sanitizer API or DOMPurify if you need to allow limited HTML.
- Aborting Tasks: Use
AbortControllerto allow users to stop long-running generations. Pass thesignaltoprompt()orpromptStreaming(), not toLanguageModel.create(). - Security: Use Permission Policies to control access in iframes:
<iframe src="..." allow="language-model"></iframe>. - Design: Review the People + AI Guidebook to ensure responsible AI implementation.
By combining structured outputs with robust session management, developers can build complex, stateful AI applications that run entirely on the user's device.
6. Fallback strategies
LanguageModel has limited availability. Supported by: Chrome 148 (May 2026) and Edge 148 (May 2026). Unsupported in: Firefox and Safari.
Before use, check if the LanguageModel object is available in the global scope:
if ('LanguageModel' in self) {
// The Prompt API is supported.
} else {
// Execute fallback strategy
}If the Prompt API is unsupported or availability checks return 'unavailable', you must gracefully fall back:
- Remote API Fallback: Redirect the detection request to a server endpoint or a cloud API (such as the Vertex AI Gemini API).
- Local API Fallback: Redirect the detection request to a local endpoint, for example, using Transformers.js.
The Summarizer API allows web developers to offer local, AI-powered text distillation directly within the browser using Gemini Nano in Chrome or Phi in Edge. This API supports various formats, including key points, headlines, and TL;DRs, while operating entirely on-device to ensure user privacy.
---
Getting Started
The Summarizer API is available starting in Chrome and Edge 138. It requires a one-time model download of Gemini Nano or Phi (respectively).
Hardware & Software Requirements
- OS: Windows 10/11, macOS 13+, Linux, or ChromeOS (Chromebook Plus).
- Storage: 22GB free space for the profile volume.
- RAM/CPU: 16GB+ RAM and 4+ CPU cores.
- VRAM: 4GB+ (if using GPU).
Model Download and Availability
Check if the model is ready, needs downloading, or is unavailable.
Mandatory Options Passing: You must pass the identical configuration options object to both Summarizer.availability(options) and Summarizer.create(options). Do NOT use the deprecated window.ai.summarizer API surface.
Mandatory Progress Monitoring: You MUST implement a monitor for model download progress by providing a monitor(m) callback to Summarizer.create() and adding a listener for the downloadprogress event.
User Gesture Requirement: When availability is 'downloadable' or 'downloading', triggering the actual download via Summarizer.create() requires a user gesture (such as a user click). You must place the creation call inside an event listener rather than calling it unconditionally on page load to prevent NotAllowedError.
const options = {
type: 'key-points',
format: 'plain-text',
length: 'medium'
};
const availability = await Summarizer.availability(options);
if (availability === 'available') {
const summarizer = await Summarizer.create(options);
// Ready to use immediately
} else if (availability === 'downloadable') {
// A user gesture is strictly required to start the download
document.getElementById('start-download-btn').addEventListener('click', async () => {
const summarizer = await Summarizer.create({
...options,
monitor(m) {
m.addEventListener('downloadprogress', (e) => {
console.log(`Downloaded ${Math.round((e.loaded / e.total) * 100)}%`);
});
},
});
});
}API Functions & Configuration
When creating a summarizer via Summarizer.create(options), you can customize the output:
| Parameter | Options | Description |
|---|---|---|
type | key-points, tldr, teaser, headline | Defines the summary strategy. |
format | markdown, plain-text | Output syntax style. |
length | short, medium, long | Target length (e.g., 1 vs 5 sentences). |
preference | auto, speed, capability | Balances latency vs. quality. |
Example Configuration
const options = {
sharedContext: 'This is a scientific article',
type: 'key-points',
format: 'markdown',
length: 'medium',
};
if (navigator.userActivation.isActive) {
const summarizer = await Summarizer.create(options);
}Language Support
You can specify expected languages to ensure the browser can handle the specific summary request.
const summarizer = await Summarizer.create({
type: 'key-points',
expectedInputLanguages: ['en', 'ja'],
outputLanguage: 'es',
});Summarization Methods
1. Batch Summarization
Processes the entire text at once and returns the result.
const longText = document.querySelector('article').innerText;
const summary = await summarizer.summarize(longText, {
context: 'This article is intended for a tech-savvy audience.',
});
console.log(summary);2. Stream Summarization
Returns results in real-time as the model generates them, providing a more responsive UI.
const stream = summarizer.summarizeStreaming(longText);
for await (const chunk of stream) {
console.log(chunk);
}Security and Permissions
- Data Privacy: No data is sent to Google; processing happens on the local
device.
- Cross-Origin: Access can be granted to iframes using the Permission
Policy.
<iframe src="https://example.com/" allow="summarizer"></iframe>- Web Workers: Currently not supported.
Fallback Strategy
Summarizer has limited availability. Supported by: Chrome 138 (Jun 2025). Unsupported in: Edge, Firefox, and Safari.
Before initializing or querying availability, check if the browser supports the Summarizer API:
if ('Summarizer' in self) {
// The Summarizer API is supported.
} else {
// Execute fallback strategy
}If the Summarizer API is unsupported or availability checks return 'unavailable', you must gracefully fall back.
Recommended options: 1. Remote API Fallback: Direct the distillation request to a server endpoint or remote API (such as the Vertex AI Gemini API) so that users still get summaries. 2. Graceful Degradation: Visually disable the summarization controls in the UI, or hide the button while displaying a friendly message (e.g., "Local summarization is currently unsupported in this browser"). Do not allow interaction to trigger generic unhandled runtime exceptions. 3. Polyfill Fallback: You can use community-maintained polyfills like built-in-ai-task-apis-polyfills or prompt-api-polyfill to emulate the API surface using remote services with models in the cloud or on-device inference with local models.
Privacy and Cost Implications: These polyfills possibly proxy requests to remote servers (such as Gemini API over the cloud). This completely nullifies the on-device privacy guarantees of the native Built-in AI APIs and will incur server-side API usage costs.
The Translator API allows developers to perform client-side text translation using built-in AI models in Chrome and Edge. This approach eliminates the need for cloud-based translation services for ephemeral content, reducing costs and improving privacy by keeping data on the user's device.
Prerequisites & Requirements
Browser Support
- Chrome: Version 138+ (Desktop only).
- Edge: Version 148+ (Desktop only).
- Not Supported: Mobile (Android/iOS), Firefox, Safari.
Hardware Requirements
To run Gemini Nano and associated models, the system needs:
- Operating System: Windows 10/11, macOS 13+, Linux, or ChromeOS (Chromebook
Plus).
- Storage: At least 22 GB free on the profile volume.
- Memory/CPU: 16 GB+ RAM and 4+ CPU cores.
- GPU: 4 GB+ VRAM (Mandatory for Prompt API with audio).
- Network: Required only for the initial download of language packs/models.
Implementation & Code Samples
1. Checking Availability & Downloading Models
Mandatory Options Passing: You must pass the identical configuration options object to both Translator.availability(options) and Translator.create(options).
Mandatory Progress Monitoring: You MUST implement a monitor for model download progress by providing a monitor(m) callback to Translator.create() and adding a listener for the downloadprogress event.
User Gesture Requirement: When availability is 'downloadable', triggering the model download via Translator.create() requires a user gesture (such as a button click listener context). Unconditional page-load calls will trigger a NotAllowedError.
const options = {
sourceLanguage: 'es',
targetLanguage: 'fr',
};
const availability = await Translator.availability(options);
if (availability === 'available' || availability === 'downloadable') {
// A user gesture is strictly required to trigger create when downloadable
document.getElementById('start-translation-btn').addEventListener('click', async () => {
const translator = await Translator.create({
...options,
monitor(m) {
m.addEventListener('downloadprogress', (e) => {
console.log(`Downloaded ${Math.round(e.loaded * 100)}%`);
});
},
});
});
}3. Executing Translations
The API supports both static and streaming responses.
Standard Translation:
const translator = await Translator.create({
sourceLanguage: 'en',
targetLanguage: 'fr',
});
const result = await translator.translate(
'Where is the next bus stop, please?',
);
console.log(result);
// Output: "Où est le prochain arrêt de bus, s'il vous plaît ?"Streaming Translation (for long text):
const stream = translator.translateStreaming(longText);
for await (const chunk of stream) {
console.log(chunk);
}Supported Languages
The API supports a wide range of BCP 47 language codes: Here are the languages supported by Chrome's implementation of the Translator API:
- ar: Arabic
- bg: Bulgarian
- bn: Bengali
- cs: Czech
- da: Danish
- de: German
- el: Greek
- en: English
- es: Spanish
- fi: Finnish
- fr: French
- hi: Hindi
- hr: Croatian
- hu: Hungarian
- id: Indonesian
- it: Italian
- iw: Hebrew
- ja: Japanese
- kn: Kannada
- ko: Korean
- lt: Lithuanian
- mr: Marathi
- nl: Dutch
- no: Norwegian
- pl: Polish
- pt: Portuguese
- ro: Romanian
- ru: Russian
- sk: Slovak
- sl: Slovenian
- sv: Swedish
- ta: Tamil
- te: Telugu
- th: Thai
- tr: Turkish
- uk: Ukrainian
- vi: Vietnamese
- zh: Chinese
- zh-Hant: Chinese (Traditional)
Security & Performance
- Permissions Policy: Cross-origin iframes require explicit permission.
<iframe src="https://example.com/" allow="translator"></iframe>- Web Workers: Currently not supported due to Permission Policy
complexities.
- Privacy: No data is sent to Google servers during the translation process
once the model is downloaded.
Fallback Strategy
Translator has limited availability. Supported by: Chrome 138 (Jun 2025). Unsupported in: Edge, Firefox, and Safari.
Before use, check if the Translator object is available in the global scope:
if ('Translator' in self) {
// The Translator API is supported.
} else {
// Execute fallback strategy
}If the Translator API is unsupported or availability checks return 'unavailable', you must gracefully fall back.
Recommended options: 1. Remote API Fallback: Redirect the translation request to a server endpoint or cloud remote API (such as the Vertex AI Gemini API) to deliver translation functionality. 2. Graceful Degradation: Visually disable translation control elements or buttons while showing an end-user friendly note (e.g., "Client-side translation is currently unsupported in this browser"). Do not allow unhandled exceptions. 3. Polyfill Fallback: You can use community-maintained polyfills like built-in-ai-task-apis-polyfills or prompt-api-polyfill to emulate the API surface using remote services.
Privacy and Cost Implications: These polyfills proxy requests to remote servers (such as Gemini API over the cloud). This completely nullifies the on-device privacy guarantees of the native Built-in AI APIs and will incur server-side API usage costs.
CSS Layouts and Responsive Design
1. 1 Fundamentals 1. Which layout mode to use? 2. Working principles 2. 2 Flexbox 3. 3 Grid and subgrid 1. Code example: grid and subgrid 4. 4 Container queries 1. Code example: fluid typography using container query units 5. 5 Native overlays, anchor positioning, and stacking contexts 6. 6 Overflow tracking and layout stability 7. 7 Viewport mechanics and track distribution 8. 8 Grid lanes (aka masonry)
1 Fundamentals
Lean on the browser's layout engine when possible for better performance. Reach for intrinsic sizing, logical properties, and aspect-ratio before resorting to hardcoded dimensions or complicated media-queries.
1.1 Which layout mode to use?
Walk the decision tree top-to-bottom and stop at the first match. Note that layouts can be nested within each-other and each decision is based on the use-case for that container.
1. Is it a simple row OR column of items? Use flexbox — 1D, content-first, content distributes along a single axis. 2. Does a nested element need to line up with its grandparent grid's tracks? Use subgrid — 2D, relationship-first, inherits parent tracks so grandchildren can align across siblings. 3. Is it a complex page or component structure with rows AND columns? Use grid — 2D, layout-first, you define the skeleton and content fills it. 4. Is the content a long flow of prose that should split into balanced columns? Use multi-column — 1D flow, newspaper-style. 5. Are items of varied heights that need to be packed tightly? Use grid with grid-auto-flow: dense today; reach for native masonry (aka "grid lanes") only when it ships in your Baseline target (see §8). 6. Does an element need to float above the page and stay spatially tethered to a trigger, even across DOM boundaries or stacking contexts? Use anchor positioning — anchor-name on the trigger, position-anchor on the overlay (see §5).
1.2 Working principles
Do:
- Use logical properties (
inline-size,block-size,margin-inline,padding-block,inset-inline-start) for layout dimensions and spacing — seecss(vianpx -y modern-web-guidance@latest retrieve "css") for full coverage. - Apply the content-first vs layout-first mental model: flexbox when items dictate flow, grid when you define the skeleton first.
- Use the
place-*shorthands (place-content,place-items,place-self) to align across both axes in one declaration. - Reach for intrinsic sizing (
min-content,max-content,fit-content()) and flexible tracks (fr,minmax()) before fixedwidth/height— fewer media queries, more resilient layouts. - Use
aspect-ratioto reserve space for media and prevent layout shift before assets load.
.sidebar { inline-size: max-content; } /* Size to longest unbreakable token. */
.main-content { inline-size: fit-content; } /* Grow to available space, no further. */
.media { aspect-ratio: 16 / 9; inline-size: 100%; block-size: auto; }
body.centered { display: grid; place-content: center; min-block-size: 100dvb; }Forcalc-size()and constraint-aware intrinsic sizing, seecalculate-with-intrinsic-sizes(vianpx -y modern-web-guidance@latest retrieve "calculate-with-intrinsic-sizes").
2 Flexbox
One-dimensional layout — items flow along a single main axis with alignment on the cross axis. Reach for it for navbars, toolbars, item rows, and any single-row-or-column distribution.
Do:
- Establish a context with
display: flexand set the main axis withflex-direction(rowdefault). - Use
flex-wrap: wrapwhenever overflow is a possibility —nowrapwithoutoverflow: auto/hiddenwill spill on narrow viewports. - Use the
flexshorthand<grow> <shrink> <basis>(e.g.,flex: 1 1 250px) on items rather than settingflex-grow/flex-shrink/flex-basisindividually. - Use
gap(or therow-gap/column-gaplonghand) for spacing between items instead of child margins. - Prefix positional alignment with
safe(e.g.,align-items: safe center) so focusable content isn't clipped when the container is narrower than its content. - Push a single item to the far end of the main axis with
margin-inline-start: auto(ormargin-block-start: auto) — that's the standard escape hatch. - Override cross-axis alignment per item with
align-self. - Use
align-itemsto center all items on the cross axis; usemargin: autoon a single item to center it on both axes independently; usealign-contentonly when the container wraps and has extra space across rows. - Set
min-inline-size: 0(ormin-width: 0) on flex items that contain long unbreakable content (URLs, code, long strings) — flex items won't shrink below their content size by default, causing overflow.
Do not:
- Don't reach for
justify-selfon flex items — it only works on grid, block, and absolutely-positioned layouts. Use auto margins instead. - Don't use
orderorflex-direction: *-reverseto reorder interactive content. They change visual order only; the DOM order still drives sequential focus, so keyboard tab flow won't match what the user sees. - Don't confuse
space-around(half-gap at the ends) withspace-evenly(equal gaps before, between, and after). - Don't forget the axis flip: when
flex-direction: column,justify-contentaligns on the block axis andalign-itemsaligns on the inline axis — the opposite of the default. - Don't size both the container and its children to fill each other — that's a common source of overflow and surprising results. Give one side a definite size.
- Don't set both
flex-basisandwidth/inline-sizeon the same item —flex-basistakes precedence in a flex context andwidthis ignored. Useflex-basis(or theflexshorthand) as the single source of truth for sizing flex items.
.card-grid { display: flex; flex-flow: row wrap; gap: 1rem; }
.card-item { flex: 1 1 250px; } /* grow, shrink, basis */
.card-item-action { margin-inline-start: auto; } /* Push to main-axis end. */
.toolbar { display: flex; align-items: safe center; }3 Grid and subgrid
Baseline status for Subgrid: Widely available. It's been Baseline since 2023-09-15. Supported by: Chrome 117 (Sep 2023), Edge 117 (Sep 2023), Firefox 71 (Dec 2019), and Safari 16 (Sep 2022).
Two-dimensional layout — define rows AND columns explicitly, or let the engine derive them. Subgrid lets a nested grid inherit its parent's tracks so grandchildren align across siblings.
Choosing grid features:
- Do you know exactly how many columns you need?
- Yes — use explicit tracks (
grid-template-columns: 200px 1fr,repeat(3, 1fr), etc.) - Do different columns need different sizes (sidebar + main, header spanning all)? → use
grid-template-areasfor named, readable regions - Are all columns uniform or positioned purely by line number? → use
repeat(N, ...)or named lines - No (responsive, unknown item count) — use
repeat(auto-fit, minmax(min, 1fr)) - Should items on the last row stretch to fill remaining space? →
auto-fit - Should empty last-row tracks hold their min size (preserving column ghost slots)? →
auto-fill - Do you need to place an item at a specific location?
- Yes — use
grid-column: <start> / <end>orgrid-area: <name> - No (just spanning multiple tracks, flow position doesn't matter) — use
grid-column: span <n> - Do child elements need to inherit the parent grid's track sizes (ragged-edge alignment across siblings)?
- Yes — use subgrid on the affected axis
- Is the number of children per cell variable? → subgrid one axis only; use
grid-auto-rows/grid-auto-columnsfor the other - Is the child count fixed? → subgrid on both axes is fine
- No — standard grid, no subgrid needed
Do:
- Establish a context with
display: grid. - Use
grid-template-areasfor complex page-level layouts — area names are self-documenting and the declaration can be aligned in rows and columns for at-a-glance readability. - Use
repeat(auto-fit, minmax(200px, 1fr))for responsive card grids that stretch filled tracks to fill the row, orauto-fillto preserve empty repeated tracks at their min size. - Use
frfor proportional track distribution andminmax(min, max)for flexible-but-bounded tracks. - Position items with
grid-column: span <n>to size across tracks,grid-column: <start> / <end>to place at specific lines, orgrid-area: <name>for named regions. - Use subgrid (
grid-template-columns: subgridorgrid-template-rows: subgrid) to solve the "ragged edge" problem in card lists — internal elements like titles, metadata, and CTAs line up across siblings. - Pair a subgrid declaration with a preceding explicit
grid-template-rows/-columnsdeclaration as a same-cascade fallback for older browsers.
Do not:
- Don't expect
auto-fit/auto-filltrack size to come from item content — it comes from therepeat()size argument. - Don't use
grid-auto-flow: denseon interactive content. It packs items efficiently but reorders them visually, breaking DOM-order keyboard tab flow. - Don't apply subgrid to both axes when the child count is variable. Extras land in the last track; use
grid-auto-rows/grid-auto-columnsfor the implicit axis instead. - Don't confuse
justify-items/align-items(aligns item content within its track) withjustify-content/align-content(aligns the grid tracks within the container). Using the wrong one silently has no effect. - Don't use
repeat(auto-fit/auto-fill, ...)without a definiteinline-sizeon the container — insidedisplay: inline-gridor an unsized flex item, the container has no width to divide, making track counts unpredictable.
3.1 Code example: grid and subgrid
Page shell: <main class="page-layout"> contains <header>, <aside>, a <section class="card-grid"> with <div class="card"> children, and <footer>.
/* Align grid-template-areas in rows and columns for readability. */
.page-layout {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-areas:
"header header header"
"sidebar main main"
"footer footer footer";
gap: 1.5rem;
}
header { grid-area: header; }
aside { grid-area: sidebar; }
footer { grid-area: footer; }
.card-grid {
grid-area: main;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
grid-template-rows: auto 1fr; /* title block, body block */
gap: 1rem;
}
.card {
grid-row: span 2;
display: grid;
/* Same-cascade fallback: ignored when subgrid is supported. */
grid-template-rows: auto 1fr;
grid-template-rows: subgrid;
}4 Container queries
Baseline status for Container queries: Widely available. It's been Baseline since 2023-02-14. Supported by: Chrome 105 (Sep 2022), Edge 105 (Sep 2022), Firefox 110 (Feb 2023), and Safari 16 (Sep 2022).
Query the size (or computed style) of an ancestor container rather than the viewport. Mental model: container queries = component context; media queries = global page layout and user preferences (prefers-color-scheme, prefers-reduced-motion).
Do:
- Establish a containment context with
container-type: inline-size(width-only queries) orcontainer-type: size(both axes) on a wrapper before its descendants can be queried. - Name containers with
container-name(or thecontainershorthand:container: inline-size card) when nested contexts could collide. - Include container query units in calculating fluid type and spacing:
cqi/cqb(logical inline/block),cqw/cqh(physical),cqmin/cqmax. - Give the container a definite
block-sizewhenevercontainer-type: sizeis used — without one, descendants collapse because size containment forces the container to ignore its content.
Do not:
- Don't use
block-sizeas acontainer-typevalue — it isn't valid. Usesizefor both axes. - Don't expect children's intrinsic size to influence the container after declaring
container-type. The container is computed as if it has no children once containment is active. - Don't rely on container query units inside descendants of a non-qualifying ancestor; they fall back to the small viewport (
svw/svh).
4.1 Code example: fluid typography using container query units
.card-wrapper {
container: inline-size / card; /* shorthand for container-type + container-name */
}
@container card (inline-size > 400px) {
.content {
display: flex;
gap: 2rem;
}
}
.title {
/* Fluid type bound to the container width, not the viewport. */
font-size: clamp(1rem, 4cqi, 2rem);
}For component-driven responsive styling patterns, seesize-aware-styling(vianpx -y modern-web-guidance@latest retrieve "size-aware-styling") andfluid-scaling(vianpx -y modern-web-guidance@latest retrieve "fluid-scaling").
5 Native overlays, anchor positioning, and stacking contexts
Baseline status for <dialog>: Widely available. It's been Baseline since 2022-03-14. Supported by: Chrome 37 (Aug 2014), Edge 79 (Jan 2020), Firefox 98 (Mar 2022), and Safari 15.4 (Mar 2022). Baseline status for Popover: Newly available. It's been Baseline since 2025-01-27. Supported by: Chrome 116 (Aug 2023), Edge 116 (Aug 2023), Firefox 125 (Apr 2024), Safari 17 (Sep 2023), and Safari iOS 18.3 (Jan 2025). Anchor positioning is not natively supported by any major browser yet.
When to use each overlay primitive:
- Use
popoverfor transient, non-modal UI (flyouts, toasts, tooltips) — lives in the top layer, noz-indexmanagement needed. - Use
<dialog>with.showModal()for modal interactions that require focus trapping and an inert backdrop. - Don't combine
popoverand.showModal()on the same element — they're mutually exclusive runtime states.
Anchor positioning (spatial layout of overlays):
- Use
position-area(oranchor()on insets) andanchor-size()to position and size an overlay relative to its trigger. - Use
position-try-fallbacks: flip-block(orflip-inline) to let the browser reposition when the overlay overflows the viewport. - Don't mix physical and logical keywords in a single
position-areavalue — pick one coordinate system. - Feature-detect with
@supports (anchor-name: --x)and provide an absolute-position fallback.
For full implementation detail, polyfill strategies, andpopovervalue reference, seedeclarative-dialog-popover-control(vianpx -y modern-web-guidance@latest retrieve "declarative-dialog-popover-control") andposition-aware-tooltips(vianpx -y modern-web-guidance@latest retrieve "position-aware-tooltips"). For anchor positioning applied to menus and tab indicators, seeresilient-context-menus-and-nested-dropdowns(vianpx -y modern-web-guidance@latest retrieve "resilient-context-menus-and-nested-dropdowns") andanchor-positioning-tab-underline(vianpx -y modern-web-guidance@latest retrieve "anchor-positioning-tab-underline").
6 Overflow tracking and layout stability
Baseline status for scrollbar-gutter: Newly available. It's been Baseline since 2024-12-11. Supported by: Chrome 94 (Sep 2021), Edge 94 (Sep 2021), Firefox 97 (Feb 2022), and Safari 18.2 (Dec 2024). line-clamp is not natively supported by any major browser yet.
Manage layout shifts, scrollbars, and clipping predictably.
Do:
- Use
overflow: autoso scrollbars appear only when content actually overflows. - Use
overflow: clipto clip content without establishing a scroll container; opt into spillover withoverflow-clip-margin. - Use
scrollbar-gutter: stableto reserve space for scrollbars and prevent layout shifts when content grows. - Use
overscroll-behavior: contain(ornone) on scrollable containers to stop scroll chains from bubbling into the parent or document. - Use the
-webkit-line-clamp+display: -webkit-box+-webkit-box-orient: verticaltriad for multi-line truncation — despite the prefix, this pattern is fully specified and not deprecated. Declare the unprefixedline-clampshorthand alongside it; browsers that don't yet support it ignore the property harmlessly.
Do not:
- Don't use
overflow: scrollwhenautowill do —scrollforces scrollbars even when there's nothing to scroll. - Don't reach for
overflow: hiddenwhen you only want to clip —hiddenestablishes a scroll container that can be programmatically scrolled.
.scrollable-list {
max-block-size: 400px;
overflow-y: auto;
scrollbar-gutter: stable; /* Reserve scrollbar space. */
overscroll-behavior: contain; /* No scroll chaining into the page. */
}
.snippet {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
line-clamp: 3; /* Ignored where unsupported. */
overflow: clip;
}Foroverflow: clipandoverflow-clip-marginin depth, seeoverflow-clipping-control(vianpx -y modern-web-guidance@latest retrieve "overflow-clipping-control"). For scrollbar color, sizing, and theming, seecustomize-scrollbar-color-and-thickness(vianpx -y modern-web-guidance@latest retrieve "customize-scrollbar-color-and-thickness"),dark-mode(vianpx -y modern-web-guidance@latest retrieve "dark-mode"), andadapt-scrollbar-to-contrast-preferences(vianpx -y modern-web-guidance@latest retrieve "adapt-scrollbar-to-contrast-preferences").
7 Viewport mechanics and track distribution
Baseline status for Small, large, and dynamic viewport units: Widely available. It's been Baseline since 2022-12-05. Supported by: Chrome 108 (Nov 2022), Edge 108 (Dec 2022), Firefox 101 (May 2022), and Safari 15.4 (Mar 2022).
- Use
dvh/dvwfor mobile layout containers that must account for browser UI shifting (URL bar collapse/expand). - Don't use
100vwfor full-width layout — it ignores scrollbar width and causes horizontal overflow. Use100%,100dvw, or100svwinstead.
For the full viewport unit reference (svh,lvh,dvi,dvb, etc.), seecss(vianpx -y modern-web-guidance@latest retrieve "css").
8 Grid lanes (aka masonry)
Masonry is not natively supported by any major browser yet.
The spec is in development. The currently agreed-upon name is "grid lanes" (e.g., display: grid-lanes). Firefox ships grid-template-rows: masonry behind a flag; no other engines ship it in stable as of this writing.
Do:
- Use grid with
grid-auto-flow: densefor tight packing today, accepting that DOM order may not match visual order. - Use multi-column (
columns: 3; column-gap: 1rem) for content-heavy masonry-like flow when items are document fragments rather than equal-weight cards. - Treat
grid-template-rows: masonryas a progressive enhancement only — feature-detect with@supports.
Do not:
- Don't ship
grid-template-rows: masonryas a hard requirement until your Baseline target catches up.
.gallery { columns: 3 200px; column-gap: 1rem; }
.gallery > * { break-inside: avoid; margin-block-end: 1rem; }
@supports (grid-template-rows: masonry) {
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
grid-template-rows: masonry;
gap: 1rem;
columns: unset;
}
}CSS: Modern Architecture and Performance
These guidelines provide a high-density reference for writing maintainable, performant, and standard-compliant CSS.
1. 1. Foundations 2. 2. Inheritance and The Cascade 3. 3. Selectors and scoping 1. Prefer CSS selectors over JS for complex element targeting 2. Use `:is()` (or `:where()`) instead of CSS rule duplication for fallbacks 3. Avoid overmatching 4. Nesting and scoping 4. 4. Interactivity 1. Focus management 2. Touch targets 5. 5. Design Tokens and Theming 1. Dark mode 2. Forced Colors Mode 3. Generating tints 4. Theming browser-generated UI 6. 6. Responsive design 1. Responsive Typography 7. 7. Typography 1. Text wrapping 8. 8. Visual effects 1. Depth and texture 2. Shapes 3. Gradients and `color-mix()` 4. Patterns 9. 9. Transitions \& animations 1. Performance 2. Accessibility 10. 10. Generated content
1. Foundations
Be allergic to knowledge duplication. Prefer variables over repetition, but whenever possible, prefer built-in conventions such as:
currentColorinstead of defining a variable and settingcolorto it- The
inheritkeyword instead of defining a variable on the parent and using it on the same property across parent and child. emunits instead offont-size: var(--size)cqw/cqh(or their logical versions —cqi/cqb) units instead of repeating box model values.- Code duplication is not knowledge duplication. The goal is robustness and maintainability, not saving characters.
- Prefer logical properties and values over physical ones (e.g.
margin-inline-startinstead ofmargin-left) so that styles adapt to different writing modes and orientations. Even if the page author does not plan to localize, external translation tools often display translated text in context. - Do not use logical properties indiscriminately — ask yourself "would I want this to flip in RTL?" — if the answer is no, use the physical property instead.
- Consider different viewing modes (dark mode, high contrast mode), different viewport sizes, and different input modes (touch, keyboard, pointer).
2. Inheritance and The Cascade
Avoid introducing BEM naming conventions to manage specificity. Instead, use modern CSS features such as cascade layers and :where() to make cascade behavior predictable and follow author intent.
Use cascade layers (@layer) to define explicit priority zones (e.g., reset, base, theme, components, utilities), and declare their order upfront (e.g. @layer reset, base, theme, components, utilities;). Within each layer, use :where() to make selectors only compete based on meaningful signals, not incidental filters (:not() edge cases, remote ancestors, etc.) or for one-off easily overridable defaults.
Use keywords like inherit, initial, unset, or revert instead of explicit values to improve maintainability and better express intent. Examples:
- When specifying a transition on a child that should match the parent's
transition-*properties, instead of repeating the transition properties on the child, usetransition: inherit(reduce duplication, improve maintainability) - Use
initialto reset a property to its initial value instead of specifying the value explicitly (clearer expression of intent)
3. Selectors and scoping
Modern browser-native selectors reduce the need for preprocessors and complex state-tracking in JS.
Prefer CSS selectors over JS for complex element targeting
- DO use
:has()to style parents based on child state instead of managing classes in JS (e.g.label:has(:checked)instead of a manuallabel.has-checkedclass) For more information, see the guides atchild-state-based-styling(vianpx -y modern-web-guidance@latest retrieve "child-state-based-styling") andcontent-based-styling(vianpx -y modern-web-guidance@latest retrieve "content-based-styling"). - DO NOT nest
:has()or use pseudo-elements inside it (browser API limitation) - Use
:nth-child(<An+B> of <selector>)when you need to style every n-th element of a certain type. E.g.details:nth-child(1 of [open])will style the first open<details>element it finds, whereasdetails[open]:first-childwould style only the first child if and only if it was open.
Use :is() (or :where()) instead of CSS rule duplication for fallbacks
DO NOT duplicate CSS rules to provide fallbacks for pseudo-classes that may not be supported — use :is() or :where() instead and take advantage of their forgiving parsing rules.
/* BAD: duplicate rules instead of using `:where()` */
[popover]:popover-open {
/* styles for native popovers */
}
[popover].\:popover-open {
/* same styles again, for polyfilled popovers */
}
/* GOOD */
[popover]:where(:popover-open, .\:popover-open) {
/* same styles in one rule */
}Do NOT use this for pseudo-elements, as they are not supported in :is() or :where().
Avoid overmatching
Write selectors in a way that expresses _intent_.
Use :not() instead of overrides to exclude irrelevant states/targets
When the intent is to exclude certain states or elements that are fundamentally irrelevant, use :not().
For example, to apply bottom borders between list items, don't do this:
.fancy-list li {
border-bottom: 1px solid silver;
}
.fancy-list li:last-child {
border-bottom: none;
}This can unintentionally overwrite a desirable border-bottom set from another rule. The actual intent was to only apply the bottom border to the non-last lis. The code above is a workaround that poorly expresses this intent. Instead, this expresses intent more clearly:
.fancy-list li:not(:last-child) {
border-bottom: 1px solid silver;
}Similarly, don't do this:
button:hover {
background: var(--color-blue);
}
button:disabled {
background: var(--color-neutral);
}If we reorder the two rules, we will get a hover background on disabled buttons! Instead, do this:
button:hover:not(:disabled) {
background: var(--color-blue);
}
button:disabled {
background: var(--color-neutral);
}This works regardless of reordering, as the first rule does not overmatch.
Prefer @scope over :not() for excluding (potentially deeply nested) subtrees
While :not() + descendant selectors can exclude subtrees, this works poorly for deeply nested structures. For example, .card :not(.content *) will not work as expected for nested cards. @scope fixes this as it takes hierarchical proximity into account:
@scope (.card) to (.content) {
/* styles for elements inside .card but not inside .content */
}This will work as expected even for nested cards.
Overrides are fine for specialization
This is fine:
button {
background: var(--color-neutral);
}
button.primary {
background: var(--color-blue);
}Both rules express legitimate _intent_: buttons are generally neutral, but primary ones are blue.
No global resets
DO NOT use global resets (styles on *) as they cannot be overridden by web components or lower-priority cascade layers (without !important). Instead, apply reset styles to specific element types and/or conditions.
Nesting and scoping
Use native CSS nesting to group related styles to the extent it improves maintainability and readability.
Prefer @scope over nesting when proximity should matter more than pure specificity. This is common in selectors that can be nested in any order, but the closest matching one (in element -> ancestor order) should win, e.g. theming classes.
For example this will not work as expected:
.dark .invert { color-scheme: light }
.light .invert { color-scheme: dark }If .invert is nested within _both_ .dark and .light, it will always resolve to dark mode as both rules have the same specificity. Using @scope fixes this:
@scope (.dark) {
.invert { color-scheme: light }
}
@scope (.light) {
.invert { color-scheme: dark }
}4. Interactivity
Focus management
- Use
:focus-visibleto define custom focus rings, not:focus. - Do not remove the browser's default focus rings (via
outline: none) without providing an alternative visible focus style. - Prefer
outlineover other properties (e.g.box-shadow) for focus rings. If you must rely onbox-shadowfor focus rings, provide anoutline-based fallback for High Contrast Mode using theforced-colorsmedia query. - Pair focus outlines with
outline-offsetto visually separate the ring from the element.
Touch targets
- Interactive elements should be at least 24×24 CSS pixels (WCAG 2.5.8 AA). Enforce with
min-block-size/min-inline-sizeor padding rather thanwidth/height, so content can grow the target but not shrink it. - Bump targets up on coarse pointers:
@media (pointer: coarse) { ... }. - DON'T use
touch-action: nonefor custom gestures — it disables page scrolling through the element. Scope to the axis you actually need:pan-yfor horizontal swipes (page still scrolls vertically),pan-xfor vertical ones. Reservenonefor elements where no native touch behavior makes sense (e.g. a drawing canvas).
5. Design Tokens and Theming
Use CSS custom properties on :root to define core design variables (colors, fonts, sizes, etc) used throughout the design, for visual consistency and to scale UI design across teams. DO NOT specify nontrivial styling values inline. E.g. background: transparent or padding: 0 is ok, but background: #f06 or padding: .3em are not. One exception is use cases where keeping code small and simple is far more important than long-term maintainability and evolution, such as testcases.
Typically these are organized in tiers, with each tier building upon the previous one. For example: 1. Tier 1: Literal design tokens (e.g. --color-blue-10, --color-gray-90, --font-sans-serif, --size-xl etc) 2. Tier 2: Semantic design tokens (e.g. --color-accent, --color-neutral, --font-body, --font-heading etc) 3. Tier 3: General UI design tokens (e.g. --ui-border, --surface-bg-subtle etc) 4. Tier 4: Component-specific design tokens (e.g. --button-bg-primary-hover, --button-border-color-secondary etc)
The smaller the scope of the use case, the fewer tiers it needs. E.g. a quick demo or toy app are fine with one tier. Do not overengineer. Check for any existing conventions around naming and levels before inventing your own.
Dark mode
- Use
color-scheme: light darkon:rootto enable dark mode support that automatically adapts to the system setting. You can also specifycolor-schemeon individual elements to force a different value for that subtree (light/darkorlight darkfor the system default) - Use
light-dark()to provide alternatives that automatically resolve based on the element'scolor-scheme.
Typically this happens in Tier 2 or Tier 3 tokens.
- IMPORTANT: When using
light-dark()on an inherited<color>property, it will resolve to a specific color based on that element'scolor-schemeand inherit as that resolved color, not as alight-dark()value. It will NOT adapt to any descendant-specificcolor-schemeoverrides. To keeplight-dark()color tokens dynamic resolve them as late as possible by only passing them around as unregistered custom properties and avoid relying on inherited color values acrosscolor-schemeboundaries.
See dark-mode (via npx -y modern-web-guidance@latest retrieve "dark-mode") for tips & best practices on supporting dark mode switching and component-specific-light-dark-theme (via npx -y modern-web-guidance@latest retrieve "component-specific-light-dark-theme") for more on applying different color-scheme modes than the page-wide setting on certain elements.
Forced Colors Mode
In Forced Colors Mode (High Contrast on Windows), the browser overrides author colors with system keywords and strips background-image, box-shadow, and border-image.
- Define system color fallbacks for color tokens using
@media (forced-colors: active). - DON'T rely on
background-image,box-shadow, orborder-imageto convey borders, separators, or state — they disappear in forced colors (and often in print too). If you must, ensure there's an alternative in forced colors mode, such asoutlineorborderwith system color keywords (CanvasText,LinkText,ButtonText,Highlight,GrayText, etc.). - Use
forced-color-adjust: nonewhere color is essential information (syntax highlighter, color picker swatch). DON'T useforced-color-adjust: nonejust to preserve aesthetics.
Generating tints
Before generating tints dynamically, check if you can use an existing, predefined, design token. This allows much more designer control and ensures consistency.
If you need to generate lighter or darker colors dynamically:
- DO NOT just adjust the lightness channel in
oklch/oklaborlch/lab, e.g.oklab(from var(--primary) 0.9 a b). While that is theoretically the correct way, browsers do not yet implement gamut mapping, so the resulting color is unpredictable. - You can use
color-mix()to mix with white or black (preferably inoklab). This keeps the color safely in gamut, but tends to over-desaturate colors and produce washed out tints and shades. - You MAY combine lightness adjustment with any of the other methods (e.g.
color-mix(in oklab, oklch(from var(--primary) 0.9 c h), white 30%)) for a balance between the two, but avoid going above 30% for the lightness adjustment.
Theming browser-generated UI
Most browser-generated UI can be customized to some extent using CSS. Even if it requires modern features, it degrades gracefully in older browsers, and thus often does not require a polyfill or fallback.
Before re-creating browser UI (form controls, scrollbars, selections, error messages, etc), first verify that: 1. the browser UI cannot be customized enough for your needs, even with modern CSS, 2. the desired customization is sufficiently critical to justify the tradeoffs of re-creating built-in UI — most notably losing accessible semantics, keyboard handling, IME, and AT integration that the native UI provides for free.
Example customizations that are possible:
- Use
::selectionto customize highlighted text colors. - DON'T apply
user-select: noneto content text — breaks copy-paste, translation tools, and AT "read from here" gestures. Limit it to chrome (drag handles, toolbars, redundant button labels). - Use
accent-colorto apply the page's accent color to any browser-generated UI. - Use
color-schemeto have browser UI adapt to light/dark mode. - Use
scrollbar-colorto customize scrollbar colors andscrollbar-widthto control scrollbar thickness — keep the thumb visibly distinct from the track (≥3:1), and don't setscrollbar-width: noneon scrollable regions (use it only when scrolling is fully replaced by another affordance). - Use
:user-invalid/:user-validfor validity styling, not:invalid/:valid— they only match after the user has interacted with the field, avoiding the hostile default of flagging required-empty fields as errors on page load. - Buttons and text fields (including
<textarea>) can generally be styled as normal elements. - Use
font-sizeto scale and other textual properties to control typography
Styling textual fields (<input> & <textarea>)
For most styling purposes (e.g. colors, borders, backgrounds, typography, etc) treat these elements as normal text containers.
- Use
:placeholder-shownand::placeholderto style input placeholders. - Use
field-sizing: contentto make text fields size to content. - For
<textarea>elements, useresize: verticalto disable horizontal resizing orresize: noneto disable all resizing.
Multiple choice controls (select, radios, checkboxes)
- To select one among many options presented in a dropdown: Use a
<select>+appearance: base-select+::picker(select). For more info seebranded-select-styling(vianpx -y modern-web-guidance@latest retrieve "branded-select-styling") - Selecting one or more among multiple options laid out inline in the page: Use a
<input type=checkbox>or<input type=radio>inside a<label>for each option. Style vialabel:has(:checked). - Style checkboxes, radios and switches via
appearance: none+ generated content (::before/::after) or background images to draw the checked state.
<!-- Customizable select listbox version currently buggy + this has much better browser support -->
Non-textual <input>s (buttons, sliders, file inputs etc.)
- File inputs: Use
::file-selector-buttonto style the button. - Do not use
<input>with atypeofbutton,submitorreset. Use<button>instead and style it as a regular element. - Sliders: Use
appearance: none+ thumb pseudo-elements (::-webkit-slider-thumb,::-moz-range-thumb, etc) and track pseudo-elements (::-webkit-slider-runnable-track,::-moz-range-track, etc) for more granular control.
6. Responsive design
- Use
@containerqueries to create component-driven responsive layouts that adapt to their parent container's size rather than the viewport. - Use dynamic viewport units (
dvh,dvw) instead ofvh/vwto prevent layout breakage when mobile browser UI elements (like address bars) appear or disappear. - Use
aspect-ratiofor media elements (like<img>and<video>) to reserve space during loading and prevent Cumulative Layout Shift (CLS).
Responsive Typography
- DO combine viewport-relative and font-relative units in
clamp()for font sizes that scale with the viewport size while ensuring they stay within a desired range. For example,clamp(2rem, 1rem + 5vw, 4rem). Adjust the proportion of viewport-relative and font-relative units to control how quickly the font-size changes. - DON'T use
vwalone for font-size withoutclamp(), as it can scale text too small or too large on extreme screens.
7. Typography
- Use unitless numbers for
line-height(e.g.,1.5) to ensure relative scaling during font-size inheritance. - Use
overflow-wrap: break-word(oranywhere) to contain long URLs. - DON'T use
pxfor font-size. Preferremto honor the user's browser font-size preferences (root font size), oremfor contextual sizing.
Text wrapping
- Use
text-wrap: balancefor balanced headlines and headline-like content (e.g.<th>) - Use
text-wrap: prettyfor long-form body text (paragraphs, blockquotes, etc.) - Use
text-wrap: balanceortext-wrap: prettydeliberately, DO NOT apply it on*as it does have a performance cost. - Avoid
text-wrap: balanceon elements with a visible box (backgrounds, borders, shadows, etc) as it does not change the container's width, it only affects how text wraps within that width. This can leave empty space at the end of the container, which is usually undesirable.
8. Visual effects
Depth and texture
- Layer multiple shadows for realistic soft depth effects.
- Use
filter: drop-shadow()instead ofbox-shadowfor non-rectangular shapes or transparent PNGs. - Use
mix-blend-modeandbackground-blend-modefor lighting overlays (limit scope withisolation: isolate)
.hero {
background-image: url('texture.png'), linear-gradient(to bottom, #fff, #eee);
background-blend-mode: soft-light;
}Shapes
- Use
corner-shape: squirclefor more aesthetically pleasing curves as a progressive enhancement over regular rounded corners. - Use elliptical
border-radius(e.g.,10px / 20px) for proportional curves without extra elements.
Gradients and color-mix()
Use in oklch or in oklab to explicitly specify the interpolation color space for gradients or color-mix().
in oklchpreserves chroma better, but can more easily get out of device gamut, especially for bigger differences between colorsin oklabstays in gamut more easily (assuming in-gamut endpoints) but can create washed out desaturated colors in the middle, especially when interpolating between opposite hues.- DON'T use
in srgbunless you have a specific reason to do so (e.g. you are building a color picker that needs to interpolate in srgb).
Fallback
Some pre-2024 browsers do not support gradient color interpolation space. To support these browsers, use the token only when its usage is safe by defining a variable:
:root {
--in-oklab: ;
--in-oklch: ;
}
@supports (linear-gradient(in oklab, white, black)) {
:root {
--in-oklab: in oklab;
--in-oklch: in oklch;
}
}Then use like:
.card {
background: linear-gradient(to bottom var(--in-oklab), var(--accent-color), var(--darker));
}- Important: If you use this technique, make sure there is always a non-empty gradient preamble without it, otherwise it will be a syntax error in older browsers.
- You do NOT need this for
color-mix(). If a browser supportscolor-mix(), it also supports itsin <color-space>argument.
Patterns
Many patterns can be created via CSS gradients + hard stops, and these can be more flexible and performant than SVGs or external images as they can have access to CSS variables and lengths from the surrounding context. You don't need to repeat the position twice — just use 0 or 0% and gradient fixup will auto-adjust it.
Examples below.
Vertical stripes of 1em width each:
background: linear-gradient(to right, var(--color-1) 50%, var(--color-2) 0) 0 / 2em;Diagonal stripes of 1em width each:
background: repeating-linear-gradient(-45deg, var(--color-1) 0 1em, var(--color-2) 0 2em);Checkerboard pattern with 1em squares:
background: repeating-conic-gradient(var(--color-1) 0 25%, var(--color-2) 0 50%) 0 / 2em 2em;Polka dot with .5em radius dots spaced 2em apart (horizontally/vertically — multiply by sqrt(2) for diagonal distance):
--distance: 2em;
--radius: .5em;
--polka: radial-gradient(circle, var(--color-1) var(--radius), transparent calc(var(--radius) + 1px));
background: var(--polka) 0 0, var(--polka) var(--distance) var(--distance) var(--color-2);
background-size: calc(var(--distance) * 2) calc(var(--distance) * 2);Simple pie chart:
.pie {
--p: 80%;
width: 60px;
aspect-ratio: 1;
border-radius: 50%;
background: conic-gradient(var(--color-1) var(--p), transparent 0%) var(--color-2);
}Important: When using gradients to render charts, ensure there is a textual fallback for screen readers. MANDATORY: You MUST provide a semantic data table as an accessible alternative, as detailed in accessibility (via npx -y modern-web-guidance@latest retrieve "accessibility") under the alternate text and media guidelines.
9. Transitions & animations
- Use
clip-pathandmask-imagefor custom geometric reveals and smooth fade-outs. - Use Scroll-Driven Animations (
animation-timeline: scroll()) for non-essential scroll-bound effects instead of JS listeners. - Use View Transitions to animate between complex layout states seamlessly.
Performance
Rendering performance is critical for smooth user experiences, especially in heavy DOM trees.
- Prefer to animate
opacityandtransform(including individual transform properties, e.g.translateinstead ofleft/right/top/bottom) to ensure animations stay on the compositor thread. - Use
transition-behavior: allow-discrete+@starting-styleto animate layout properties likedisplayor<dialog>state natively. - Always pair
content-visibilitywithcontain-intrinsic-sizeto prevent scrollbar jumps (CLS). - When setting
contain-intrinsic-sizeuse theautokeyword and a value that’s derived from what is known about the contents (i.e. text size, spacing, size of graphics, character count). Preferably use units such asrem,lh,cap, orchthat match values used for the elements within the contents rather thanpx. If the content for items in a group is not consistently sized, then use an average size. - Use
contain: layout style paintto isolate component rendering updates.
Code Example: Render Optimization
.large-section {
content-visibility: auto;
contain-intrinsic-block-size: auto 800px;
}
.row {
--row-gap: .4rem;
--title-height: 1lh;
--description-height: 0.85lh;
display: grid;
row-gap: var(--row-gap);
content-visibility: auto;
/* The sum of the title height, row gap, and description height should be the size of the contents when skipped for rendering. */
contain-intrinsic-block-size: auto calc(var(--title-height) + var(--row-gap) + var(--description-height));
}
.popover-reveal {
/* Allow discrete animations for display transitions */
transition: display 0.2s allow-discrete;
}Accessibility
Use prefers-reduced-motion media queries to turn off heavy motion for users who prefer it.
DO NOT globally apply animation-duration: 0.01ms; globally as it can cause certain animations to become _more_ jarring. Either apply reduced motion versions on a case by case basis, or use a custom property like:
@property --animation-reduced {
syntax: "*";
inherits: false;
initial-value: none;
}
@media (prefers-reduced-motion: reduce) {
* {
animation: var(--animation-reduced) !important;
}
}Then, reduced motion versions can be kept together with the original animations:
progress:not([value]) {
animation: slide 1s infinite linear;
--animation-reduced: slide 20s infinite linear;
}10. Generated content
- DON'T use
contentto convey meaningful text (labels, state, instructions) — keep that in the DOM (WCAG F87). The alt text argument is harm reduction for cases where decoration accidentally carries meaning, not a license. - Use the alternative text argument of
contentto provide alt text for screen readers. E.g.content: url(cloud.svg) / "Save"; - Use
content: "text" / "";to prevent purely decorative text from being announced to screen readers. - DON'T use an empty alt text argument for images — they're already presentational by default. E.g. this is wrong:
content: url(cloud.svg) / "";. - DON'T use the alt text argument to describe emojis unless the description differs from the official emoji name. E.g. don't do
content: "🎉" / "celebration";, butcontent: "🎉" / "Yay!";is fine.
ONLY use the alt text argument when the text is different than the primary value and is not already present in the DOM. I.e. this is wrong:
HTML:
<button class="save">Save</button>CSS:
button.save::before {
content: url(cloud.svg) / "Save";
}A screen reader would read it out as "Save save".
The CSS Custom Highlight API lets you style arbitrary text ranges on a page without modifying the DOM structure. This enables search-result highlighting, syntax coloring, collaborative editing cursors, or spelling and grammar error markers without wrapping text in extra elements or relying on innerHTML manipulation.
Core implementation
To highlight text ranges, you must collect the target text nodes, create Range and Highlight objects, register them in the HighlightRegistry, and then style them with the ::highlight() pseudo-element.
1. Collect text nodes and create ranges
Use a TreeWalker to collect all text nodes in the target element, then create Range objects pointing at the character offsets you want to highlight.
const article = document.querySelector("article");
// MANDATORY: Use TreeWalker to collect text nodes — do not manipulate innerHTML.
const treeWalker = document.createTreeWalker(article, NodeFilter.SHOW_TEXT);
const allTextNodes = [];
let currentNode = treeWalker.nextNode();
while (currentNode) {
allTextNodes.push(currentNode);
currentNode = treeWalker.nextNode();
}
// MANDATORY: Set range start/end on text nodes, not element nodes.
const range = new Range();
range.setStart(textNode, matchStartIndex);
range.setEnd(textNode, matchEndIndex);Cache the text-node list and only rebuild it when the DOM content actually changes, since walking the tree is expensive.
2. Create a Highlight from the ranges
Group one or more Range objects into a Highlight. Multiple ranges that share the same style belong in a single highlight.
const searchHighlight = new Highlight(...matchingRanges);3. Register the highlight in the registry
Register each Highlight under a custom name using CSS.highlights, which is a Map-like HighlightRegistry.
// MANDATORY: Clear previous highlights before registering new ones
// to avoid stale ranges persisting on the page.
CSS.highlights.clear();
CSS.highlights.set("search-results", searchHighlight);When multiple highlights overlap, use the priority property to control stacking order. Higher priority highlights paint on top.
const primary = new Highlight(...primaryRanges);
primary.priority = 1;
const secondary = new Highlight(...secondaryRanges);
secondary.priority = 0; // painted first (behind primary)
CSS.highlights.set("primary", primary);
CSS.highlights.set("secondary", secondary);4. Style with ::highlight()
Use the ::highlight() pseudo-element in CSS to style each registered highlight by name.
::highlight(search-results) {
background-color: #ffdd00;
color: black;
}Only a limited set of CSS properties work inside ::highlight(): color, background-color, text-decoration and its longhands, text-shadow, -webkit-text-stroke-color, -webkit-text-fill-color, and -webkit-text-stroke-width. Properties like background-image, font-size, or padding are ignored.
Accessibility
AVOID: using custom highlights as a replacement for semantic HTML.
Custom highlights are purely presentational and are not exposed to the accessibility tree. If the highlighted text is semantically relevant to the document (e.g., a user-selected passage), use <mark> instead. Reserve custom highlights for transient, visual-only effects like search results or syntax coloring.
Highlights should not rely solely on color to convey meaning. If a highlight indicates an error, pair it with another visual indicator such as text-decoration: wavy underline or an adjacent text label. Ensure sufficient contrast between the highlight background and text color to meet WCAG 2.1 requirements (at least 4.5:1 for normal text).
Fallback strategies
Baseline status for Custom highlights: Newly available. It's been Baseline since 2026-03-24. Supported by: Chrome 105 (Sep 2022), Edge 105 (Sep 2022), Firefox 149 (Mar 2026), and Safari 17.2 (Dec 2023).
For browsers that do not support the CSS Custom Highlight API, you should provide a functional base experience where text is still legible, even without the visual highlight.
You can detect support before using the API:
if (CSS.highlights) {
// CSS Custom Highlight API is supported.
} else {
// Fallback: wrap matches in <mark> elements.
}If the highlight is critical for the user experience, fall back to wrapping matched text in <mark> elements. This modifies the DOM, so take care to preserve event listeners and avoid breaking the document structure.
if (!CSS.highlights) {
// Walk text nodes and wrap matches in <mark>, preserving structure.
const walker = document.createTreeWalker(article, NodeFilter.SHOW_TEXT);
const nodes = [];
for (let n = walker.nextNode(); n; n = walker.nextNode()) nodes.push(n);
const term = searchTerm.toLowerCase();
for (const textNode of nodes) {
const text = textNode.textContent;
let pos = text.toLowerCase().indexOf(term);
if (pos === -1) continue;
const frag = document.createDocumentFragment();
let last = 0;
while (pos !== -1) {
frag.append(text.slice(last, pos));
const mark = document.createElement("mark");
// textContent assignment avoids HTML injection.
mark.textContent = text.slice(pos, pos + term.length);
frag.append(mark);
last = pos + term.length;
pos = text.toLowerCase().indexOf(term, last);
}
frag.append(text.slice(last));
textNode.replaceWith(frag);
}
}Animated Select Picker
The customizable select API offers a declarative, CSS-driven way to animate <select> elements and their dropdown pickers. By combining appearance: base-select with modern CSS animation techniques—such as @starting-style and the allow-discrete transition behavior—you can create fluid, premium UI transitions for top-layer elements without relying on heavy JavaScript libraries.
Previously, animating native select dropdowns was impossible because their UI was rendered outside the accessible viewport constraints. With appearance: base-select, the picker becomes styleable and animatable like any other page element.
How to Implement
To implement an animated select picker:
1. Opt-in to customization: Apply appearance: base-select to both the <select> element and the ::picker(select) pseudo-element. 2. Enable auto-sizing transitions (Optional): Define interpolate-size: allow-keywords (usually on :root) to allow the browser to transition between discrete metric values like height: auto and height: 0. 3. Animate the top-layer container: Apply standard entry/exit styles to ::picker(select). To make sure the opacity transition works when moving between display: none and display: block, you must use transition-behavior: allow-discrete (often written inline as transition: display 0.3s allow-discrete). 4. Hook into the opening state with `@starting-style`: Use @starting-style to define the baseline styles the browser should compute before the transition begins. For example, if you want it to fade in, set the opacity to 0 inside the @starting-style block. 5. Rotate the icon: Use pseudo-element focus or active selectors like :open::picker-icon to apply transitions (such as rotation or translation) to the arrow indicator.
Example Code: Smooth Select Scale and Fade
The following example demonstrates a custom select styled with standard page animations for the picker container.
<!-- Always use a <label> linked via 'for' to the select for accessibility -->
<label for="theme-select">Visual Theme</label>
<select id="theme-select" class="animated-select" name="theme">
<!-- The <button> inside <select> becomes the visible trigger when appearance: base-select is used -->
<button>
<!-- <selectedcontent> automatically displays the content of the chosen <option> -->
<selectedcontent></selectedcontent>
</button>
<option value="system">
<!-- MANDATORY: Decorative inline SVGs MUST set aria-hidden="true" to prevent redundant screen reader announcement -->
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
<line x1="8" y1="21" x2="16" y2="21"></line>
<line x1="12" y1="17" x2="12" y2="21"></line>
</svg>
System Default
</option>
<option value="light">
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
Light UI
</option>
</select>/* Opt-in to customizable select */
.animated-select,
.animated-select::picker(select) {
appearance: base-select;
}
/* Enable auto-keyword transitions (usually set globally at :root) */
:root {
interpolate-size: allow-keywords;
}
/* Style the visible trigger and icon rotation */
.animated-select {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 0.875rem 1rem;
font-size: 1rem;
border-radius: 8px;
cursor: pointer;
transition: border-color 0.2s ease;
}
.animated-select::picker-icon {
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.animated-select:open::picker-icon {
transform: rotate(180deg);
}
/*
* The Picker Container
* Uses top-layer animations with `allow-discrete` visibility hooks
*/
.animated-select::picker(select) {
background: white;
border-radius: 12px;
box-shadow: 0 10px 25px -3px rgba(0,0,0,0.1);
padding: 0.5rem;
margin-top: 0.25rem;
width: anchor-size(width);
overflow: hidden;
/* The crucial transition setting for popover animations */
transition:
display 0.4s allow-discrete,
overlay 0.4s allow-discrete,
opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1),
height 0.4s cubic-bezier(0.16, 1, 0.3, 1);
opacity: 0;
height: 0;
}
/* Open State */
.animated-select:open::picker(select) {
opacity: 1;
height: auto;
}
/* @starting-style to hook the transition on initial popover open */
@starting-style {
.animated-select:open::picker(select) {
opacity: 0;
height: 0;
}
}
/* Support for SVG inside Options and Selected Content */
.animated-select option svg,
.animated-select selectedcontent svg {
flex-shrink: 0; /* Prevent icons from shrinking */
width: 1.25rem;
height: 1.25rem;
}
/* MANDATORY: Provide multiple indicators (e.g. bold font and distinct background) for the checked state to avoid color-only state communication */
.animated-select option:checked {
font-weight: 700;
background-color: #f1f5f9;
}
/* Ensure copy-paste safety for users with motion sensitivities */
@media (prefers-reduced-motion: reduce) {
.animated-select::picker(select),
.animated-select::picker-icon {
transition: none !important;
}
}Strategic Implementation & Best Practices
- DO use
@starting-stylewhen you need animations to trigger exactly when an element transitions fromdisplay: noneto visible. - DO NOT use ad-hoc scroll locking. Top-layer elements managed by ‘base-select’ should allow natural backdrop dismiss behaviors.
- DO verify reduced motion preferences. Always wrap animation constraints in a
prefers-reduced-motionmedia query to ensure accessible environments for those affected by motion sickness. - DO test layout behavior. Setting
appearance: base-selectremoves the default browser behavior of sizing the select based on its longest option width. You may need to set a fixed width or use flex/grid constraints to prevent layout shifts. - DO ensure your
<select>has anameattribute and an associated<label>. This ensures that even with a custom UI, the component remains accessible to screen readers and works correctly with standard form submissions.
Fallback strategies
Fallbacks & browser support for Customizable <select>
Customizable <select> has limited availability. Supported by: Chrome 135 (Apr 2025) and Edge 135 (Apr 2025). Unsupported in: Firefox and Safari.
For browsers that do not yet support appearance: base-select, the <select> element degrades gracefully to a standard operating system dropdown.
- Non-Text Content Ignored: Older browsers strip HTML tags (like
<svg>or<div>) inside<option>tags and render only the text nodes. Ensure the text content of the<option>is readable and meaningful on its own. - HTML Structure Handling: Standard parsers may ignore the
<button>and<selectedcontent>tags inside<select>or treat them as invalid. No heavy JavaScript polyfills are strictly required for progressive enhancement if you view standard text as a readable fallback.
document.addEventListener("DOMContentLoaded", () => {
// Check if browser supports base-select value
if (!CSS.supports("appearance", "base-select")) {
// Custom select overrides are not supported natively.
}
});Build an address form that follows best practice
Create a form that makes it as easy as possible for users to enter address data on desktop and mobile. Ensure the form makes the most of built-in browser features for autofill, validation and data entry constraints.
How to implement
Outlined below are the most important guidelines for building successful address forms.
Use meaningful, valid HTML
Make the most of the elements and attributes built for creating forms:
-
<form>,<input>,<label>, and<button> -
type,autocomplete, andinputmode
These enable built-in browser functionality, improve accessibility, and add meaning to markup.
Use the <label> element to label form fields for data entry
To label an <input>, <select>, or <textarea>, use a <label>. Associate a label with an input by giving the label's for attribute the same value as the input's id.
Make the most of HTML attributes
Make it easy for users to enter data, by using the appropriate <input> element <type> attribute to provide the right keyboard on mobile and enable basic built-in validation by the browser.
Always use type="email" for email addresses and type="tel" for phone numbers.
<!-- type="email"/"tel" gives mobile users the right keyboard and enables built-in validation -->
<input type="email" id="email" name="email" autocomplete="email" required>
<input type="tel" id="phone" name="phone" autocomplete="tel">Every <input>, <select>, and <textarea> element SHOULD have an appropriate autocomplete attribute, to improve accessibility and help users avoid re-entering data.
Make buttons helpful
Use <button> for buttons. You can also use <input type="submit">, but don't use a div or some other random element acting as a button. Button elements provide accessible behaviour, built-in form submission functionality, and can easily be styled.
Give each form submit button a value that says what it does. For each step towards checkout, use a descriptive call-to-action that shows progress and makes the next step obvious. For example, label the submit button on your delivery address form Proceed to Payment rather than Continue or Save.
Use a single name input where possible
Allow your users to enter their name using a single input, unless you have a good reason for separately storing given names, family names, honorifics, or other name parts. Using a single name input makes forms less complex, enables cut-and-paste, and makes autofill simpler.
Allow international names. For validation, avoid using regular expressions that only match Latin characters. Latin-only excludes users with names or addresses that include characters that aren't in the Latin alphabet. Allow Unicode letter matching instead—and ensure your backend supports Unicode securely as both input and output. Unicode in regular expressions is well supported by modern browsers.
Allow for a variety of address formats
When building an address form, be aware of the variety of address formats, even within a single country. Do not make assumptions about "normal" addresses.
Use a single <textarea> element for the street address if possible.
<!-- textarea handles multi-line international address formats that split inputs can't accommodate -->
<textarea id="address" name="address" autocomplete="street-address" required></textarea>This is the most flexible option for a variety of local and international address formats.
Help save users from accidentally missing data fields
Add the required attribute to mandatory fields.
<input type="text" id="city" name="city" autocomplete="address-level2" required>Fallback strategies
:autofill has limited availability. Supported by: Chrome 110 (Feb 2023), Edge 110 (Feb 2023), and Safari 15 (Sep 2021). Unsupported in: Firefox.
Autofill is a progressive enhancement. In browsers that do not support autofill, users will simply need to manually enter their address details. The semantic HTML constraints (such as type, inputmode, and required) will still function appropriately as standard form validation.
Use the CSS :autofill pseudo-class to highlight form fields that have been autofilled by the browser and not edited by the user
Use the CSS :autofill to highlight fields that have (or have not been) autofilled, to help guide the user to successful form completion.
How to implement
To highlight a form field that has been autofilled by the browser (and not edited by the user) add a selector to your CSS using the :autofill class. This can be used for an <input>, <select>, or <textarea> element.
When styling autofilled states, you must adhere to accessibility best practices:
- Multiple State Indicators: Do not rely on border color alone to indicate the autofilled state. Use multiple indicators such as border thickness and custom background shading to ensure the state is perceivable.
- Preserve Focus Indicators: Never remove focus outlines (
outline: none) without providing a clear, high-contrast replacement for keyboard users.
The following example uses :autofill to set a custom border and background, along with explicit focus styles:
input:autofill,
input:-webkit-autofill {
/* Multiple indicators: use both a distinct border and background color via box-shadow to avoid color-only state */
border: 2px solid #2e7d32;
box-shadow: 0 0 0 100vmax #e8f5e9 inset;
}
/* MANDATORY: Always provide an explicit focus indicator when styling autofilled states */
input:autofill:focus-visible,
input:-webkit-autofill:focus-visible {
outline: 3px solid #000;
outline-offset: 2px;
}As shown in this example, the box-shadow property is used to customize the background, since background-color cannot be overridden directly on autofilled fields.
Use the correct CSS pseudo-class name
Do not use :auto-fill: this is incorrect.
MANDATORY: Use :autofill as this is the correct pseudo-class name.
Fallback strategies
:autofill has limited availability. Supported by: Chrome 110 (Feb 2023), Edge 110 (Feb 2023), and Safari 15 (Sep 2021). Unsupported in: Firefox.
The :autofill pseudo-class is a progressive enhancement. In browsers that do not support it, the form will still function normally, but the inputs will simply not receive the custom autofill highlighting. Users will still be able to successfully complete the form. No additional JavaScript fallback should be used.
Branded Select Styling
The customizable select API offers a declarative, CSS-driven way to style <select> elements to perfectly match your brand's design system. By opting into appearance: base-select, you gain access to the internal shadow DOM of the select element, allowing you to style the button, the options picker list, the arrow icon, and the checkmark indicator using standard CSS properties.
Previously, achieving a fully branded select required rebuilding the control from scratch with JavaScript, which often broke accessibility, keyboard navigation, and native form integration. With appearance: base-select, you get a custom look while the browser handles focus management, top-layer rendering, and accessibility bindings.
How to Implement
To implement branded select styling:
1. Opt-in to customization: Apply appearance: base-select to both the <select> element and the ::picker(select) pseudo-element (which targets the drop-down list of options). 2. Structure the custom button (Optional): Define a <button> element directly inside the <select> to replace the default trigger. Use the <selectedcontent> element inside this button to represent the text or content of the currently selected option. 3. Style the Picker List: Use the ::picker(select) pseudo-element to apply typography, background colors, borders, and shadows to the dropdown list. The browser renders this in the top-layer, making z-index conflicts a thing of the past. 4. Style Internal Icons:
- Use
select::picker-iconto style or replace the arrow icon. - Use
option::checkmarkto style the checkmark indicator next to the active option.
5. Style Options: Apply styles to <option> elements for hover states, padding, and layout.
Example Code: Branded Courier Select
The following example demonstrates a custom select styled with a monospace font and dashed borders to match a specific "parcel" brand aesthetic.
/* Enable customization for the select and its picker */
.brand-select,
.brand-select::picker(select) {
appearance: base-select;
}
/* Style the visible trigger button */
.brand-select {
font-family: 'Courier New', monospace;
background-color: #fffaf0;
color: #8b4513;
border: 2px dashed #8b4513;
border-radius: 4px;
padding: 0.75rem;
font-size: 1rem;
cursor: pointer;
}
/* Style the dropdown options list */
.brand-select::picker(select) {
font-family: 'Courier New', monospace;
background-color: #fffaf0;
border: 2px dashed #8b4513;
border-radius: 4px;
padding: 0.5rem;
}
/* Customize internal part colors to match text */
.brand-select::picker-icon {
color: #8b4513;
}
.brand-select option::checkmark {
color: #8b4513;
}
/* Style individual options and hover effects */
.brand-select option {
padding: 0.5rem;
border-radius: 4px;
color: #8b4513;
cursor: pointer;
}
.brand-select option:hover {
background-color: #fdf5e6;
}<label for="preferences">Select shipping preference</label>
<select class="brand-select" id="preferences" name="preferences">
<button>
<selectedcontent></selectedcontent>
</button>
<option value="standard">Standard Shipping</option>
<option value="express" selected>Express Shipping</option>
<option value="overnight">Overnight Delivery</option>
</select>Strategic Implementation & Best Practices
- DO use
appearance: base-selectwhen your design system requires high-fidelity, visual consistency across all form controls that cannot be achieved with standard cross-browser select overrides. - DO NOT use this if you rely on the operating system's native picker experience (e.g., the standard scroll wheel picker on iOS devices). Opting into
base-selectopts out of native mobile UI controls in favor of web-rendered top-layer menus. - DO verify that color contrast meets WCAG standards. The customizable picker allows you to set ad-hoc colors, but you are responsible for ensuring text remains legible against custom backgrounds.
- DO test layout behavior. Setting
appearance: base-selectremoves the default browser behavior of sizing the select based on its longest option width. You may need to set a fixed width or use flex/grid constraints to prevent layout shifts. - DO ensure your
<select>has anameattribute and an associated<label>. This ensures that even with a custom UI, the component remains accessible to screen readers and works correctly with standard form submissions.
Fallback strategies
Fallbacks & browser support for Customizable <select>
Customizable <select> has limited availability. Supported by: Chrome 135 (Apr 2025) and Edge 135 (Apr 2025). Unsupported in: Firefox and Safari.
For browsers that do not yet support appearance: base-select, the <select> element degrades gracefully to a standard operating system dropdown.
- Non-Text Content Ignored: Older browsers strip HTML tags (like
<svg>or<div>) inside<option>tags and render only the text nodes. Ensure the text content of the<option>is readable and meaningful on its own. - HTML Structure Handling: Standard parsers may ignore the
<button>and<selectedcontent>tags inside<select>or treat them as invalid. No heavy JavaScript polyfills are strictly required for progressive enhancement if you view standard text as a readable fallback.
document.addEventListener("DOMContentLoaded", () => {
// Check if browser supports base-select value
if (!CSS.supports("appearance", "base-select")) {
// Custom select overrides are not supported natively.
}
});Related skills
How it compares
Choose modern-web-guidance over static frontend skills when answers must reflect APIs and CWV guidance newer than model training cutoffs.
FAQ
What does modern-web-guidance do?
# Accessibility Coding Guidelines This guide provides actionable DOs and DON'Ts for AI coding agents to ensure web applications are accessible to all users, including those using assistive technologies. Keep these princi
When should I use modern-web-guidance?
# Accessibility Coding Guidelines This guide provides actionable DOs and DON'Ts for AI coding agents to ensure web applications are accessible to all users, including those using assistive technologie
Is modern-web-guidance safe to install?
Review the Security Audits panel on this page before installing in production.