Suggest an editImprove this articleRefine the answer for “Methods for style isolation in CSS”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Style isolation in CSS** scopes component styles so they cannot affect other elements on the page. ```css /* Global */ .primary { background: blue; } /* affects ALL .primary */ /* CSS Modules */ .primary { } /* compiled to .Button_primary__abc123 */ ``` **Key rule:** plain site: BEM. React/Vue: CSS Modules. Web Components: Shadow DOM. Dynamic themes: CSS-in-JS.Shown above the full answer for quick recall.Answer (EN)Image**Style isolation in CSS** scopes styles to specific components so they cannot leak into unrelated parts of the page. ## Theory ### TL;DR - Analogy: apartments in a building. Each has its own paint and lights that stay inside. No bleeding into neighbors. - Global CSS is one shared namespace. Any `.button` rule anywhere hits every `.button` element on the page. - Isolation automates scoping via naming conventions, build-time hashing, or browser encapsulation. - Decision rule: plain site? BEM. React/Vue app? CSS Modules. Web Components? Shadow DOM. Dynamic themes? CSS-in-JS. ### Quick example Global CSS leaks by design. Isolation stops that. ```css /* Global - leaks */ .primary { background: blue; } /* Now <nav class="primary"> also turns blue. Not what you wanted. */ ``` ```jsx /* CSS Modules - scoped */ /* Button.module.css */ .primary { background: blue; } /* Webpack compiles this to: .Button_primary__abc123 */ import styles from './Button.module.css'; <button className={styles.primary}>Save</button> /* <nav class="primary"> stays untouched */ ``` The build tool rewrites `.primary` to a unique hash. Nothing else on the page can match it by accident. ### Key difference Browsers parse CSS into one shared stylesheet. Specificity and DOM order decide which rule wins. That works fine for small sites. But in a component app with 50 engineers, `.button` in one file will eventually collide with `.button` in another. Style isolation converts CSS from a shared namespace into per-component buckets. Each component owns its styles. No collisions. No cascade surprises. ### When to use - Plain CSS, no build tools: BEM. Names like `.button__icon` and `.button--primary` prevent overlaps by convention alone. - React, Vue, or Svelte apps: CSS Modules. The build step hashes class names automatically at compile time. - Reusable web components: Shadow DOM. The browser enforces the boundary natively, no configuration needed. - Dynamic themes or component libraries: CSS-in-JS (styled-components, Emotion). Scoping happens at runtime per instance. - Large teams or monorepos: CSS Modules with TypeScript. Class name imports become type-safe. ### Comparison table | Method | How it scopes | Tooling | Runtime cost | Best for | |---|---|---|---|---| | **BEM** | Naming convention: `.block__element--modifier` | None | None | Vanilla CSS, simple sites | | **CSS Modules** | Build-time class hashing: `Button_button__xYz` | Webpack, Vite | None | Component frameworks | | **Shadow DOM** | Browser DOM boundary via `attachShadow()` | None (native) | Low | Web Components | | **CSS-in-JS** | Runtime unique class per instance | styled-components, Emotion | Medium | Dynamic styling | | **Atomic CSS** | Single-purpose utility classes | Tailwind, UnoCSS | None | Utility-first teams | | When to use | No build: BEM. Framework: Modules. Custom elements: Shadow. Dynamic: CSS-in-JS. Utility teams: Tailwind | | | | ### How the browser handles it Browsers parse CSS into a global stylesheet. Selectors cascade via specificity and DOM order. BEM fights that by making class names long and unique enough that collisions become unlikely. CSS Modules (via PostCSS or a Webpack loader) rewrites class names to hashes before the browser ever sees the CSS, so `.primary` becomes `.Button_primary__abc123` at build time. Shadow DOM takes a different path: `attachShadow({mode: 'open'})` inserts a `#shadow-root` node. The Blink and WebKit rendering engines enforce a hard boundary there. External stylesheets cannot cross into the shadow. Internal styles cannot leak out. ### Common mistakes **1. BEM: single underscore instead of double** ```css /* Wrong - flat name, can match unrelated selectors */ .button_icon { color: red; } /* Correct - double underscore signals the element relationship */ .button__icon { color: red; } ``` The single underscore version looks like BEM but breaks the hierarchy. Two components can both have `.button_icon` and you are back to a collision. **2. CSS Modules: trying to mutate the styles object** ```js /* Wrong - the object is frozen post-build, this does nothing */ styles.primary = 'my-override'; /* Correct - compose classes with a template literal */ <button className={`${styles.primary} ${styles.active}`}>OK</button> ``` The imported `styles` object maps names to hashed strings. You can read from it, not write to it. **3. Shadow DOM: using `mode: 'closed'` without a reason** ```js /* Closed - shadowRoot returns null from outside */ this.attachShadow({ mode: 'closed' }); this.shadowRoot.querySelector('button'); /* null - breaks Puppeteer, Playwright, unit tests */ /* Open - accessible for testing and DevTools */ this.attachShadow({ mode: 'open' }); ``` Closed mode blocks external `shadowRoot` access. It sounds more secure, but it also breaks your test suite and most automation tools. Use `'closed'` only when you have a concrete security reason. **4. Shadow DOM: forgetting `:host` for host element styles** ```html <my-button class="warning"></my-button> <script> /* Wrong - the external class won't apply inside the shadow */ shadow.innerHTML = `<style>.warning { background: yellow; }</style>`; /* Correct - :host() targets the custom element itself */ shadow.innerHTML = `<style>:host(.warning) { background: yellow; }</style>`; </script> ``` Styles inside a shadow root cannot target the host element with regular selectors. `:host` and `:host()` are the only way in. **5. CSS-in-JS: missing ThemeProvider** A `styled.div` component without a wrapping `<ThemeProvider>` falls back to global context. Any theme token it references resolves to `undefined`, and you get no styles or unexpected defaults. Styled-components v6 docs flag this as the most common production issue. ### Real-world usage - Next.js 14+: `page.module.css` ships with every new app. CSS Modules is the default. - Vue 3 and Nuxt 3: `<style scoped>` on any component. Vue's compiler hashes classes the same way CSS Modules does. - SvelteKit: scoping is built into the compiler. No config needed. - Lit (used in Chrome DevTools extensions): Shadow DOM. Custom elements like `<time-ago>` are fully encapsulated. - Shopify Hydrogen: styled-components for runtime theming across storefronts. I've seen the Shadow DOM `:host` mistake in roughly half the web component code I review. It's not obvious from the docs and it trips up people who know regular CSS well. ### Follow-up questions **Q:** How does CSS cascade work inside Shadow DOM compared to a regular document? **A:** In a regular document, inheritance and specificity cross all DOM nodes. In Shadow DOM, the boundary cuts off external stylesheets. Only `:host()` lets styles from inside target the host element. `::part()` lets external CSS reach named parts of the shadow tree. **Q:** How does CSS Modules handle media queries and nesting? **A:** Hashing applies only to class names. `@media (hover: hover) { .Button_primary__xyz { ... } }` is valid output. Sass nesting works too via the appropriate Webpack or Vite loader. **Q:** BEM vs SMACSS for large codebases? **A:** SMACSS groups rules by category (base, layout, module, state). BEM enforces uniqueness per component. In component-heavy apps, BEM scales better because each block is self-contained. SMACSS requires more discipline to prevent shared category rules from colliding. **Q:** What is the performance difference between Shadow DOM and CSS Modules? **A:** CSS Modules has zero runtime cost. Hashing happens at build time. Shadow DOM adds a small overhead in the Blink rendering engine, roughly 5-10% per Chromium metrics, because the browser maintains separate style scopes. For most apps that number is not a problem, but it is a real cost. **Q:** In a micro-frontend setup, how do you isolate styles across bundles without Shadow DOM? **A:** CSS Modules with build-time hashing prevents class name collisions across bundles even when they share a page. For runtime theme sync between micro-frontends, postMessage works for passing CSS custom property values. Avoid global class names. iframes give full isolation but add communication overhead. ## Examples ### Reusable button in a React dashboard A real scenario: a design system button that needs to stay blue on hover, no matter what the parent page's CSS looks like. ```jsx /* Button.module.css */ .primary { background: #007bff; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; } .primary:hover { background: #0056b3; } /* Webpack compiles this to: .Button_primary__abc123 */ ``` ```jsx /* Button.jsx */ import styles from './Button.module.css'; function DashboardButton({ children }) { return ( <button className={styles.primary}> {children} </button> ); } /* Dashboard.jsx */ <DashboardButton>Save</DashboardButton> <aside className="primary">Sidebar content</aside> /* aside is not blue - the compiled class name is different */ ``` The `aside` uses a plain `.primary` class. The button uses `.Button_primary__abc123`. No collision possible. ### Web component with Shadow DOM host styles The `:host` pseudo-class is where most developers hit a wall the first time. ```html <my-button class="warning" id="host"></my-button> <script> class MyButton extends HTMLElement { constructor() { super(); const shadow = this.attachShadow({ mode: 'open' }); shadow.innerHTML = ` <style> :host { display: block; padding: 1rem; } :host(.warning) { background: yellow; /* applies when host has .warning class */ } button { background: green; color: white; } ::slotted(.label) { color: red; /* styles content passed via slot */ } </style> <slot></slot> <button>Internal button</button> `; } } customElements.define('my-button', MyButton); document.querySelector('my-button').innerHTML = '<span class="label">Slotted text</span>'; </script> ``` Result: the host element gets padding and a yellow background from the `.warning` class. The internal button is green. The slotted `span` is red. Without `:host(.warning)`, the yellow background does not apply even though the class is on the element. ### BEM naming in a navigation component A simple example showing how double-underscore and double-hyphen prevent collisions in practice. ```css /* nav.css */ .nav {} .nav__item {} .nav__item--active { font-weight: bold; color: #007bff; } .nav__icon { width: 16px; height: 16px; } ``` ```html <nav class="nav"> <a class="nav__item nav__item--active" href="/"> <img class="nav__icon" src="home.svg" alt=""> Home </a> <a class="nav__item" href="/about">About</a> </nav> ``` `.nav__item--active` only matches elements inside this component. A generic `.active` class somewhere else on the page cannot match it.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.