Shadow DOM vs. Modern CSS

Search within this presentation

Introduction: Why Modern CSS and Shadow DOM Don't Always Mix

The speaker opens with brief introductory remarks before establishing the core thesis: browser support metrics like Baseline and Can I Use only measure light DOM feature availability, not Shadow DOM compatibility. Shadow-specific selectors like :host introduce additional complexity that most documentation ignores. The goal of the talk is to equip developers to make informed, future-forward decisions when incorporating modern CSS features into web component-based projects.

CSS Cascade Fundamentals Inside Shadow DOM

The speaker provides an essential refresher on how styles cascade into the Shadow DOM, covering three key rules. Inheritable properties only apply to shadow elements when they come from ancestor elements outside the shadow root. Slotted content lives in the light DOM, giving global styles priority over shadow :slotted() rules unless !important is used. Custom properties are inheritable by default, allowing them to pierce the shadow boundary, which makes them a popular approach for web component style customization in design systems.

Color Scheme, Cascade Layers, and CSS Nesting Across the Shadow Boundary

Using a social card component as a running example, the speaker walks through three lightweight CSS features. The color-scheme property is inheritable, so the light-dark() function works seamlessly across the shadow boundary, enabling dark mode with minimal effort. Cascade layers do not cross shadow boundaries and must be treated separately per context, though they remain useful within a component for managing competing variant and state styles. CSS nesting is safe for shadow elements as long as :host selectors are avoided, and the speaker demonstrates grouping complex slotted and default image styles together with nested media queries.

Subgrid: Enabling Web Components to Participate in Application Grid Layouts

The speaker introduces Subgrid as the solution to the classic visual alignment problem that occurs when card titles wrap to different heights across a set of components. Subgrid can apply to Shadow DOM content when initialized on the host element, enabling web components to participate in an external application grid. The implementation requires setting grid display on the host, configuring row spanning, and having a nested wrapper element inherit the row definitions — producing shared row alignment across all card instances even though each is a separate web component.

Container Queries and the Subgrid–Containment Conflict

The speaker introduces container queries to handle responsive layout changes in the card component, noting that shadow DOM elements can query containers defined in the external light DOM context. Using :host as a convenient inline-size container allows the grid to rearrange and stack elements at narrow widths. However, a significant conflict is revealed: applying both subgrid and containment to the same element breaks subgrid propagation because containment cuts off layout inheritance as a necessary side effect of preventing style recalculation loops. The recommendation is to design components to opt into either subgrid or container query behavior depending on the visual priorities of their context.

Container Style Queries: Group Opt-In Behavior for Web Components

The speaker introduces container style queries as a powerful (though not yet Firefox-supported) approach for applying an alternate layout to a group of web components simultaneously without setting individual attributes on each one. Because custom property values are inheritable through the Shadow DOM, shadow styles can query custom property values set on light DOM ancestors. The speaker demonstrates applying a horizontal card layout variant to an entire card grid by setting a single custom attribute on the wrapper, and shows that additional container size queries can be nested inside the style query to handle responsive concerns within the alternate layout.

Popover API: Accessing the Browser Top Layer from Shadow DOM

The speaker explains why the Popover API is covered in a CSS talk: it solves the classic stacking context problem by promoting elements to the browser's guaranteed top layer, making z-index hacks unnecessary. For Shadow DOM, encapsulation is actually a benefit because shadow IDs cannot conflict with global IDs. The speaker walks through building a custom tooltip component that accepts popover attributes, making association with a trigger no different than using custom attributes, and introduces the hint popover type for hover-triggered behavior, which currently requires a small JavaScript polyfill until full browser support arrives.

Anchor Positioning: Placing Tooltips Relative to Their Triggers

The speaker introduces CSS anchor positioning to solve the problem of placing the detached popover tooltip relative to its trigger button, noting its presence in Chromium, Safari Technical Preview, and the newly announced Safari 26 beta. Key Shadow DOM constraints are outlined: you cannot anchor a global element to a shadow element, or anchor a shadow element to slotted content, but anchoring within the same shadow root and anchoring global elements to custom elements are both supported. Since anchor positioning styles must live in the same context as their anchor, the speaker presents two implementation options: including the CSS alongside the consuming component's styles, or applying it via the style attribute in the connected callback.

CSS Scope Overview and Talk Recap

The speaker briefly covers CSS @scope, noting it cannot cross the shadow boundary except through inheritance but may apply to slotted light DOM content and offers some utility for flattening specificity without affecting specificity scores. Resources for finding Shadow DOM-specific browser support data are mentioned, including test results on wpt.fyi. The talk closes with a recap emphasizing that modern CSS features are safe to use independently within either context, that most conflicts arise around :host selectors or when features like subgrid and containment interact, and that developers should experiment, file browser and CSS bugs, and share use cases.

Good morning, everyone. I really hope that you have had your coffee because we are gonna dig into some details. But I gotta say that yesterday during drinks, I learned something about Australians, which is as an American, thinking about Australia growing up, I was, like, really fascinated with the flora and the fauna that you have here. But last night, I learned that maybe I have a fun animal fact, which is that in The US, we have not only brownish red squirrels, but also gray squirrels and black squirrels.

So I don't know if that blew your mind or not. My small focus group was very impressed by that fact. Alright. So because we have such a time limit today, I do wanna set some expectations about what we're gonna talk about today. So I do, unfortunately, have to assume a little familiarity with the features we'll discuss. You're still gonna see what they can do, but we won't get to get into a lot of the details there.

Instead, we're gonna spend our time focusing on how modern CSS features work across the light versus shadow DOM boundary. I know that might be a horror term for some of you. I also say that this is not a talk about styling strategies for web components. And rather, the goal is to, if you are working with these technologies, to enable the you to make future forward decisions about incorporating these CSS features, with your team and your projects.

But why is this topic important? The first reason is when you hear the term browser support from something like Baseline or you go look it up on Can I Use, it's really only a measure of light DOM availability? In other words, does the feature technically pass enough tests to say it is supported in that browser?

But we have shadow specific selectors, for example, host, that introduce further support complexities that aren't gonna match your light DOM expectation. So consequently, we have conflicts that can be unexpected since most documentation and tutorials that you find about these modern CSS features are gonna skip the nuances of Shadow DOM implementation.

Not any fault of the author of those materials. They probably just don't have experience with them. What I want you to really understand, though, is that all of the features we'll discuss are workable if they're done independently, either within Shadow DOM or global context, but they may not cross that boundary between light and shadow styles or have more unique conflict points.

So that's what we're gonna focus on for each feature. Now to better understand the impact of modern CSS features in the Shadow DOM, we need to do a quick refresher on the basics of how styles cascade into the Shadow DOM. If you haven't ever been exposed to this before, it's gonna probably feel a little overwhelming.

It is a confusing topic, unfortunately. But if you know nothing else about Shadow DOM, you might have at least heard a rumor about this fact that it can be used to encapsulate styles. So that's true except for the fact that inheritable properties may apply to shadow content. And with the further caveat that inheritable properties apply to shadow elements only if they come from ancestors to the shadow element. So in this example, the shadow DOM paragraph is gaining its size from the shadow DOM style and inheriting the font family from the global body font style.

But the color that is attached to the paragraph selector is not applying because there's no paragraph as an ancestor to our Shadow DOM element. So it is only applying to the light DOM content paragraph. Slotted content is unique because it exists in the light DOM unless you're slotting in another web component.

So for styles, that's going to mean the outer or global context has priority over the inner context of the shadow DOM. And that means that your global style sheet rules will win even above specificity over shadow styles using the shadow specific selector of slotted.

The exception is when you use important, and in this case, the priority is inverted, and the slotted selector regains priority over the global styles. So in this revised example where we've introduced a slot and used a paragraph to populate that slot, the global paragraph style is still responsible for the color of the paragraph since it has that priority, over the slotted paragraph style that is included in the Shadow DOM. If we wanted the slotted styles to win, again, we would have to apply important.

It's also important to know that custom properties are inheritable by default, and therefore, they can cross into the Shadow DOM. This is also known as the ability to pierce the Shadow DOM boundary. And so they are a popular method for style customization for web components, especially from design systems and frameworks, such as in this example where this button component accepts custom properties for both the background and text colors. And the second instance has included a style to customize the background related property.

Okay. Now that those essentials are sorted out, let's move on. To see how the modern CSS features apply, we will be creating this social card component. It will need to handle four light and dark themes, basic responsive behaviors, and an alternate layout variant with its own responsive concerns. Additionally, it will need to play nice with the tooltip component.

We already have our foundational styles in place, but it's time to enable the dark mode colors. And luckily for us, this property pairing will do the trick super quick. Fortunately, the value of color scheme is inheritable, so the use of the light dark function in Shadow DOM will successfully select the right color. So we can set up our application to honor color scheme preferences at our global style sheet level.

And then in our Shadow DOM styles, we'll use light dark for every definition of a color based property, such as for the card's background. So with pretty low effort, we've enabled our card component to gracefully adapt between light and dark themes. Next, let's look at a tool meant to improve CSS architecture, which is cascade layers as applied using the at layer rule.

Now, unfortunately, cascade layers do not cross the shadow boundary beyond any applicable inheritance. What this means in practice is that you cannot make use of or modify the global layer order from within the shadow DOM, and you cannot append styles into global defined layers.

So as in this example, as revealed by DevTools, we have two instances of the layer named components. Due to the cascade rules that we learned, the global layer is winning for the slotted content, causing it to be read. This might go against your intention of trying to append shadow styles into that global layer, but instead, they're treated separately, and there's no impact on the cascade of these rules.

