CSS tabs finally tell a screen reader they are tabs
The markers CSS generates for a scroll-snap strip can now declare themselves a tablist, so one scroller and a few lines of CSS make a whole tabs component. Firefox 157, out today, adds a cleaner way to test for new at-rules.
Chrome 154
stable 22 Sep
Edge 154
24 Sep
Firefox 157
29 Sep, today
Carousel markers
One scroller, three modes
A plain scroll-snap strip
Panels side by side, one visible at a time. It swipes in every browser, which makes it a safe base for everything below.
scroll-snap-type: x mandatory;
Links mode, the default
The marker group becomes a navigation list and each marker a link, like a row of in-page anchors. Every marker is its own tab stop.
scroll-marker-group: after;
Tabs mode, new in 154
The group becomes a tablist, markers become tabs, panels become tabpanels. Pressing Tab from the active marker lands inside its panel.
scroll-marker-group: before tabs;
Live. In Chrome and Edge 154 the tab strip above is generated by CSS in tabs mode; elsewhere it is a row of anchor links over the same scroller.
Today's art direction
The page this issue is dressed as.
The Settings Pane
A SaaS settings console on cool grey, where every news item is a setting with its real value beside it.
A settings console is the page where designers build tabs and in-page navigation most often, so the issue borrows its bones: an app bar, a sticky section list, grouped rows with a label column, and a value well for each setting. The spine on the left carries the masthead the way a product carries its workspace name. Three two-colour linocut prints stand in for the three states of one scroller.
settings group
label column
value well
section nav
spine masthead
tablist
linocut sequence
Technique
What changed in CSS this week.
Scroll markers learn the tabs pattern
Chrome 154, stable 22 September
The scroll-marker-group property now takes two modes. In tabs mode the generated group gets the tablist role, each marker a tab, and each originating panel a tabpanel, per the Chrome 154 release notes.
Edge 154 shipped the same feature on 24 September, and its notes add the part a tabs script usually gets wrong: inactive panels drop out of the accessibility tree.
Scroller
scroll-marker-group:
before tabs;
Underlines you can pull in from the ends
Chrome 154, stable 22 September
Text-decoration-inset reached stable Chrome, taking lengths, percentages, and separate start and end offsets. The release notes pitch it for underline reveal effects that used to need a background gradient or an extra element.
Link
text-decoration-inset:
4px;
Tooling
Embeds and overlays that behave.
Iframes that grow to fit their document
Chrome and Edge 154
The new frame-sizing property sizes an iframe in the parent page to the embedded document, so a booking form or newsletter embed stops showing its own scrollbar.
Iframe
frame-sizing:
content-height;
Popovers stop closing when you scroll
Chrome and Edge 154
Light dismiss for popovers and dialogs now fires on click rather than a pointerdown and pointerup pair. A touchscreen scroll gesture or a right-click no longer closes the menu the reader was using.
HTML
<div popover>
From the release notes
Content from inactive tabs is explicitly hidden from the accessibility tree.
Microsoft Edge 154 web platform release notes
Workflow
Firefox 157, released today.
Test for an at-rule before you build on it
Firefox 157, 29 September
The at-rule() function inside a supports query checks whether the browser understands an at-rule such as scope, and it also works in the supports condition of an import, per the Firefox 157 notes. A design system can now ship scoped styles behind one honest test.
Stylesheet
@supports
at-rule(@scope) {
…
}
Chain the scroll, skip the bounce
Firefox 157, 29 September
Overscroll-behavior gains a chain value: scrolling passes to the parent scroller at the edge, without the browser's bounce. Useful for a drawer or a settings pane nested inside a scrolling page.
Drawer
overscroll-behavior:
chain;
Borrow this pattern
Tabs that degrade into a carousel.
Build tabs as a scroller first
What
Put each tab panel in a horizontal scroll-snap strip, then let CSS generate the tab strip in tabs mode. No script, no roles written by hand.
Where
Settings pages, pricing plan switchers, product detail tabs, any place a client asks for tabs.
Keep it honest
Browsers without the feature get a swipeable strip and a row of anchor links, so write the fallback nav in HTML and hide it only when the markers are supported.
Export this page as a prompt for your own settings console.
Works in Claude Code, Claude Design, Beaver Builder AI, v0, Lovable, Framer AI.
Build a SaaS account settings page, the kind a freelancer ships for a client's web app: a real settings console, not a poster about one.
Structure: a 60px sticky app bar across the top (square vermilion product mark at the far left, product name, three text links at right). Below it a three-column frame: a 60px slate spine on the far left holding the workspace name, date, and plan as vertical mono text; a 232px sticky sidebar card titled "Settings" with an ordered list of section anchor links (counts in small mono at the right of each link); and a main column up to 1000px wide.
Main column: a page header (small mono uppercase kicker in vermilion, a 17ch grotesk H1, a 60ch serif deck), then one raised demo panel, then settings groups. Each settings group is a two-column grid (1fr label column, 2.35fr content): the label column holds the group h2 and a one-line description; the content column holds a white rows card with hairline-separated rows. Each row is a two-column settings row: on the left a grotesk title, a small mono source line, and one or two serif paragraphs; in a fixed 250px slot on the right a tiny uppercase key over a value well (mono, pale grey fill, 6px radius) showing the real setting value or CSS. Beside the page header, a small card of release rows (name left, date right in mono).
Tabs: build the demo panel as a horizontal scroll-snap strip of panels (each panel a 3fr/2fr grid of an image and text). Use scroll-marker-group: before tabs on the strip and ::scroll-marker with content: attr(data-label) on each panel, underlined in vermilion on :target-current. Write a fallback row of anchor links in HTML and hide it inside @supports selector(::scroll-marker).
Palette: flat cool grey ground #DCE3E7; the slate spine carries the only pattern, a 60x40px SVG tile of a toggle-switch outline in pale grey at 13 percent; panels #F7F8F7; value wells #EDF1F3; ink #1B2836; slate #22344A for the spine and one inverted "Borrow this pattern" group; muted text #4E5B68; one accent, vermilion #C73A22, used for the product mark, kicker, active tab underline, and the copy button. Italic accent #8E3322 only for one pullquote.
Type: Familjen Grotesk 500/700 for UI, titles, and the H1; Source Serif 4 for body text; IBM Plex Mono for labels, values, and code.
Images: two-colour linocut prints (vermilion and slate on off-white paper) as the tab panel images, one print per tab, one object centred per print, no readable text in any image.
Guardrails: body at least 18px with 1.65 line height and 60-75 character lines, never justified; WCAG AA contrast for every small label against its actual surface; radius 12px for cards, 6-8px for wells and buttons, no pill shapes; one soft shadow on the demo panel only; hover and focus states on every link and button at 150ms, and a prefers-reduced-motion guard; no fake toggles, fake search fields, gradients, neon, or glow; on mobile hide the sidebar, turn the spine into a horizontal strip, and stack every settings group.
Field note
A generated tabs component used to mean a script and a stack of hand-written roles an agent could get subtly wrong. With tabs mode the structure is the scroller and the semantics come from the browser, which leaves less for a prompt, or a person, to forget.