惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
MongoDB | Blog
MongoDB | Blog
博客园_首页
博客园 - 三生石上(FineUI控件)
博客园 - 聂微东
B
Blog RSS Feed
D
Docker
IT之家
IT之家
大猫的无限游戏
大猫的无限游戏
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
阮一峰的网络日志
阮一峰的网络日志
罗磊的独立博客
Recent Announcements
Recent Announcements
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
A
About on SuperTechFans
The GitHub Blog
The GitHub Blog
G
Google Developers Blog
V
V2EX
量子位
雷峰网
雷峰网
月光博客
月光博客
云风的 BLOG
云风的 BLOG
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
T
Tailwind CSS Blog

daverupert.com

I wish you the best in the Offline Introducing Microlighter Shadow-piercing local component variables Vibe Check №43 Before I go: Double your business with companies you like The new yet familiar CodePen 2.0 Finding curated public domain images Spoons by Sam Vibe Check №42 The duality of language models in the browser 10,000-watt GPU meet 40-watt lump of meat I don’t want a screenshot of your Claude conversation When moving fast, talking is the first thing to break Inverted themes with light-dark() Ozempic dreams Before I go: People like it when other people make things People are not friction Smaller and dumber Priority of idle hands Magic Words Write about the future you want I’m swearing off APIs entirely Waiting for the power to go out The best version of my site so far… Focus rings with nested contrast-color()? Interpolate contrast-color() to manipulate lightness Using your design system colors with contrast-color() Algorithmic hover states with contrast-color() Twenty Twenty-Five Vibe Check №41
A workaround for hiding empty slots in Chromium
Dave Rupert · 2026-06-19 · via daverupert.com

Named slots are one of web components’ biggest superpowers ✨. Imagine a Button component with an optional icon; in Web Components we don’t need a separate Button and IconButton, a single Button component with <slot name="icon"> will do. Or a card component with a handful of predefined slots for image, title, description, price, metadata, etc. Not 100 different cards… one card.

Slots are a wonderful way to declaratively compose UI, but one shadow styling challenge that has vexed my team over the years is how to contextually hide a <slot> when there’s no slotted content.

The problem… ghost gaps

The most idiomatic way to check if a particular slot is in-use is to use :host(:has([slot="foo"])). But… In a weird twist to how web component browser-compat bugs tend to go, :host(:has([slot])) works in Safari and Firefox today but doesn’t work in Chromium!

Chromium not supporting :host(:has([slot])) means we can’t query an element’s Light DOM contents from the Shadow DOM. That interpretation of encapsulation makes sense (I guess) and in most situations it’s not a big problem; an empty slot is like an empty span and probably won’t impact rendering. But it becomes a problem when the empty slots appear in a flex or grid layout using gap because unless you hide the element and/or its wrapper, the gap will not collapse in Chromium, so you end up with a “ghost gap”.

Example: With slot="optional" content in Light DOM

Required content

Optional content - expected: show content with gap

<optional-slot-demo-broken>
  <div slot="required">Required content</div>
  <div slot="optional">Optional content - expected: show content with gap</div>
</optional-slot-demo-broken>

Example: Without slot="optional" content in Light DOM

Required content

<optional-slot-demo-broken>
  <div slot="required">Required content</div>
  <!-- expected: does not show extra whitespace -->
</optional-slot-demo-broken>
  • ✅ Works in Safari = wrapper div hidden, no gap
  • ✅ Works in Firefox = wrapper div hidden, no gap
  • ❌ Broken in Chromium = extra whitespace below slot="required"

The failed attempts…

We’ve tried a lot of fixes over the last couple years. Variations on :host(:has()), element-name:has(), !important, ::slotted([slot]) ~ [slot], ::slotted([slot]:empty), and all permutations of those. Copilot/Claude were certain these would work… but alas…

The fix… use @scope

This week my co-worker Jeff and I were navigating this issue again. Coincidentally, the ShopTalkShow Discord was discussing the same exact problem and cited the HasSlotController in WebAwesome (née Shoelace) as a working solution to this problem. I’d prefer to keep the solution in pure CSS, but was getting desperate.

While glancing through the code I noticed the use of :scope in L38-40 and while unrelated it sparked a curiosity to try to use :scope to juke Chromium’s shadow limitations. A handful of tries later and I had a working solution.

Here’s what I came up with:

:host {
  display: grid;
  gap: 1rem;
}

/* Hide optional slots by default */
.optional-slot {
  display: none;
}

/* Show in Safari/Firefox */
:host(:has([slot="optional"])) .optional-slot {
  display: block;
}

/* Fix for Chromium */
@scope {
  :scope:has([slot="optional"]) .optional-slot {
    display: block;
  }
}

Example: With slot="optional" content in Light DOM

Required content

Optional content - expected: show content with gap

<optional-slot-demo>
  <div slot="required">Required content</div>
  <div slot="optional">Optional content - expected: show content with gap</div>
</optional-slot-demo>

Example: Without slot="optional" content in Light DOM

Required content

<optional-slot-demo>
  <div slot="required">Required content</div>
  <!-- expected: does not show extra whitespace -->
</optional-slot-demo>

And the results…

  • ✅ Works in Safari = wrapper div hidden, no gap
  • ✅ Works in Firefox = wrapper div hidden, no gap
  • ✅ Works in Chrome = wrapper div hidden, no gap

The why…

I love a good CSS-trick, but even worse than a bug is not knowing why something works. My other teammate Zacky dug into the spec and confirmed this is a viable fix because as its defined @scope without a scope-root in the shadow styles allows :scope to match the shadow-host instead of the shadow-root… which is clutch. From the spec:

The :scope selector can match the featureless shadow host when that host is the scoping root element. (Issue 9025)

And…

@scope rule without scope-start scopes to the shadow host instead of the shadow root. (Issue 9178)

Thanks, Team CSSScopeRule, for enabling this workaround (checks notes) three years ago. There’s a wonderful bit of irony that I’m using @scope to break out of the Shadow DOM’s style containment and that’s part of why I love CSS.

I look forward to the day we can use :host(:has()) and :has-slotted() in all browsers, but in the meantime I’m happy to have stumbled upon a duplicative-yet-simple fix. Let me know if you end up using this and it works for you. I already know we have a handful of minor spacing bugs to patch up in our system.