So with that, my recommendation is to expect to treat global versus shadow layers separately if you have this dual system. But good news is that for most components, custom elements, you're gonna have a pretty tight feature set. So usually layers are probably actually unneeded overhead, but they can potentially be a useful tool for managing complex components and especially competing states and variants, such as in this example, again, for a button component, where layers are used to ensure that the hover state style will win over the variant styles.

Using layers, we're able to use a very simple selector instead of trying to increase specificity to match or exceed the specificity of the variant selector. This is useful outside of Shadow DOM as well. Now for a feature I found very useful across light versus shadow DOM stacks, and especially including for our card component, which is native CSS nesting. And there are some issues with using nesting, but they are specifically currently around using the host selectors.

So if you're not using host selectors, you can use nesting no problem. So I wanted to include details for those that are actually interested in this, but I want your takeaway to be that nesting is safe for selecting elements inside contained wholly within the Shadow DOM. So in other words, if you wrap your shadow elements with a containing element rather than style the host itself, you'll be completely safe using nesting. Here's an example of using nesting to handle the card's media area. There's complexity here because we want to handle for both the possibility of slotted content as well as the use of our default image that's provided within the Shadow DOM.

So using nesting, I was able to keep all those styles really clearly grouped together, including the, whoops, the nested media query for dark mode adjustments. Alright. Those are the lightweight topics. So let's get into something a little bit inter more interesting with Subgrid. Usually, subgrid is desired to fix a classic layout problem, which is when designers want to align things visually.

And prior to subgrid being available, we either had to explain to designers that it wasn't possible or use some kind of hack around the issue like fixed heights or truncation. And in our cards, the alignment mismatch is noticeable when the title begins to wrap. The good news on the support front is that Subgrid can apply to Shadow DOM content, when it's initialized on the host. And then from there, you can inherit it if needed to apply to a containing element within the Shadow DOM.

And this means that use of Subgrid enables your web components to participate in an application grid layout. There are a couple steps of using subgrid with our card component to enable the alignment that we're ultimately after. First, we need a grid parent for the components. And note that this is an external div. This is living in global context, and it's serving as the container for all of our card elements on the page.

Second, our component that we want to participate in subgrid has to itself be a grid. So in this case, we are using a nested container for the main card layout rather than using the host, which will become important shortly. But we also need to know how many of either columns or rows we want to make use of for the parent grid.

In this case, we are interested in alignment across rows, and we can see that we have three rows for the card layout grid. Our main card layout is on a nested element inside the shadow DOM, and we need that nested element to behave as if it is a direct child of the parent grid.

So we will force inheritance to get that behavior. This means we'll first set host up with grid display, then direct its child rows to use subgrid, and then direct the host element itself on how to live within that parent grid. Where this shorthand is saying, the starting row is auto depending on placement within the DOM.

And wherever that is, it should span three rows. Finally, on the nested wrapper for our card, we set up grid display and inherit those row definitions. With that all in place, when we select the parent grid layout, we see how the overlay exposes the, shared rows across all of our card components, resulting in the fix for the visual alignment for the content even though each card is a separate web component.

Alright. So our base component's looking pretty good, but it's time to handle some of the responsive concerns. And for those, we'll use container queries. To see why these will be helpful, here's our current card in a larger space where everything fits comfortably. But when it starts to squeeze, having those action buttons next to the title really, really gets narrow, and it's at risk of causing overflow. So we're going to employ container queries to adjust the underlying grid template and stack those action buttons above the title in a narrower space. The awesome news about container queries in Shadow DOM is that you have basic cross browser access to global containers, meaning you can query containers external to the Shadow DOM and modify shadow element styles just like any other container query.

So here's an example of doing just that. As a reminder, for use of container queries in general, a container query cannot style the queried container. And so for our cards purposes, we'll make use of host as a convenient container element because it's always going to match the inline space available for our card.

I do have to add a warning here to not take this little bit of info and go out and make a global rule across your web component system to have host automatically be a designated container. Because in certain context, the elements that are containers can collapse a layout or cause other unexpected results.

So my advice is to always make container behavior opt in in some way for your components and ensure that they work reasonably well without requiring that container query provided behavior. Usually, I treat container queries similar to the olden days of media queries where we do a small space first and build up from there.

So our first step to incorporate is to update our host styles and add the container query rule. And we're going to designate a container named card with the inline size containment. Then we provide a container query for our card element, rearrange the grid template, stack the elements. Easy peasy. Right? But here's the result.

What is happening here? Unfortunately, we've run into a conflict point between subgrid and container queries, and this, conflict is not exclusive to Shadow DOM, but happens anytime and anywhere You try to apply both subgrid and containment to the same element. The reason why is that containment cuts off the propagation of subgrid, and that's a side effect of containing the elements layout.

Containment is a really complicated thing. This is part of what they had to do in order to solve and make container queries available anyway and prevent loops from style recalculations. So consequently, we should set up our component so that in context, it can opt in to either subgrid or container query behavior depending on the content and visual priorities.

Once we remove subgrid, our layout adjustment from within the container query works as we expect, and both variants are shown here. But wait. We have another variant for a horizontal card layout that we want to implement. And, ideally, we would want all cards in a related group, such as all cards part of that layout grid, to be able to opt in to this variant all at once.

What we did for this simple responsive behavior was use container size queries. But for this variant alteration, we can employ container style queries. Now I do have to say for now, this does fall into progressive enhancement territory. We are missing Firefox support. Please, if you are interested in what I'm going to say next or any other use of container style queries, please write posts.

Please put pressure on Firefox. This is amazing for lots of reasons, but especially this one I'm gonna show you. So container style queries enable querying the value of a custom property. Eventually, it'll be the value of any property, maybe, but for now, it's custom properties. And as with any other inheritable properties, those values are inheritable through the Shadow DOM, which means that Shadow DOM styles can query custom property values set on light DOM ancestors.

So here's our big takeaway, an exciting proposition, that container style queries enable opt in styles for a group of web components without individually setting attributes. So knowing that, we'll update our global light DOM styles to accept a custom attribute of layout orientation.

And if it's set to horizontal, we'll assign the corresponding custom property. Then we'll apply it to instances of our layout grid where we want to use that horizontal layout. And then within our shadow DOM styles, we'll use a container style query that will apply when the value of layout orientation custom property is horizontal. Notice that we even are able to nest additional container size queries into the style query so that we can handle the horizontal layout's responsive concerns as well.

Very powerful feature in and out of Shadow DOM. And with that, we have the different layout mutations ready to go. This preview shows the transition from the default layout shrinking down, switching to horizontal layout, and that responsive behavior as well. We have one remaining feature to handle for, which is the tooltip that appears to label the action buttons.

And to help with that, we're gonna reach for the pretty new popover API. This feature is cross browser and considered baseline newly available, but this is an HTML API. So why am I bringing it up in a CSS talk? And to understand that, let's see what CSS problem it helps solve. A classic CSS problem is getting overlay type elements like tooltips to properly appear above everything else, which has to do with stacking context.

The stacking context defines the order things are placed in the third dimension or the z axis. Think of z axis as the layers of DOM elements between the root of the stacking context within the viewport and you, the user, looking through the viewport. If you've used z index, like z index nine nine nine nine nine nine nine to deal with this problem or try to, you've worked with stacking context.

The popover API gives us access to a guaranteed way to be any stacking context, which is by promoting its context to this fairly new browser feature of the top layer. The top layer is guaranteed to be the topmost layer above any other stacking context. It spans the entire viewport, although things you place there can have smaller visible dimensions.

Promoting an element to the top layer breaks it free of any stacking context it may otherwise exist within. In other words, wherever the element lives in the DOM, you don't have to portal it out or do anything else. This top layer access guarantees it will be visible over everything else when the popover is open.

We're reaching for popover because, unfortunately, there is no CSS only way to access the top layer. So how do we create a popover? Super simple. First, we add the attribute popover target to a button with a value that matches the ID of an element with the popover attribute. And just like that, you have a toggleable popover.

The popover will be hidden until it's toggled open from the button, and the default browser style is a fixed position within the viewport, which leaves us the remaining problem of positioning it relative to our trigger. Now some of you might already have spotted a potential conflict for trying to use popover API in the Shadow DOM context, which is use of the popover API, in this declarative HTML method requires the ability to associate the popover trigger with its content via a unique ID. Now on the one hand, Shadow DOM is a benefit here because within itself, we don't need to worry about a shadow ID conflicting with a global ID due to the encapsulation.

However, in a real world, system, you probably want to have a tooltip component that it's that it's its own extensible component and not something you recreate within any other component that needs to display it. Fortunately, custom attributes or elements, excuse me, accept popover attributes.

So, ultimately, this makes associating your custom tooltip with its related trigger really no different than the use of your own custom attributes. For our actual tooltip component, in anticipation of having those popover related attributes, we're first resetting the default browser styles for popovers.

And normally, the slot itself has a browser supplied style of display contents, and that usually essentially prevents it from affecting layout. We're actually gonna go ahead and give it a display value and use it to style the tooltip's content area. That makes the body of the tooltip as simple as including an implicit slot, which ultimately gets filled with whatever content is provided.

Now we can associate the custom tooltip to a trigger wherever we need to using the popover attributes just as we reviewed. We did sneak in one feature that has lower browser support support currently, which is the hint type of popover. This allows showing a popover on hover as well as click while still responding to light dismiss and escape key presses.

For now, we do have to polyfill part of this behavior. It's not that much JavaScript, fortunately. But once Hint is fully supported, the HTML is all you'll need for this solution. Okay. Are tooltips correctly associated with the button, but our remaining problem is positioning it. As you can see right now, it is completely disconnected from the trigger and appearing in that top left corner.

