v130 · css

Migration Guide

Step-by-step migration from SASS/SCSS nesting to native CSS nesting with Chrome 130's stable bare declarations. Each step shows the before/after diff and how CSSNestedDeclarations preserves source order.

Feature detection: checking…
Step 1 — Basic nesting. SASS lets you nest selectors inside a parent. Native CSS does the same. The key difference: in native CSS, bare declarations before any nested rule are identical in behaviour to SASS. No change needed.
SASS / SCSS
.card {
  background: white;
  border: 1px solid #ccc;
  padding: 1rem;

  &__title {
    font-size: 1.25rem;
    font-weight: 700;
  }

  &__body {
    color: #555;
    line-height: 1.6;
  }
}
Native CSS (Chrome 130)
.card {
  background: white;
  border: 1px solid #ccc;
  padding: 1rem;

  & .card__title {
    font-size: 1.25rem;
    font-weight: 700;
  }

  & .card__body {
    color: #555;
    line-height: 1.6;
  }
}

✓ No behavioural change

Bare declarations before nested rules work identically in SASS and native CSS.

ℹ BEM: use descendant selector

SASS's &__title compiles to .card__title. Native CSS nesting uses a descendant: & .card__title.

Step 2 — Pseudo-classes. &:hover, &:focus, &:disabled — these are identical between SASS and native CSS. Direct copy-paste works.
SASS / SCSS
.btn {
  background: #2855ad;
  color: white;

  &:hover {
    background: #1a3a80;
  }

  &:focus-visible {
    outline: 2px solid #2855ad;
    outline-offset: 2px;
  }

  &:disabled {
    opacity: 0.45;
    cursor: not-allowed;
  }
}
Native CSS (identical)
.btn {
  background: #2855ad;
  color: white;

  &:hover {
    background: #1a3a80;
  }

  &:focus-visible {
    outline: 2px solid #2855ad;
    outline-offset: 2px;
  }

  &:disabled {
    opacity: 0.45;
    cursor: not-allowed;
  }
}

✓ Zero changes needed

Pseudo-class nesting syntax is identical. Paste as-is.

ℹ Specificity unchanged

Native nesting computes the same specificity as the SASS-compiled output.

Step 3 — Late overrides (the Chrome 130 fix). In SASS, bare declarations written after a nested rule stay after the nested rule in the compiled output. Pre-Chrome-130 native CSS hoisted those declarations before the nested rule, breaking source-order cascades. Chrome 130 wraps them in CSSNestedDeclarations, restoring correct behaviour.
Pre-Chrome-130 (BROKEN)
.chip {
  background: #eee;
  color: #222;

  &.active {
    background: #2855ad;
    color: white;
  }

  /* OLD: this was hoisted BEFORE &.active → */
  /* .active override could beat it. BUG.     */
  background: #ddd;  ← hoisted, wrong order
}
Chrome 130 (CORRECT)
.chip {
  background: #eee;
  color: #222;

  &.active {
    background: #2855ad;
    color: white;
  }

  /* Chrome 130: wrapped in CSSNestedDeclarations */
  /* stays in source order — wins over &.active   */
  background: #ddd;  ← correct position ✓
}

⚠ Breaking change in pre-130

If your SASS relied on late bare declarations overriding nested rules, pre-130 browsers may render incorrectly.

✓ Chrome 130 matches SASS

CSSNestedDeclarations wraps late bare decls so source order is preserved — matching compiled SASS output.

Step 4 — SASS mixins → @layer / @property. SASS mixins inject declarations inline. In native CSS, use @layer for grouping or @property for typed tokens. There is no direct mixin equivalent, but the cascade gives you similar control.
SASS @mixin
// SASS mixin definition
@mixin focus-ring {
  outline: 2px solid currentColor;
  outline-offset: 2px;
}

.btn {
  background: blue;
  color: white;

  &:focus-visible {
    // include mixin
    @include focus-ring;
  }
}
Native CSS — @layer approach
/* Shared styles in a base layer */
@layer base {
  :focus-visible {
    outline: 2px solid currentColor;
    outline-offset: 2px;
  }
}

.btn {
  background: blue;
  color: white;

  /* :focus-visible inherits from @layer base */
  &:focus-visible {
    /* override only if needed */
  }
}

ℹ No 1:1 mixin equivalent

Native CSS has no mixin system. Use @layer for shared base styles, or custom properties for dynamic values.

✓ Nesting works inside @layer

All CSS nesting syntax is valid inside @layer blocks.

Step 5 — @media queries inside nesting. Native CSS allows @media queries directly inside a rule, exactly like SASS. This works in Chrome 130 and all modern browsers.
SASS / SCSS
.grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1rem;

  @media (min-width: 640px) {
    grid-template-columns: 1fr 1fr;
  }

  @media (min-width: 1024px) {
    grid-template-columns: repeat(3, 1fr);
  }
}
Native CSS (identical)
.grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1rem;

  @media (min-width: 640px) {
    grid-template-columns: 1fr 1fr;
  }

  @media (min-width: 1024px) {
    grid-template-columns: repeat(3, 1fr);
  }
}

✓ Zero changes needed

@media inside a rule works identically in native CSS. The implied & is applied automatically.

ℹ @container works too

@container queries inside rules follow the same pattern.

see also