Update: I originally reported this as an inferred cause. I have since
unpacked the shipped extension bundle (v1.96.0) and confirmed the exact file,
line, and mechanism. The issue is also broader than .hidden alone.
Body rewritten accordingly.
Summary
The extension ships build/content.css (243 KB) and injects it into every
page on the web via content_scripts. That stylesheet is DeepL's own UI
stylesheet, and 61 of its 118 top-level class selectors are generic,
unnamespaced names — .hidden, .column, .Container, .section,
.radio, .close, .search-input, .btn-primary, .Logo, .Spacer, etc.
These rules silently apply to unrelated elements on the host page. In our case
.hidden { display: none } made our site header, product image gallery,
thumbnail rail and product-information accordions permanently invisible.
The stylesheet contains no @layer at all, so its declarations outrank
every layered declaration on the host page regardless of specificity or source
order (CSS Cascade L5, cascade sorting order).
Since Tailwind CSS v4 places all utilities inside @layer utilities, every
Tailwind v4 site on the web is affected, not just ours.
What it looks like
Same page, same viewport, same build — the only difference is whether the
extension is enabled.
| DeepL enabled |
DeepL disabled |
| Site header, product image gallery, thumbnail rail and information accordions are all gone. Page content shifts up by exactly the header height. |
Everything renders correctly. |
 |
 |
Root cause — verified in the shipped bundle
manifest.json (v1.96.0):
"content_scripts": [{
"matches": ["<all_urls>"],
"css": ["build/content.css"],
"js": ["build/content.js"],
"run_at": "document_idle",
"all_frames": false
}]
build/content.css, line 2703 — note the surrounding context:
li.dl-dropdown-item .checkmark-icon:dir(rtl) { left: auto; right: 6px; }
.hidden {
display: none;
}
.dropdown__wrapper { width: 100%; position: relative; }
.dropdown__wrapper button.dropdown-label { height: 44px; /* … */ }
.hidden is a private utility class belonging to DeepL's own dropdown UI. It
was never meant to affect host pages — it leaks because the entire UI
stylesheet is injected at document level with no scoping.
Bundle facts:
|
|
| Extension version |
1.96.0 |
build/content.css size |
243,002 bytes (~1,746 rules) |
Occurrences of @layer |
0 |
Occurrences of !important |
13 |
| Top-level single-class selectors |
118 |
— namespaced (.deepl-*, .dl-*, .lmt-*) |
57 |
| — unnamespaced / generic |
61 |
Note that the extension already namespaces correctly elsewhere
(.deepl-icon, .deepl-gmail-write-toolbar, .deepl-loading-overlay,
.deepl-shield-icon, li.dl-dropdown-item, …). So this is an inconsistency in
the stylesheet rather than a deliberate design decision — which suggests it
should be cheap to fix.
Impact beyond .hidden
.hidden is simply the one that hit us. Other injected rules are equally or
more destructive to any site using those class names:
| Injected selector |
Declaration |
Effect on an unrelated host page |
.hidden |
display: none |
Element disappears permanently |
.column |
flex: 1; list-style: none; margin: 0; padding: 0 |
Grid/column layouts collapse |
.Container |
container-type: inline-size; container-name: component |
Establishes containment → size/layout calculations change |
.radio |
display: flex; margin-bottom: 8px |
Form control layout changes |
.search-input |
width: 100%; height: 35px; border: 1px solid #e5e5e5; … |
Site search field restyled |
.section |
margin-bottom: 12px |
Spacing shifts |
.btn-primary |
background: #0070c9; color: White; … |
Buttons recolored |
.gray-background |
background-color: #c6c6c6 |
Background overridden |
.open-side-panel-button |
border: none !important |
Overrides host CSS with !important |
container-type: inline-size on .Container is worth special attention: it
creates a containment context, which changes layout behaviour in ways that are
very hard for a site owner to diagnose.
Why Tailwind v4 sites break specifically
Tailwind v4 emits all utilities inside @layer utilities. A responsive
show/hide pair therefore looks like this:
@layer utilities {
.hidden { display: none }
@media (width >= 64rem) { .lg\:block { display: block } }
}
.lg\:block normally wins above 1024px because it comes later in the same
layer. But DeepL's .hidden is unlayered, and unlayered author
declarations beat layered ones unconditionally. So .hidden wins at every
viewport width and the element never appears.
This affects the single most common responsive idiom in Tailwind:
hidden md:block, hidden lg:flex, hidden md:contents, …
Reproduction
A. Self-contained — no extension and no external site required
This isolates the exact cascade mechanism. Save as an .html file and open it
in a window wider than 1024px:
<style>
@layer utilities {
.hidden { display: none }
@media (width >= 64rem) { .lg\:block { display: block } }
}
</style>
<div id="box" class="hidden lg:block" style="background:#0a0;height:80px">
This box must be visible at >= 1024px.
</div>
<script>
const d = () => getComputedStyle(document.getElementById('box')).display
console.log('before injection:', d()) // "block"
// simulates what build/content.css does on every page
document.head.appendChild(Object.assign(
document.createElement('style'),
{ textContent: '.hidden{display:none}' }
))
console.log('after injection: ', d()) // "none" <-- bug
</script>
Verified output at a 1512px viewport:
before injection: block
after injection: none
A single unlayered rule defeats the layered .lg\:block, even though
.lg\:block has identical specificity and appears later in the source.
B. With the extension, on a live Tailwind v4 site
Note: the site in the screenshots above has since been patched on our
side — we renamed every hidden to max-lg:hidden as a workaround, so it no
longer demonstrates the bug. Any unpatched Tailwind v4 site containing a
hidden lg:block element will reproduce it.
- Enable the DeepL extension.
- Open an unpatched Tailwind v4 site in a window wider than 1024px.
- Paste into the DevTools console:
['hidden', 'lg:block', 'hidden lg:block'].forEach(function (c) {
var el = document.createElement('div')
el.className = c
document.body.appendChild(el)
console.log(JSON.stringify(c), '->', getComputedStyle(el).display)
el.remove()
})
| class |
expected |
actual with extension ON |
hidden |
none |
none |
lg:block |
block |
block |
hidden lg:block |
block |
none ← bug |
This runs on freshly created elements that the page never touched, so no
site-specific JavaScript, framework or hydration logic is involved.
Notes on diagnosis difficulty
This class of bug is extremely expensive for site owners to diagnose:
- CSS injected via
content_scripts / chrome.scripting.insertCSS() does
not appear in document.styleSheets, so a page cannot detect or
enumerate it programmatically.
- The affected elements remain in the DOM with no
hidden attribute and no
inline style, so it looks like a framework or hydration bug.
- Only
display is affected, while other utilities in the same media block
(lg:grid-cols-*, lg:col-start-1) still apply — which points investigators
away from CSS entirely.
We initially spent significant time investigating React 19 hydration recovery
and DOM mutation before identifying the cascade as the cause.
Suggested fix
In rough order of preference:
- Move the UI stylesheet into the shadow DOM. The extension already uses a
shadow root for <deepl-input-controller>, so the infrastructure exists.
Host pages should receive no stylesheet at all.
- Namespace every remaining class. The extension already does this for 57
of 118 selectors (.deepl-*, .dl-*). Applying the same convention to the
other 61 would resolve the collision entirely.
- If page-level CSS is unavoidable, wrap it in a cascade layer —
@layer deepl { … }. Any named layer loses to unlayered host CSS, so this
alone would prevent the extension from outranking host page styles.
- Drop the 13
!important declarations from page-level CSS.
Fix 3 is a one-line change to the build output and would immediately stop the
worst symptom (permanently invisible content), even before 1 or 2 land.
References
Prior incidents of the same class:
Thank you.
Summary
The extension ships
build/content.css(243 KB) and injects it into everypage on the web via
content_scripts. That stylesheet is DeepL's own UIstylesheet, and 61 of its 118 top-level class selectors are generic,
unnamespaced names —
.hidden,.column,.Container,.section,.radio,.close,.search-input,.btn-primary,.Logo,.Spacer, etc.These rules silently apply to unrelated elements on the host page. In our case
.hidden { display: none }made our site header, product image gallery,thumbnail rail and product-information accordions permanently invisible.
The stylesheet contains no
@layerat all, so its declarations outrankevery layered declaration on the host page regardless of specificity or source
order (CSS Cascade L5, cascade sorting order).
Since Tailwind CSS v4 places all utilities inside
@layer utilities, everyTailwind v4 site on the web is affected, not just ours.
What it looks like
Same page, same viewport, same build — the only difference is whether the
extension is enabled.
Root cause — verified in the shipped bundle
manifest.json(v1.96.0):build/content.css, line 2703 — note the surrounding context:.hiddenis a private utility class belonging to DeepL's own dropdown UI. Itwas never meant to affect host pages — it leaks because the entire UI
stylesheet is injected at document level with no scoping.
Bundle facts:
build/content.csssize@layer!important.deepl-*,.dl-*,.lmt-*)Note that the extension already namespaces correctly elsewhere
(
.deepl-icon,.deepl-gmail-write-toolbar,.deepl-loading-overlay,.deepl-shield-icon,li.dl-dropdown-item, …). So this is an inconsistency inthe stylesheet rather than a deliberate design decision — which suggests it
should be cheap to fix.
Impact beyond
.hidden.hiddenis simply the one that hit us. Other injected rules are equally ormore destructive to any site using those class names:
.hiddendisplay: none.columnflex: 1; list-style: none; margin: 0; padding: 0.Containercontainer-type: inline-size; container-name: component.radiodisplay: flex; margin-bottom: 8px.search-inputwidth: 100%; height: 35px; border: 1px solid #e5e5e5; ….sectionmargin-bottom: 12px.btn-primarybackground: #0070c9; color: White; ….gray-backgroundbackground-color: #c6c6c6.open-side-panel-buttonborder: none !important!importantcontainer-type: inline-sizeon.Containeris worth special attention: itcreates a containment context, which changes layout behaviour in ways that are
very hard for a site owner to diagnose.
Why Tailwind v4 sites break specifically
Tailwind v4 emits all utilities inside
@layer utilities. A responsiveshow/hide pair therefore looks like this:
.lg\:blocknormally wins above 1024px because it comes later in the samelayer. But DeepL's
.hiddenis unlayered, and unlayered authordeclarations beat layered ones unconditionally. So
.hiddenwins at everyviewport width and the element never appears.
This affects the single most common responsive idiom in Tailwind:
hidden md:block,hidden lg:flex,hidden md:contents, …Reproduction
A. Self-contained — no extension and no external site required
This isolates the exact cascade mechanism. Save as an
.htmlfile and open itin a window wider than 1024px:
Verified output at a 1512px viewport:
A single unlayered rule defeats the layered
.lg\:block, even though.lg\:blockhas identical specificity and appears later in the source.B. With the extension, on a live Tailwind v4 site
hiddennonenonelg:blockblockblockhidden lg:blockblocknone← bugThis runs on freshly created elements that the page never touched, so no
site-specific JavaScript, framework or hydration logic is involved.
Notes on diagnosis difficulty
This class of bug is extremely expensive for site owners to diagnose:
content_scripts/chrome.scripting.insertCSS()doesnot appear in
document.styleSheets, so a page cannot detect orenumerate it programmatically.
hiddenattribute and noinline style, so it looks like a framework or hydration bug.
displayis affected, while other utilities in the same media block(
lg:grid-cols-*,lg:col-start-1) still apply — which points investigatorsaway from CSS entirely.
We initially spent significant time investigating React 19 hydration recovery
and DOM mutation before identifying the cascade as the cause.
Suggested fix
In rough order of preference:
shadow root for
<deepl-input-controller>, so the infrastructure exists.Host pages should receive no stylesheet at all.
of 118 selectors (
.deepl-*,.dl-*). Applying the same convention to theother 61 would resolve the collision entirely.
@layer deepl { … }. Any named layer loses to unlayered host CSS, so thisalone would prevent the extension from outranking host page styles.
!importantdeclarations from page-level CSS.Fix 3 is a one-line change to the build output and would immediately stop the
worst symptom (permanently invisible content), even before 1 or 2 land.
References
https://www.w3.org/TR/css-cascade-5/#cascade-sort
https://developer.chrome.com/docs/extensions/develop/concepts/content-scripts
Prior incidents of the same class:
Thank you.