Published: August 14, 2025
As the Google I/O event season comes to a close, this post recaps the top highlights for CSS and Web UI shared at our events this year.
Incredibly powerful features that developers once only dreamed of have arrived in browsers and are reaching cross-browser compatibility faster than ever before. However, despite this progress, some of the most common UI patterns remain surprisingly difficult to implement correctly. You often have to rely on JavaScript frameworks, complex CSS tricks, and mountains of custom code to build components that feel like they should be simpler.
The Chrome team, in collaboration with other browser vendors, standards bodies like the CSSWG and WHATWG, and community groups like Open UI, is focusing on making these fundamental UI patterns genuinely straightforward to implement.
Customizable select menus
The <select>
element is essential for forms, but its internal structure has historically been shielded by the browser, making consistent and comprehensive CSS styling nearly impossible. To build a better <select>
requires an understanding of its building blocks—the Popover API and the CSS Anchor Positioning API.
The Popover API: Now in Baseline
A custom drop-down needs a floating box of options that appears above all other UI elements, is trivial to dismiss, and manages focus correctly. The Popover API handles all of this, and as of this year, it has reached Baseline Newly available status, meaning it's stable in every major browser.
Creating a popover needs two parts: a trigger element (like a <button>
) and the popover element itself. Connect them by giving the popover an id
and the [popover]
attribute, and then reference that id
in the button's [popovertarget]
attribute.
The Popover API manages the element's entire lifecycle, providing:
- Top-layer rendering: No more fighting with z-index.
- Optional light dismiss capabilities: It closes when a user clicks outside the popover area.
- Automatic focus management: The browser handles tab navigation into and out of the popover.
- Accessible bindings: The underlying interaction model is handled natively.
The <dialog>
element gets an upgrade
While popover is powerful, it's not always the right choice. For example, in page-blocking interactions that require user feedback, a modal <dialog>
is more appropriate.
Historically, <dialog>
lacked some of [popover]
's conveniences, but that is changing. With the new closedby="any"
attribute, modal dialogs now support light dismiss functionality, closing when the user clicks outside or presses the Escape key.
Additionally, command invokers ([command]
and [commandfor]
) provide a declarative, JavaScript-free way to connect a button to an action, such as opening a dialog with command="show-modal"
.
<dialog> Element + closedby=any + command invokers |
[popover] attribute |
|
---|---|---|
Primary Use | Modal Interaction (user agreements, walk-throughs, etc.) | Transient UI (menus, tooltips, cards, toast alerts) |
Light Dismiss-able | Yes | Yes |
Traps Focus | Yes | No |
Inerts page | Yes | No |
Declarative Activation | Yes | Yes |
Implementation | Element | Attribute |
Renders in Top Layer | Yes | Yes |
Fully Styleable | Yes | Yes |
CSS Anchor Positioning
Once a popover appears, it needs to be positioned relative to the element that opened it. Manually calculating this with JavaScript is fragile and can hurt performance.
From Chrome 125, you can use the CSS Anchor Positioning API. This new capability declaratively tethers one element to another, automatically handling repositioning when it gets close to the edge of the screen. This feature is part of Interop 2025, a cross-browser initiative to land highly-requested features, meaning we can expect it to be in all major browsers by the end of 2025.

While you can explicitly link elements with the anchor-name
and position-anchor
properties, an update in the specification and in Chrome 133 creates an implicit anchor relationship between a <popover>
and its invoking <button>
. This greatly simplifies the code, and means that you can now position the popover with a single line of CSS, such as: position-area: bottom span-left
.
The anchor tool from chrome.dev shows you how to use position-area
to get the placement that you may want:
Take it a step further and have the browser reposition your anchors, preventing them from going off-screen, by defining fallbacks with position-try-fallbacks
. The following demo shows a popover that uses this property for built-in repositioning logic:
A truly customizable <select>
With those building blocks in place in earlier versions, web-native styling for <select>
elements has finally arrived in Chrome 134. This includes a new appearance
property, new pseudo-elements, and the <selectedcontent>
element.
To unlock customization, apply appearance: base-select;
to the <select>
element and its new ::picker(select)
pseudo-element, which targets the drop-down list of options. This exposes new internal parts for styling, including:
<selectedcontent>
: Represents the content of the selected option shown in the button.::picker-icon
: The drop-down arrow icon<option>:checked
and::checkmark
: For styling the selected option and its checkmark indicator

This allows for rich content within options and fine-grained control over the display. For example, you can show an icon and subtitle in the options list but hide them in the closed state using display: none
within selectedcontent
.
The best part is, this API can be progressively enhanced. In browsers that don't support these features, users will still get a functional web-native select. You get a custom look while preserving the built-in accessibility, keyboard navigation, and form integration of the web-native select element.
Carousels
Carousels are everywhere on the web, and not just in hero sections. This includes horizontally scrollable content in tight layouts like an app store UI. But building carousels on the web is still a struggle, with many considerations like state management, scroll-jank, interactivity, and accessibility. But if you think about it, carousels are essentially fancy scroll areas with extra UI affordances.
Getting started with scrollers
To build a carousel, you start with a list of items that overflows their container. To hide the horizontal scrollbar while keeping the content scrollable, use scrollbar-width: none
. Additionally, to make the scroller feel "snappy," apply scroll-snap-type
and scroll-snap-align
, which makes sure that items snap into place as the user scrolls.
Previous and next with a ::scroll-button
The new ::scroll-button()
pseudo-element, which landed in Chrome 135, tells the browser to generate stateful, accessible "next" and "previous" buttons. The browser automatically handles the correct ARIA roles, tab order, and even disables the buttons when you reach the start or end—all without any added JavaScript.
To initiate the scroll buttons, give them content and an accessible label, like so:
.carousel {
&::scroll-button(left) {
content: "⬅" / "Scroll Previous";
}
&::scroll-button(right) {
content: "⮕" / "Scroll Next";
}
}

