{
  "$type": "site.standard.document",
  "content": {
    "$type": "site.standard.content.markdown",
    "text": "import PlaceholderImage from '../../components/PlaceholderImage.astro';\nimport InlineCallout from '../../components/InlineCallout.astro';\nimport ScopeDemo1 from '../../components/demos/ScopeDemo1.astro';\nimport ChecklistItem from '../../components/ChecklistItem.astro';\n\nAs a parent of two, I've found writing CSS can be a bit like having toddlers - you tell them to only use the red pens and somehow, blue pen appears somewhere completely unexpected. The sofa. The wall. Something you haven't seen in six months.\nCSS has the same problem. You tie up a new button style for a new part of a project and suddenly all the buttons across the site have that random border-radius or hover state. \n\nThe rules are loose and the damage spreads.\n\nEnter @scope.\n\n## How it works\n\nLet's get to the code. How do we set this up so that completely-made-up scenario doesn't even become a reality. Firstly, you use the `@scope` at-rule, following by your selector of choice - let's drag the infamous card example out here.\n\n```css\n@scope (.card) {}\n```\n\nWell...that's kind of it, right? We've it set it up. Nothing more to do here, apart from adding in our styles *within* those lovely braces on the `.card`. Anything inside this 'scoped' card is exclusive just to that class name. This allows us to use more generic CSS selectors, ditching the specific class/IDs like `.image` in favor of just the `img` tag. Let's do a comparison between a basic card layout using both the `BEM` naming convention and `@scope`.\n\n### Our example card markup\n\n```html\n<div class=\"card\">\n\t<PlaceholderImage ratio=\"16 / 9\" />\n  <div class=\"card__content\">\n    <h3 class=\"card__heading\">Card Title</h3>\n    <p class=\"card__text\">\n   \t  Amet aute minim eiusmod tempor do minim eu. Eu officia amet \n      consequat et esse pariatur Lorem proident aliqua reprehenderit \n      esse elit. Eiusmod laborum fugiat irure culpa adipisicing velit. \n      In consequat adipisicing ea consequat aute elit elit. Do proident \n      veniam commodo voluptate adipisicing ullamco et ut aliqua fugiat \n      id veniam do. Aliquip non incididunt ipsum occaecat cillum est \n      consequat consectetur cillum. Dolore in excepteur veniam adipisicing \n      pariatur Lorem.\n    </p>\n    <a class=\"card__link\" href=\"#\">Learn More</a>\n  </div>\n</div>\n```\n\nThis just gives us this beautifully unstyled masterpiece.\n\n<div class=\"card\">\n  <picture class=\"card__image-container\">\n\t  <img\n\t      class=\"card__image\"\n\t      src=\"https://picsum.photos/900\"\n\t      alt=\"Descriptive alt text\"\n\t      width=\"450\"\n\t      height=\"450\"\n\t  />\n  </picture>\n  <div class=\"card__content\">\n    <h3 class=\"card__heading\">Card Title</h3>\n    <p class=\"card__text\">Amet aute minim eiusmod tempor do minim eu. Eu officia amet consequat et esse pariatur Lorem proident aliqua reprehenderit esse elit. Eiusmod laborum fugiat irure culpa adipisicing velit. In consequat adipisicing ea consequat aute elit elit. Do proident veniam commodo voluptate adipisicing ullamco et ut aliqua fugiat id veniam do. Aliquip non incididunt ipsum occaecat cillum est consequat consectetur cillum. Dolore in excepteur veniam adipisicing pariatur Lorem.</p>\n    <a class=\"card__link\" href=\"#\">Learn More</a>\n  </div>\n</div>\n\n## BEM vs @scope - side by side\n\nSame card. Same output. Very different markup.\n\n<ScopeDemo1 />\n\nFor browsers that don't support `@scope` yet, wrap it in a feature query and keep your BEM styles as the fallback.\n\n### The one thing BEM couldn't solve\n\nBEM naming conventions are pretty good at stopping styles from leaking from one to another - for example `.card__heading` will never accidentally style a `.nav__heading`. But there's always been a blind spot of nested instances of the same component. If a `.card` element can have another `.card` inside it, then every BEM rule on `.card__heading` will apply to both the inner and outer card. No mistake, no weird gotcha. That's just how it works. There's no way to specifically target just the 'outer' one without adding extra classes e.g. `.card--outer`, `.card--parent`, or using specificity hacks. With `@scope`, it has that same problem, but it also has a fix built in to rectify it.\n\n### It's cards all the way down\n\nSo what's the fix? We can use the `to` keyword to define where a scope stops:\n\n```css\n@scope (.card) to (.card) {\n  h3 { \n  \tfont-size: 24px; \n  }\n}\n```\n\nThe scope now ranges in the ring between the outer `.card` and any of the `.card` elements inside it. Styles reach the `h3` within the containing outer `.card`, but stop before reaching any inner `.card` elements. The nested card gets its own clean scope when its own @scope rule kicks in.\n\n```html\n<div class=\"card\">\n  <h3>Outer card - gets font-size: 24px</h3>\n\n  <div class=\"card\">\n    <h3>Inner card - unaffected</h3>\n  </div>\n</div>\n```\n\nThis is sometimes called the donut hole - a term coined by [Nicole Sullivan in 2011](https://www.stubbornella.org/2011/10/08/scope-donuts/) - meaning the scope has a gap punched through it at the lower boundary. Styles apply in the ring, not inside the hole. It's an odd analogy, but it sticks.\n\n### When do you actually need to use this?\n\nNot every component needs a lower boundary - most cards won't nest, and hopefully we never enter a hellscape where developers are putting buttons inside buttons. But it's worth reaching for whenever:\n\n- A component can genuinely contain itself (comments with replies that are also comments, dropdown menu containing a nested submenu)\n- Two different components share element selectors (h3, img, a) and one can appear inside the other\n\nFor the card we've built in this post, you probably don't need it. But now you know it exists for when you do!\n\n## Theming without modifier classes\n\nYou might have noticed the `@scope` card example already does something quiet but useful with custom properties:\n\n```css\n@scope (.card) {\n    --color-dark: grey;\n\n    border: 1px solid var(--color-dark, #000);\n}\n```\n\nThat `--color-dark: grey` value lives *within* the scope, and overrides whatever `--color-dark` is set to globally, but only within this card component. Everything else is given mercy and left alone completely.\n\n### The problem with global tokens\n\nDesign token systems typically live on `:root`. A colour palette, a spacing scale, a type ramp - all declared globally, all inherited everywhere. That's fine, until you need a component to look different in a specific context. Let's use an example of two cards in a promotional banner needing both light and dark backgrounds. When we're using global tokens, we have to either write separate styles for each variant or use modifier classes/data attributes to switch between them. Both approaches mean the component's styles are duplicated across variants.\n\n```css\n/* The global token */\n:root {\n    --card-bg: white;\n    --card-text: #222;\n    --card-border: #d4cfc0;\n}\n\n.card {\n    background: var(--card-bg);\n    color: var(--card-text);\n    border: 1px solid var(--card-border);\n}\n\n/* Modifier: override each token individually */\n.card--promotional {\n    --card-bg: #2c5364;\n    --card-text: white;\n    --card-border: transparent;\n}\n```\n\nOf course, this works, but the modifier class is doing two jobs: it's both a style hook and a token override. As the number of variants grows, so does the amount of maintenance needed, the technical debt has amassed and lastly the code becomes harder to read.\n\n### The scoped token pattern\n\n`@scope` lets you redefine tokens at the component boundary without a modifier class. The scope root becomes the theming surface:\n\n```css\n/* Global token defaults */\n:root {\n    --card-bg: white;\n    --card-text: #222;\n    --card-border: #d4cfc0;\n}\n\n/* Base component - uses whatever the tokens say */\n@scope (.card) {\n    :scope {\n        background: var(--card-bg);\n        color: var(--card-text);\n        border: 1px solid var(--card-border);\n    }\n\n    h3 { color: var(--card-text); }\n    a  { color: var(--card-text); }\n}\n\n/* Promotional context - redefine the tokens at the parent level */\n.banner {\n    --card-bg: #2c5364;\n    --card-text: white;\n    --card-border: transparent;\n}\n```\n\n```html\n<!-- Default card -->\n<div class=\"card\"> … </div>\n\n<!-- Same card, different context - no modifier class needed -->\n<div class=\"banner\">\n    <div class=\"card\"> … </div>\n</div>\n```\n\nThe card component itself hasn't changed. The `.banner` parent reestablishes the tokens, and the card gets the new values through the cascade. One component has been defined, but it can work in any number of contexts.\n\n### The missing layer\n\nA typical token/design system works mainly in one direction: it gets declared globally and is then taken on by components. Changing the global value will affect all components that use it - which is fantastic for rebranding a project effectively and with relative ease. But what if you need just _one_ instance of a component to behave differently from all the others? A \"Featured\" card that sits at the top of a content feed for example, one that uses the same markup, same component, but has a dark background instead of light. What then?\n\nScoped tokens get involved in between the global and component layers. They let you redefine tokens at the context level, without the need to touch the component **or** the global defaults. This means that you can have a single instance of a component behave differently from all the other ones, without needing to modify the component itself or those site-wide defaults.\n\nFurther to this, we can use it alongside the lower boundary mentioned earlier, using the same functionality to stop styles leaking into other components.\n\n```css\n/* Tokens redefined in the banner only reach as far as the first nested .card */\n@scope (.banner) to (.card) {\n    --card-bg: #2c5364;\n    --card-text: white;\n}\n```\n\n<InlineCallout\n\ttagline=\"Want to find out more?\"\n\ttitle=\"Level Up Your Styles With CSS Variables\"\n\tdescription=\"If this has made you want to go deeper on custom properties, this is the next post to read.\"\n\tslug=\"/writing/level-up-your-styles-with-css-variables\"\n/>\n\n## Migrate from BEM: A Practical Checklist\n\nSo, if the above sounds like a good fit for the next time you're building out a component, or even better, get the budget to refactor an old one, the checklist below will help you get started. This checklist walks through how to go about replacing BEM class names with `@scope` on a component, using our card example above as a reference.\n\n### Before you start\n\n<ul class=\"checklist\">\n\t<ChecklistItem id=\"pre-browser-support\">Check your browser support requirements. `@scope` is supported in Chrome 118+, Firefox 120+, and Safari 17.2+. If you're supporting anything older, wrap your scoped styles in a feature query and keep BEM as the fallback: `@supports (selector(@scope)) { @scope (.card) { … } }` - it adds two lines and costs nothing.</ChecklistItem>\n\t<ChecklistItem id=\"pre-pick-one-component\">Pick one component to migrate first. Don't rewrite everything. Choose a self-contained component - a card, a nav item, a badge - and use it as your proof of concept before touching shared or global styles.</ChecklistItem>\n\t<ChecklistItem id=\"pre-keep-bem-open\">Have the original BEM styles open alongside. You're not deleting them yet. Migration goes: write the scoped version → confirm it works → remove the BEM version.</ChecklistItem>\n</ul>\n\n### Step 1 - Identify your scope root\nYour BEM block becomes your @scope selector. The block class is the only class you need to keep on the HTML element.\n```css\n/* BEM: block is the root */\n.card { … }\n.card__image { … }\n.card__content { … }\n\n/* @scope: same root, different shape */\n@scope (.card) {\n  /* everything lives in here now */\n}\n```\n\n<ul class=\"checklist\">\n\t<ChecklistItem id=\"s1-identify-block-class\">Identify the block class - this becomes your @scope (.block-name) selector</ChecklistItem>\n\t<ChecklistItem id=\"s1-keep-block-class\">Keep the block class on the HTML element (.card, .nav, .badge) - it's now the scope root, not just a selector</ChecklistItem>\n\t<ChecklistItem id=\"s1-remove-block-prefix\">Remove the block prefix from every element class in your markup - card__image becomes image, or just gets removed in favour of an element selector (see step 2)</ChecklistItem>\n</ul>\n\n### Step 2 - Replace element classes with element selectors where possible\nThis is the cleanest win. BEM element classes exist to prevent style leakage - @scope handles that, so you can target img, h3, p, a directly inside the scope without them bleeding out.\n```css\n/* BEM: explicit class on every element */\n.card__image   { aspect-ratio: 16/9; }\n.card__heading { font-size: 24px; }\n.card__text    { -webkit-line-clamp: 2; }\n.card__link    { margin-inline-start: auto; }\n\n/* @scope: element selectors are safe now */\n@scope (.card) {\n  img { aspect-ratio: 16/9; }\n  h3  { font-size: 24px; }\n  p   { -webkit-line-clamp: 2; }\n  a   { margin-inline-start: auto; }\n}\n```\n\n<ul class=\"checklist\">\n\t<ChecklistItem id=\"s2-ask-semantically-unique\">For each BEM element class, ask: is this targeting a semantically unique element within the component? (__image → img, __heading → h3)</ChecklistItem>\n\t<ChecklistItem id=\"s2-replace-with-element-selector\">If yes - replace with the element selector inside the scope, remove the class from the HTML</ChecklistItem>\n\t<ChecklistItem id=\"s2-keep-non-unique-class\">If no - the element isn't semantically unique (two different div wrappers, multiple span types) - keep a class, but drop the BEM prefix. .card__content becomes .content inside the scope; the scope handles the leakage</ChecklistItem>\n</ul>\n\n### Step 3 - Handle modifier classes\n\nBEM modifiers (--) don't map directly to @scope - @scope solves leakage, not state. Modifiers still need a signal to attach to. Your options:\n\n#### Option A: Keep a simplified modifier class (recommended)\nDrop the block prefix but keep the modifier concept as a class on the root element.\n```css\n/* BEM modifier */\n.card--featured { border-color: gold; }\n\n/* @scope equivalent */\n@scope (.card) {\n  :scope.featured { border-color: gold; }\n}\n```\n\n#### Option B: Use data attributes\nCleaner semantic separation between styling hooks and JS hooks.\n```css\n/* @scope equivalent */\n@scope (.card) {\n  :scope[data-variant=\"featured\"] { border-color: gold; }\n}\n```\n\n#### Option C: Use :has() for state-driven modifiers\n\nWhere the modifier is driven by content or state rather than an explicit class.\n\n```css\n/* @scope equivalent */\n@scope (.card) {\n  :scope:has(img) { /* card has an image - adjust layout */ }\n}\n```\n\n<ul class=\"checklist\">\n\t<ChecklistItem id=\"s3-list-modifiers\">List every --modifier class in the component</ChecklistItem>\n\t<ChecklistItem id=\"s3-decide-per-modifier\">Decide per modifier: simplified class, data attribute, or :has() condition</ChecklistItem>\n\t<ChecklistItem id=\"s3-update-html-css\">Update the HTML and CSS together - don't leave orphaned modifier classes in the markup</ChecklistItem>\n</ul>\n\n### Step 4 - Migrate scoped variables\nIf you've read the design tokens section above, you already know the full picture here. The short version for the migration:\n\n<ul class=\"checklist\">\n\t<ChecklistItem id=\"s4-identify-custom-props\">Identify any custom properties currently set on the block class</ChecklistItem>\n\t<ChecklistItem id=\"s4-move-inside-scope\">Move them inside @scope - they now only apply within the component</ChecklistItem>\n\t<ChecklistItem id=\"s4-check-global-tokens\">Check that global tokens (:root variables) still resolve correctly from inside the scope - they will, but worth confirming if you're overriding the same property name</ChecklistItem>\n\t<ChecklistItem id=\"s4-context-theming\">If the component needs context-based theming (different values in a banner, a sidebar, a dark section) - set those token overrides on the parent element rather than using a modifier class on the component itself</ChecklistItem>\n</ul>\n\n### Step 5 - Add a lower boundary if components nest\nYou've seen this in the donut hole section above - just the decision checklist here:\n\n<ul class=\"checklist\">\n\t<ChecklistItem id=\"s5-check-nesting\">Does this component ever nest inside itself, or inside a component with the same element selectors?</ChecklistItem>\n\t<ChecklistItem id=\"s5-add-lower-boundary\">If yes - add a lower boundary: `@scope (.card) to (.card) { … }`</ChecklistItem>\n\t<ChecklistItem id=\"s5-skip-if-no-nesting\">If no - skip this step</ChecklistItem>\n</ul>\n\n### Step 6 - Clean up the HTML\nOnce the scoped CSS is confirmed working, the markup gets leaner.\n\n<ul class=\"checklist\">\n\t<ChecklistItem id=\"s6-remove-element-classes\">Remove all block__element classes now targeted by element selectors</ChecklistItem>\n\t<ChecklistItem id=\"s6-remove-simplified-classes\">Remove all block__element classes now targeted by simplified classes</ChecklistItem>\n\t<ChecklistItem id=\"s6-keep-block-and-modifiers\">Keep: the block class (scope root) + any remaining modifier/variant classes</ChecklistItem>\n\t<ChecklistItem id=\"s6-seven-to-one\">The card from this post goes from 7 classes to 1 - just .card on the wrapper</ChecklistItem>\n</ul>\n\nBefore:\n\n```html\n<div class=\"card\">\n  <picture class=\"card__image-container\">\n    <img class=\"card__image\" />\n  </picture>\n  <div class=\"card__content\">\n    <h3 class=\"card__heading\">Card Title</h3>\n    <p class=\"card__text\">…</p>\n    <a class=\"card__link\" href=\"#\">Learn More</a>\n  </div>\n</div>\n```\n\nAfter:\n\n```html\n<div class=\"card\">\n  <picture>\n    <img />\n  </picture>\n  <div>\n    <h3>Card Title</h3>\n    <p>…</p>\n    <a href=\"#\">Learn More</a>\n  </div>\n</div>\n```\n\n### Step 7 - Remove the BEM styles\nOnly once the scoped version is confirmed working in all target browsers.\n\n<ul class=\"checklist\">\n\t<ChecklistItem id=\"s7-delete-bem-declarations\">Delete the original BEM block and element declarations</ChecklistItem>\n  <ChecklistItem id=\"s7-search-old-classnames\">Run a search for the old class names in your templates and markup - confirm none remain</ChecklistItem>\n  <ChecklistItem id=\"s7-check-supports-fallback\">If you added an @supports fallback, confirm it's still in place and the BEM styles inside it are intentional</ChecklistItem>\n</ul>\n\n### What you're not migrating\nA few BEM patterns that @scope doesn't replace - keep these as-is:\n- Global utility classes. .visually-hidden, .sr-only, .text-center - these aren't component styles and shouldn't be scoped. Leave them alone.\n- Layout-level classes. .grid, .container, .stack - if these live above the component level, they belong outside any scope.\n- Multi-component relationships. If you have BEM classes that describe a relationship between two separate components (.card--in-sidebar, .nav--has-dropdown), @scope doesn't naturally express those. Keep them as classes or replace with container queries on the parent.\n\n### Quick reference\n\n| BEM pattern | @scope equivalent |\n| --- | --- |\n| `.block {}` | `@scope (.block) { :scope {} }` |\n| `.block__element {}` | `@scope (.block) { element {} }` |\n| `.block--modifier {}` | `@scope (.block) { :scope.modifier {} }` |\n| `.block__element--modifier {}` | `@scope (.block) { element.modifier {} }` |\n| Nested component leak | `@scope (.outer) to (.inner) {}` |",
    "version": "1.0"
  },
  "description": "How @scope finally gives CSS the style boundaries it's always needed - and why that means BEM can retire.",
  "path": "/writing/css-scope",
  "publishedAt": "2025-02-15T00:00:00.000Z",
  "site": "https://dominickjay.com",
  "tags": [
    "css"
  ],
  "textContent": "import PlaceholderImage from '../../components/PlaceholderImage.astro';\nimport InlineCallout from '../../components/InlineCallout.astro';\nimport ScopeDemo1 from '../../components/demos/ScopeDemo1.astro';\nimport ChecklistItem from '../../components/ChecklistItem.astro';\n\nAs a parent of two, I've found writing CSS can be a bit like having toddlers - you tell them to only use the red pens and somehow, blue pen appears somewhere completely unexpected. The sofa. The wall. Something you haven't seen in six months.\nCSS has the same problem. You tie up a new button style for a new part of a project and suddenly all the buttons across the site have that random border-radius or hover state. \n\nThe rules are loose and the damage spreads.\n\nEnter @scope.\n\nHow it works\n\nLet's get to the code. How do we set this up so that completely-made-up scenario doesn't even become a reality. Firstly, you use the  at-rule, following by your selector of choice - let's drag the infamous card example out here.\n\nWell...that's kind of it, right? We've it set it up. Nothing more to do here, apart from adding in our styles within those lovely braces on the . Anything inside this 'scoped' card is exclusive just to that class name. This allows us to use more generic CSS selectors, ditching the specific class/IDs like  in favor of just the  tag. Let's do a comparison between a basic card layout using both the  naming convention and .\n\nOur example card markup\n\nThis just gives us this beautifully unstyled masterpiece.\n\n  \n\t  \n  \n  \n    Card Title\n    Amet aute minim eiusmod tempor do minim eu. Eu officia amet consequat et esse pariatur Lorem proident aliqua reprehenderit esse elit. Eiusmod laborum fugiat irure culpa adipisicing velit. In consequat adipisicing ea consequat aute elit elit. Do proident veniam commodo voluptate adipisicing ullamco et ut aliqua fugiat id veniam do. Aliquip non incididunt ipsum occaecat cillum est consequat consectetur cillum. Dolore in excepteur veniam adipisicing pariatur Lorem.\n    Learn More\n  \n\nBEM vs @scope - side by side\n\nSame card. Same output. Very different markup.\n\nFor browsers that don't support  yet, wrap it in a feature query and keep your BEM styles as the fallback.\n\nThe one thing BEM couldn't solve\n\nBEM naming conventions are pretty good at stopping styles from leaking from one to another - for example  will never accidentally style a . But there's always been a blind spot of nested instances of the same component. If a  element can have another  inside it, then every BEM rule on  will apply to both the inner and outer card. No mistake, no weird gotcha. That's just how it works. There's no way to specifically target just the 'outer' one without adding extra classes e.g. , , or using specificity hacks. With , it has that same problem, but it also has a fix built in to rectify it.\n\nIt's cards all the way down\n\nSo what's the fix? We can use the  keyword to define where a scope stops:\n\nThe scope now ranges in the ring between the outer  and any of the  elements inside it. Styles reach the  within the containing outer , but stop before reaching any inner  elements. The nested card gets its own clean scope when its own @scope rule kicks in.\n\nThis is sometimes called the donut hole - a term coined by Nicole Sullivan in 2011 - meaning the scope has a gap punched through it at the lower boundary. Styles apply in the ring, not inside the hole. It's an odd analogy, but it sticks.\n\nWhen do you actually need to use this?\n\nNot every component needs a lower boundary - most cards won't nest, and hopefully we never enter a hellscape where developers are putting buttons inside buttons. But it's worth reaching for whenever:\nA component can genuinely contain itself (comments with replies that are also comments, dropdown menu containing a nested submenu)\nTwo different components share element selectors (h3, img, a) and one can appear inside the other\n\nFor the card we've built in this post, you probably don't need it. But now you know it exists for when you do!\n\nTheming without modifier classes\n\nYou might have noticed the  card example already does something quiet but useful with custom properties:\n\nThat  value lives within the scope, and overrides whatever  is set to globally, but only within this card component. Everything else is given mercy and left alone completely.\n\nThe problem with global tokens\n\nDesign token systems typically live on . A colour palette, a spacing scale, a type ramp - all declared globally, all inherited everywhere. That's fine, until you need a component to look different in a specific context. Let's use an example of two cards in a promotional banner needing both light and dark backgrounds. When we're using global tokens, we have to either write separate styles for each variant or use modifier classes/data attributes to switch between them. Both approaches mean the component's styles are duplicated across variants.\n\nOf course, this works, but the modifier class is doing two jobs: it's both a style hook and a token override. As the number of variants grows, so does the amount of maintenance needed, the technical debt has amassed and lastly the code becomes harder to read.\n\nThe scoped token pattern\n\n lets you redefine tokens at the component boundary without a modifier class. The scope root becomes the theming surface:\n\nThe card component itself hasn't changed. The  parent reestablishes the tokens, and the card gets the new values through the cascade. One component has been defined, but it can work in any number of contexts.\n\nThe missing layer\n\nA typical token/design system works mainly in one direction: it gets declared globally and is then taken on by components. Changing the global value will affect all components that use it - which is fantastic for rebranding a project effectively and with relative ease. But what if you need just one instance of a component to behave differently from all the others? A \"Featured\" card that sits at the top of a content feed for example, one that uses the same markup, same component, but has a dark background instead of light. What then?\n\nScoped tokens get involved in between the global and component layers. They let you redefine tokens at the context level, without the need to touch the component or the global defaults. This means that you can have a single instance of a component behave differently from all the other ones, without needing to modify the component itself or those site-wide defaults.\n\nFurther to this, we can use it alongside the lower boundary mentioned earlier, using the same functionality to stop styles leaking into other components.\n\nMigrate from BEM: A Practical Checklist\n\nSo, if the above sounds like a good fit for the next time you're building out a component, or even better, get the budget to refactor an old one, the checklist below will help you get started. This checklist walks through how to go about replacing BEM class names with  on a component, using our card example above as a reference.\n\nBefore you start\n\n\tCheck your browser support requirements.  is supported in Chrome 118+, Firefox 120+, and Safari 17.2+. If you're supporting anything older, wrap your scoped styles in a feature query and keep BEM as the fallback:  - it adds two lines and costs nothing.\n\tPick one component to migrate first. Don't rewrite everything. Choose a self-contained component - a card, a nav item, a badge - and use it as your proof of concept before touching shared or global styles.\n\tHave the original BEM styles open alongside. You're not deleting them yet. Migration goes: write the scoped version → confirm it works → remove the BEM version.\n\nStep 1 - Identify your scope root\nYour BEM block becomes your @scope selector. The block class is the only class you need to keep on the HTML element.\n\n\tIdentify the block class - this becomes your @scope (.block-name) selector\n\tKeep the block class on the HTML element (.card, .nav, .badge) - it's now the scope root, not just a selector\n\tRemove the block prefix from every element class in your markup - cardimage becomes image, or just gets removed in favour of an element selector (see step 2)\n\nStep 2 - Replace element classes with element selectors where possible\nThis is the cleanest win. BEM element classes exist to prevent style leakage - @scope handles that, so you can target img, h3, p, a directly inside the scope without them bleeding out.\n\n\tFor each BEM element class, ask: is this targeting a semantically unique element within the component? (image → img, heading → h3)\n\tIf yes - replace with the element selector inside the scope, remove the class from the HTML\n\tIf no - the element isn't semantically unique (two different div wrappers, multiple span types) - keep a class, but drop the BEM prefix. .cardcontent becomes .content inside the scope; the scope handles the leakage\n\nStep 3 - Handle modifier classes\n\nBEM modifiers (--) don't map directly to @scope - @scope solves leakage, not state. Modifiers still need a signal to attach to. Your options:\n\nOption A: Keep a simplified modifier class (recommended)\nDrop the block prefix but keep the modifier concept as a class on the root element.\n\nOption B: Use data attributes\nCleaner semantic separation between styling hooks and JS hooks.\n\nOption C: Use :has() for state-driven modifiers\n\nWhere the modifier is driven by content or state rather than an explicit class.\n\n\tList every --modifier class in the component\n\tDecide per modifier: simplified class, data attribute, or :has() condition\n\tUpdate the HTML and CSS together - don't leave orphaned modifier classes in the markup\n\nStep 4 - Migrate scoped variables\nIf you've read the design tokens section above, you already know the full picture here. The short version for the migration:\n\n\tIdentify any custom properties currently set on the block class\n\tMove them inside @scope - they now only apply within the component\n\tCheck that global tokens (:root variables) still resolve correctly from inside the scope - they will, but worth confirming if you're overriding the same property name\n\tIf the component needs context-based theming (different values in a banner, a sidebar, a dark section) - set those token overrides on the parent element rather than using a modifier class on the component itself\n\nStep 5 - Add a lower boundary if components nest\nYou've seen this in the donut hole section above - just the decision checklist here:\n\n\tDoes this component ever nest inside itself, or inside a component with the same element selectors?\n\tIf yes - add a lower boundary: \n\tIf no - skip this step\n\nStep 6 - Clean up the HTML\nOnce the scoped CSS is confirmed working, the markup gets leaner.\n\n\tRemove all blockelement classes now targeted by element selectors\n\tRemove all blockelement classes now targeted by simplified classes\n\tKeep: the block class (scope root) + any remaining modifier/variant classes\n\tThe card from this post goes from 7 classes to 1 - just .card on the wrapper\n\nBefore:\n\nAfter:\n\nStep 7 - Remove the BEM styles\nOnly once the scoped version is confirmed working in all target browsers.\n\n\tDelete the original BEM block and element declarations\n  Run a search for the old class names in your templates and markup - confirm none remain\n  If you added an @supports fallback, confirm it's still in place and the BEM styles inside it are intentional\n\nWhat you're not migrating\nA few BEM patterns that @scope doesn't replace - keep these as-is:\nGlobal utility classes. .visually-hidden, .sr-only, .text-center - these aren't component styles and shouldn't be scoped. Leave them alone.\nLayout-level classes. .grid, .container, .stack - if these live above the component level, they belong outside any scope.\nMulti-component relationships. If you have BEM classes that describe a relationship between two separate components (.card--in-sidebar, .nav--has-dropdown), @scope doesn't naturally express those. Keep them as classes or replace with container queries on the parent.\n\nQuick reference\n\n| BEM pattern | @scope equivalent |\n| --- | --- |\n|  |  |\n|  |  |\n|  |  |\n|  |  |\n| Nested component leak |  |",
  "title": "Put Your CSS In Its Place with @scope"
}