That's partly because the user agent default is set to position fixed, and, also, we reset two other properties for inset and margin. So now it's appearing at the start of the document. To resolve positioning, we'll bring in one of the most modern CSS features of anchor positioning. The spec is evolving, but it is in Chromium, Safari technical preview, and most, notably in the newly announced Safari 26 beta.

Yes. 26. It's also part of Interop 25, meaning it's actively being worked on. So expect to have more full support by end of year. Anchor position elements have their position set relative to a defined anchor. So in this example, our popovers anchor to that favorite button.

And this demo video is from a resource from Yuna showing how you can use this new property of position area to, very simply direct where to place a anchor positioned element. For the use of anchor positioning with non manual popovers, so using the declarative way we've shown, we'll soon only need to apply this property. And the reason we only will need this is because eventually, popovers will have an implicit anchoring reference to their trigger.

Unfortunately, the Safari '20 six release notes specifically stated that implicit behavior is not going to be there yet, so you'll have just a couple more steps, to make that association if you pick this up soon. Now in terms of Shadow DOM, we do have some caveats. You're not able to anchor a global element to a Shadow DOM element, and you're not able to anchor a Shadow DOM element to a slotted light DOM content, but you can anchor a global element to the custom element itself, and you can anchor a global element to slotted content that exists within the light DOM, which is all to say. Oh, and you can anchor within the same shadow DOM.

So just to say that if you are using web components or thinking about using them, just consider how that may work or conflict with anchor positioning. A constraint impacting our current setup is that anchor positioning styles must live in the same context as the anchor, which leaves us two options for actually positioning our tooltip.

The first one is we can include that simple CSS property alongside the styles, such as for the card component where the tooltip component is included. Or option two within our connected callback, we can add the positioning via the style attribute. You can, of course, do more fancy things to accept that as an option, and that results in the style living in the same context as the anchor.

Number two may feel a bit hacky, but I verified this. If we try to apply this style via the host selector for the tooltip component, it actually would fail because that's considered to a different context. I'm at time, so you won't really get into go into scope. Just know it's on the horizon for interop as well.

I didn't actually end up using it for the card component, but I guess the key point I want you to understand if you're interested, because it is another splashy feature, it cannot cross cross the shadow boundary, except for inheritance, but it may apply to your slotted light DOM content. Have kind of an interesting example here of flattening specificity, because scope itself doesn't add to the specificity score.

So I'm going through this kind of quickly. Sorry. We can basically set up something similar as we did into layers. Scope is gonna have more features for you, but because it can't cross that boundary, if you're using slotted content, you might find slightly less utility from using scope except for possibility this idea of flattening specificity. Here's another one.

Take a picture if this actually interests you, impacts your team. Here's more full details of using scope. There again, most of these issues are around the host selector, or conflicts with the ampersand for nesting. Easy to get around if you're not trying to scope on the host. So how do you find the support details that I showed you today? Well, currently, that's not super easy.

One method is digging through the results on w p t dot f y I. Here's some example test results for scope as they relate to Shadow DOM stuff. And I'm gonna warn you that it's not very easy to dig through this. So for my presentation, I did a lot of prototypes to validate, these details.

Okay. We made it. So quickly to recap, I hope you learned some things about how modern CSS features work across the light versus shadow boundary, And know, again, that overall, these features are safe to use independently in each context. The conflicts we saw were mostly around the host selectors, and some use cases require consideration of how your just general web components architecture is and how components coexist, such as when we looked at popovers and anchor positioning and also container queries for a subgrid.

So go forth. Experiment. Get weird. Test this out, Favil Browser bugs and CSS bugs, and share your use cases as well. Thank you.

Assumptions & Goals

  • assumes familiarity with the features we’ll discuss
  • focus on how modern CSS features work across the light vs. shadow DOM boundary
  • not a talk about styling strategies for web components
  • enable future-forward decision making on incorporating recent CSS features

Why is this important?

  • “Browser support” (ex. Baseline, Can I Use) is only a measure of light DOM availability
  • Shadow-specific selectors (ex. :host) introduce further support complexities
  • Conflicts can be unexpected since most documentation and tutorials skip nuances of shadow DOM implementation

Key Takeaway

All features are workable independently within shadow DOM, but may not cross the boundary between light and shadow styles.

The CSS Cascade vs. the Shadow DOM

CSS Cascade + Shadow DOM

  • Shadow DOM is often used for the ability to “encapsulate styles.”
  • Inheritable properties may apply to shadow content.
  • Inheritable properties apply to shadow elements only if they come from ancestors.

A code example compares shadow and light DOM paragraphs. The shadow paragraph receives its locally declared font-size: 3rem and inherits the global body font family. The global p { color: mediumvioletred; } rule affects only the light DOM paragraph because a paragraph is not an ancestor of the shadow content.

CSS Cascade + Shadow DOM

Slotted content

  • exists in the light DOM (when it’s not another web component in the slot)
  • for styles, the outer (global) context has priority over the inner context (shadow DOM)
    • meaning global stylesheet rules win over shadow styles using ::slotted(), regardless of specificity
  • Exception: Styles using !important with ::slotted() from within the shadow DOM win over global styles

CSS Cascade + Shadow DOM

Slotted content

A demonstration adds a light DOM paragraph to a shadow DOM slot and compares a global paragraph rule with a shadow ::slotted(p) rule. The browser’s style inspector shows that the global color declaration wins and the slotted color declaration is overridden, confirming that outer-context styles take priority unless the shadow rule uses !important.

Use of Custom Properties

  • Are inheritable by default, and therefore can cross into the shadow DOM
    • aka “pierce the shadow boundary”

Use of Custom Properties

  • Are inheritable by default, and therefore can cross into the shadow DOM
    • aka “pierce the shadow boundary”
  • Popular method to offer style customization for web components, especially for design systems and frameworks

A button component defines background and text-color custom properties on :host. One instance uses the default background, while another supplies --button-bg: mediumvioletred through its light DOM style, demonstrating that a custom property can cross the shadow boundary and customize the component.

Shadow DOM vs. Modern CSS

A social-card component is shown across progressively expanded examples: light and dark themes, wide and narrow responsive sizes, and an alternate horizontal layout. Each version retains the same image, account metadata, heading, body copy, favorite control, and secondary action while reorganizing them for its theme and available space.

color-scheme && light-dark()

color-scheme and light-dark()

✅ The value of color-scheme is inheritable, so use of light-dark() in shadow DOM will successfully select the right color.

Global stylesheet

:root {
  color-scheme: light dark;
}

Shadow DOM styles

.card {
  background-color: light-dark(
    var(--color-surface),
    var(--color-surface-dark)
  );
}

A sample card switches between light and dark surfaces, demonstrating that a globally declared color scheme is inherited across the shadow boundary and allows light-dark() inside the component to choose the appropriate color.

color-scheme and light-dark()

✅ The value of color-scheme is inheritable, so use of light-dark() in shadow DOM will successfully select the right color.

Global stylesheet

:root {
  color-scheme: light dark;
}

Shadow DOM styles

.card {
  background-color: light-dark(
    var(--color-surface),
    var(--color-surface-dark)
  );
}

A light-themed sample card confirms the component can select the appropriate surface color from the inherited color scheme.

Cascade Layers

@layer

Cascade Layers + Shadow DOM

  • 🚫 Do not cross the shadow boundary beyond any applicable inheritance
  • Cannot make use of or modify the global layer order from within the shadow DOM
  • Cannot append styles into globally defined layers

Cascade Layers + Shadow DOM

  • 🚫 Do not cross the shadow boundary beyond any applicable inheritance
  • Cannot make use of or modify the global layer order from within the shadow DOM
  • Cannot append styles into globally defined layers

A card and browser developer-tools view show two separate layers named components: one global and one inside the shadow root. The global rule makes the slotted text red, demonstrating that the shadow layer does not extend or reorder the global layer.

Cascade Layers + Shadow DOM

  • Expect to treat global vs. shadow layers separately
  • Possibly unneeded overhead for simple components
  • Potentially useful for complex components with competing states and variants

A button-component example defines separate reset, base, variants, and state layers. Three rendered buttons demonstrate default, success-variant, and hover-state outcomes, illustrating where internal layer ordering can simplify competing component styles.

Native CSS Nesting

Native CSS Nesting + Shadow DOM

Native CSS Nesting + Shadow DOM

  • ✅ Nesting related to non-:host selectors works as expected
  • 🚫 Mixed browser support—Safari earlier than 18.1 has issues with nesting within :host
  • 🚫 Nesting states on :host, such as :hover, fails
    • Lower impact since states would usually be applied to shadow DOM elements, not the host element itself
  • 🚫 Featureless & fails to reference :host

Takeaway

Nesting is safe for selecting elements inside the shadow DOM.

Native CSS Nesting + Shadow DOM

#media {
  grid-area: media;

  &,
  ::slotted(*) {
    object-fit: cover;
    aspect-ratio: 4 / 3;
  }

  *,
  ::slotted(*) {
    display: block;
    inline-size: 100%;
  }

  * {
    block-size: 100%;
  }

  @media (prefers-color-scheme: dark) {
    *,
    ::slotted(*) {
      opacity: 0.85;
      filter: saturate(1.1);
    }
  }
}

A developer-tools view highlights the card’s internal #media container and its named media slot. The nested rules apply shared sizing and cropping to both the default shadow-DOM image and slotted media, with dark-mode adjustments grouped in a nested media query.

Subgrid

Why use subgrid?

  • Use to align content and elements visually

Three cards contain images, metadata, titles of different lengths, actions, and body copy. The differing content lengths expose the need for shared row alignment across cards.

Subgrid + Shadow DOM

  • ✅ Subgrid can apply to shadow DOM content when initialized on :host
  • Can be inherited to apply to a containing element within shadow DOM
  • Use of subgrid enables web components to participate in application grid layout

Applying Subgrid

A browser grid overlay spans a collection of card custom elements while developer tools identify the external div.layout-grid as their shared grid parent. This establishes the global grid that the separate web components will participate in.

Applying Subgrid

Developer tools expand one card’s open shadow root and highlight its internal div.card as a grid. Its three content areas—media, header and actions, and body content—form the component rows that need to align with neighboring cards.

Applying Subgrid

:host {
  display: grid;
  /* Subgrid span 3 represents how many rows
     of parent grid to occupy */
  grid-template-rows: subgrid;
  grid-row: auto / span 3;
}

.card {
  display: grid;
  /* Inherit subgrid from :host */
  grid-template-rows: inherit;
  grid-row: inherit;
}

Applying Subgrid

A grid overlay labels three shared rows across multiple card components: media, header and actions, and body content. Despite different title and copy lengths, corresponding content aligns across cards, demonstrating the completed subgrid layout through separate shadow roots.

Applying Subgrid

A grid of five photo cards demonstrates that subgrid aligns corresponding metadata, headings, and body-copy rows across otherwise independent web components.

Container Queries

Responsive card layouts

Large card

Current narrow

Ideal narrow

A comparison shows the card at three widths. The large version fits its title and action buttons in one row. In the current narrow version, that row becomes cramped and the title wraps severely. The ideal narrow version moves the action buttons above the metadata and title, leaving the content readable without overflow.

Container Queries + Shadow DOM

  • Reminder: a container query cannot style the queried container.
  • :host can become a convenient container element that will always match the inline space available to the component.
  • Warning: Do not make a system-wide rule for :host to automatically be a designated container; this can cause unexpected layout side effects.
  • Consider making container-query behavior opt-in.
    • Ensure the component works well without requiring container-query-provided behavior.
:host {
  display: grid;
  grid-template-rows: subgrid;
  grid-row: auto / span 3;

  container: card / inline-size;
}

.card {
  /* Re-order template for narrow cards to
     place media above header */
  @container card (inline-size <= 35ch) {
    grid-template-areas:
      "media"
      "actions"
      "header"
      "content";
    grid-template-columns: 1fr;
  }
}

Greatest Day Ever!!

Favorite

Two versions of the same photo card—one light and one dark—show a tooltip labeling the heart action as “Favorite.” The favorite controls are highlighted to demonstrate the completed component behavior across themes.

Popover API

Stacking Context

  • The positioning of elements in the 3rd dimension, or the “z” axis
  • Manipulate the “stack” by setting z-index

Note: There is no CSS-only method of access to the top layer.

An animation expands one plane into a vertical stack between the viewport and the user. A highlighted element moves from a lower stacking context into the browser’s top layer, where it is guaranteed to appear above the other layers.

Creating a Popover

<button popovertarget="my-popover">Open Popover</button>

<div id="my-popover" popover>Popover</div>

Numbered callouts connect the button’s popovertarget value to the popover element’s matching id, then identify the popover attribute that makes the element toggleable.

Popover API + Shadow DOM

Popover API + Shadow DOM

  • Declarative HTML requires the ability to associate the popover trigger with its content via a unique ID.

Popover API + Shadow DOM

  • Declarative HTML requires the ability to associate the popover trigger with its content via a unique ID.
  • Shadow DOM Pro: encapsulated, reducing ID conflict
  • Shadow DOM Con: components should exist separately, not tightly coupled
  • Custom elements accept popover attributes.

Tooltip Web Component

<template id="ui-tooltip">
  <style>
    :host {
      /* Reset browser styles */
      inset: unset;
      border: 0;
      background-color: transparent;
    }

    slot {
      /* styles tooltip content box */
    }
  </style>
  <slot>Tooltip</slot>
</template>
<button id="favorite" class="action"
  popovertarget="favorite-tooltip">
</button>

<ui-tooltip id="favorite-tooltip" popover="hint">
  Favorite
</ui-tooltip>

Callouts identify the host-level reset of browser popover styles, the slotted tooltip-content styling, the fallback slot content, and the matching attributes that associate the trigger with the custom tooltip.

Tooltip Web Component

<button id="favorite" class="action"
  popovertarget="favorite-tooltip">
</button>

<ui-tooltip id="favorite-tooltip" popover="hint">
  Favorite
</ui-tooltip>

popover="hint" currently requires a polyfill:

  • show for mouseover and focus
  • hide for mouseout and blur

Tooltip positioning demo

Greatest Day Ever!!

Add to Collection

A social-card demo contains a collection button beside a favorite button. Hovering over the collection button opens its correctly associated tooltip, but the tooltip appears at the top-left of the viewport, visibly disconnected from its trigger.

Anchor Positioning

Anchor Positioning

Support: Chromium, Safari TP & 26 Beta

CSS anchor positioning is an active Interop focus area.

Anchor-positioned popover

An animation shows a tooltip anchored to a favorite button on a card. The tooltip moves through several placement areas—below, above, inside to the left, and outside to the right—demonstrating that an anchored element can be positioned relative to its trigger.

Anchor Positioning

anchor-tool.com by Una Kravets

position-area: bottom;
position-area: bottom span-left;
position-area: right span-bottom;
position-area: top span-left;
position-area: bottom left;
position-area: right center;
position-area: bottom right;
position-area: top left;

An interactive reference demonstrates how different position-area values place the same positioned element around its anchor. The sequence moves the element below, above, left, and right of the anchor, including centered, corner, and spanning alignments.

Anchor Positioning for Popovers

ui-tooltip {
  /* Position relative to popover */
  position-area: bottom;
}

Takeaway

Declarative popovers have an implicit anchor reference to their trigger.

Support: Chromium

Anchor Positioning + Shadow DOM

  • Unable to anchor a global element to a shadow DOM element
  • Unable to anchor a shadow DOM element to a slotted light DOM element
  • Can anchor a global element to the custom element itself
  • Can anchor a global element to slotted content that exists in the light DOM
  • Can anchor within the same shadow DOM

Anchor Positioning for Popovers

Option 1

/* Must be placed in shadow styles
where ui-tooltip has been
implemented */
ui-tooltip {
  position-area: bottom;
}

Option 2

/* Include in tooltip component’s JS */
connectedCallback() {
  this.style = "position-area: bottom";
}

Takeaway

Anchor positioning styles must live in the same context as the anchor.

@scope

@scope

An Interop focus area with uneven browser support.

Browser-support and Interop dashboards summarize implementation progress for @scope. The compatibility table shows support arriving across current browser versions, while the Interop panel reports differing conformance levels among engines.

@scope + Shadow DOM

@scope + Shadow DOM

  • Global-context @scope cannot cross the shadow boundary and apply to shadow elements, except through any applicable inheritance.

@scope + Shadow DOM

  • Global-context @scope cannot cross the shadow boundary and apply to shadow elements, except through any applicable inheritance.
  • May apply to slotted, light DOM content.
  • Suggestion: use primarily to flatten specificity.

@scope to flatten specificity

/* Specificity: 0-2-1 */
:host([success]) button {}
/* Specificity: 0-0-1 */
@scope (:host([success])) {
  button {}
}
/* Specificity: 0-1-0 */
:hover {}

The comparison moves the :host([success]) condition into an @scope boundary. Because that boundary does not add to the nested selector’s specificity, the button rule falls from 0-2-1 to 0-0-1, allowing a :hover rule at 0-1-0 to override it. A rendered example shows default, success, and hovered button states.

@scope + Shadow DOM

  • Browser inconsistency when using @scope within Shadow DOM styles
  • Chromium: per issues and specification, appears to have the current intended behavior.
    • @scope without a boundary is equivalent to @scope (:host).
    • & does not reference @scope (:host), but :scope does.
  • Safari:
    • :scope and & fail to reference the scope root.
    • Styles fail inside a scope block without a boundary.
    • ::part() used in global context within @scope fails.

How do I find these support details?

wpt.fyi

Example Shadow DOM tests for @scope: Chrome, Edge, and Firefox pass 9 of 9 subtests; the shown Safari preview passes 0 of 9.

A Web Platform Tests results table compares nine @scope and Shadow DOM subtests across Chrome, Edge, Firefox, and a Safari preview. The first three browsers pass every displayed subtest, while Safari fails all nine.

Recap

  • learned how modern CSS features work across the light vs. shadow DOM boundary
  • overall, features are safe to independently use in each context
    • conflicts usually around :host/:host() selectors
  • some use cases require consideration of how different web components co-exist
    • popovers and anchor positioning
    • container queries vs. subgrid

Go forth, experiment, get weird, and file browser & CSS bugs!

People

  • Yuna

Technologies & Tools

  • Baseline
  • Can I Use
  • Chromium
  • Safari 26
  • Shadow DOM
  • Web Components
  • wpt.fyi

Standards & Specs

  • ::slotted
  • :host
  • color-scheme
  • Container Queries
  • Container Style Queries
  • CSS Anchor Positioning
  • CSS Cascade Layers
  • CSS Containment
  • CSS Custom Properties
  • CSS Nesting
  • CSS Scope
  • CSS Subgrid
  • Custom Elements
  • light-dark()
  • Popover API
  • position-area
  • Top Layer

Concepts & Methods

  • Progressive Enhancement
  • Stacking Context

Organisations & Products

  • Firefox
  • Interop 2025