Style these buttons and position them relative to their parent carousel with CSS anchor positioning, which is the recommended approach to doing so.
Direct Navigation with ::scroll-marker
For dot indicators or thumbnails, the ::scroll-marker
and ::scroll-marker-group
pseudo-elements associate navigation markers directly with the items in your scroll container. The browser treats the group like a tablist
and handles keyboard navigation.
Similar to scroll buttons, initiate the scroll markers by opting in with the content
property, and provide an accessible label. In the following example, a data attribute is used to set the label for the scroll marker. Additionally, position the scroll markets in the ::scroll-marker-group
using the scroll-marker-group
property. Finally, style the active marker using the new :target-current
pseudo-class. Here's an example of what this might look like for a basic carousel:
.carousel {
scroll-marker-group: after;
> li::scroll-marker {
content: ''/ attr(data-name);
}
> li::scroll-marker:target-current {
background: blue;
}
}

Scroll-state queries
New scroll-related CSS features let you create more dynamic and interactive carousels. The scroll-state query is a new media query that applies based on a scroller's state. There are three different types of scroll-state queries, which can be accessed using scroll-state()
in an @container
statement. They are:
scroll-state(snapped)
: Matches when an element is in the "snapped" position. In carousels, that's when it's the one snapped in the center of the carousel.scroll-state(stuck)
: Style an element, like a header, when its parent becomes sticky.scroll-state(scrollable)
: Add visual indicators, like a fade, to show there is more content to scroll to.
Bringing it all together
A combination of new CSS carousel primitives, scroll-state queries, and anchor positioning, make it easier for you to build customized and interactive carousels. Take it a step further by incorporating scroll-driven animations to link animations directly to the scroll position, creating performant effects like having items scale and fade as they scroll into view. These animations run off the main thread, enabling a silky-smooth experience.
This interactive carousel combines scroll-state()
queries, ::scroll-button
, ::scroll-marker
, CSS anchor positioning, and :target-current
.
Additionally, you can use a new property called interactivity
to help users focus on the active content. interactivity: inert
allows the user to apply inertness using CSS, making off-screen carousel items unfocusable and removing them from the accessibility tree.
Learn more about CSS Carousels.
Interactive hovercards
Hovercards—the rich popups that appear when you mouse over a username or link—are incredibly useful but notoriously difficult to build correctly. Getting the delays, event handling, and multi-device support right can take a dedicated team months. But we're working on a new declarative solution that should solve this problem once and for all.
Interest-triggered popovers with [interestfor]
The core magic behind declarative hovercards is the[interestfor]
attribute. This upcoming feature brings the power of popovers but triggers them based on user "interest." For example, user interest on a pointer device would be a pointer-hover, tab navigation with a keyboard, or a long-press or tap on touch screens. The mobile interaction is yet to be resolved.
To convert a click-based popover into an interest-based one, build an invoking element, which can be a <button>
or an <a>
, and give it an [interestfor]
attribute that equals the id
of the [popover]
element. It looks like this in HTML:
<button interestfor="profile-callout">
...
</button>
<div id="profile-callout" popover>
...
</div>
The browser handles all the complex event logic, including:
- Enter and exit events: Hover-entry on fine pointer devices, tab navigation with keyboard, long-press or touch on coarse pointer devices.
- Event delays: Control the entry and exit delays with a single CSS property.
This feature supports other popover features like top-layer support, where the popover renders on a new layer above the rest of the DOM tree. And the semantic component bindings and underlying accessibility tree model are handled natively.
Styling interest invokers
Interest invokers include some new capabilities. One of which is the ability to control entry and exit delays using a CSS property: interest-target-delay
. The other is the ability to style the invoking element based on if it has interest or not, using the :has-interest
pseudo-class.
[interesttarget] {
interest-target-delay: 0s 1s;
&:has-interest {
background: yellow;
}
}
popover="hint"
and multi-functional UI
A key piece of the puzzle for interest invokers is a new popover type: popover="hint"
. The primary differentiator from other popovers is that a hint popover does not close other popovers when it opens. This is perfect for tooltips or preview cards that should appear without dismissing an already-open menu or chat window.
Browser Support
popover=auto | popover=manual | popover=hint | |
---|---|---|---|
Light dismiss (via click-away or esc key) | Yes | No | Yes |
Closes other popover=auto elements when opened | Yes | No | No |
Closes other popover=hint elements when opened | Yes | No | Yes |
Closes other popover=manual elements when opened | No | No | No |
Can open and close popover with JS (showPopover() or hidePopover() ) | Yes | Yes | Yes |
Default focus management for next tab stop | Yes | Yes | Yes |
Can hide or toggle with popovertargetaction | Yes | Yes | Yes |
Can open within parent popover to keep parent open | Yes | Yes | Yes |
This lets you declaratively build powerful, multi-functional UI. A single button can now have an auto popover using popovertarget
for its primary click action (like opening a notifications panel) and an interest invoked hint popover to show a helpful tooltip on pointer-hover.
The Future is Declarative
The features covered here represent a fundamental shift toward a more powerful and declarative web platform. By letting the browser handle the complex, repetitive work of state management and accessibility, we can remove mountains of JavaScript, improve performance, and focus on what we do best: creating innovative and engaging user experiences. This is truly a golden age for web UI, and it's only just beginning. Follow along right here as we work on building a more powerful web, made easier.
Further resources: