Skip to content
This repository was archived by the owner on Aug 21, 2026. It is now read-only.
This repository was archived by the owner on Aug 21, 2026. It is now read-only.

[DeepL-Extension] Unnamespaced global CSS injected into every page (build/content.css) breaks host site layouts — .hidden defeats all Tailwind v4 utilities #61

Description

@solssak

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.
Broken with the DeepL extension enabled Correct with the DeepL extension disabled

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.

  1. Enable the DeepL extension.
  2. Open an unpatched Tailwind v4 site in a window wider than 1024px.
  3. 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions