HTML & CSS

CSS Nesting, @layer, @scope та @property: нативний препроцесор

Глибоке занурення у сучасний CSS-синтаксис: нативне вкладення правил (nesting), каскадні шари (@layer), обмеження стилів (@scope) та типізовані Custom Properties (@property). Замінюємо SCSS нативним CSS.

CSS Nesting, @layer, @scope та @property: нативний препроцесор

Коли SCSS перестав бути обов'язковим

Протягом понад десяти років Sass/SCSS був негласним стандартом у CSS-розробці. Причина проста: нативний CSS не мав вкладення правил, не мав пріоритетних шарів, не міг анімувати кастомні властивості, не мав засобів ізоляції стилів. Sass заповнював ці прогалини.

З 2023–2024 років ситуація кардинально змінилась. CSS отримав нативне вкладення (Nesting), каскадні шари (@layer), звуження стилів (@scope) та типізовані Custom Properties (@property). Для більшості проєктів ці чотири специфікації замінюють те, навіщо раніше потрібен був препроцесор.

Ця стаття — практичне порівняння: що ви робили у SCSS, і як це виглядає тепер у нативному CSS.

Loading diagram...
graph LR
    subgraph SCSS ["SCSS (раніше)"]
        S1["&:hover { }"]
        S2["@mixin button { }"]
        S3["!default variables"]
        S4["@extend .class"]
    end
    subgraph CSS ["Native CSS (зараз)"]
        C1["&:hover { } ← Nesting"]
        C2["@layer + custom props ← Cascade Layers"]
        C3["@property ← Typed Custom Props"]
        C4["@scope ← Style Scoping"]
    end
    S1 -->|"замінено"| C1
    S2 -->|"замінено"| C2
    S3 -->|"замінено"| C3
    S4 -->|"замінено"| C4
    style SCSS fill:#c084fc,color:#fff,stroke:#9333ea
    style CSS fill:#3b82f6,color:#fff,stroke:#2563eb

CSS Nesting — вкладення правил

Проблема: дублювання селекторів

У класичному CSS стилізація компонента з кількома станами виглядала так:

/* Класичний CSS — багато повторень */
.card { background: white; border-radius: 8px; }
.card:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
.card.active { border-color: #6366f1; }
.card__title { font-size: 1.25rem; color: #1e293b; }
.card__title:hover { color: #6366f1; }
.card__body { padding: 1rem; color: #64748b; }
.card__footer { border-top: 1px solid #e2e8f0; padding: 0.75rem; }

Щоразу потрібно повторювати .card, що важко підтримувати при перейменуванні. SCSS вирішував це через вкладення — і тепер це вміє нативний CSS.

Базовий синтаксис CSS Nesting

Підтримка: Chrome 112+, Firefox 117+, Safari 17+

/* Native CSS Nesting */
.card {
  background: white;
  border-radius: 8px;

  /* Вкладений псевдоклас */
  &:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  }

  /* Вкладена модифікація класу */
  &.active {
    border: 2px solid #6366f1;
  }

  /* Вкладений дочірній елемент (BEM) */
  & .card__title {
    font-size: 1.25rem;
    color: #1e293b;

    /* Ще глибше вкладення */
    &:hover {
      color: #6366f1;
    }
  }

  /* Вкладений псевдоелемент */
  &::before {
    content: '';
    display: block;
  }
}

Символ & — це посилання на батьківський селектор. Без нього CSS не знає, до якого елемента застосовувати вкладене правило.

🔒localhost:3000

Вкладання медіа-запитів

CSS Nesting дозволяє вкладати @media безпосередньо всередині правила:

.hero {
  padding: 2rem;
  font-size: 1rem;

  /* Замість окремого @media { .hero { } } */
  @media (min-width: 768px) {
    padding: 4rem;
    font-size: 1.25rem;
  }

  @media (min-width: 1200px) {
    padding: 6rem;
    font-size: 1.5rem;
  }
}
🔒localhost:3000

Порівняння SCSS vs нативний CSS

// Без компілятора — не працює в браузері
.button {
  padding: 0.6em 1.2em;
  background: $primary;
  border-radius: 6px;
  transition: all 0.15s;

  &:hover {
    background: darken($primary, 10%);
  }

  &:focus-visible {
    outline: 2px solid $primary;
    outline-offset: 2px;
  }

  &--large {
    padding: 0.8em 1.6em;
    font-size: 1.125rem;
  }

  &--ghost {
    background: transparent;
    border: 2px solid $primary;
    color: $primary;

    &:hover {
      background: rgba($primary, 0.1);
    }
  }

  @include respond-to('md') {
    padding: 0.75em 1.5em;
  }
}
darken($color, 10%) із Sass тепер замінює color-mix(in srgb, var(--color) 85%, black) — нативна CSS-функція, що стала доступною у 2023 році. Детальніше — у статті про сучасні можливості CSS.

@layer — Каскадні шари (Cascade Layers)

Проблема специфічності

Уявіть: ви підключаєте бібліотеку компонентів. Вона задає .button { background: blue } з певною специфічністю. Ваш код задає .button { background: green }. Хто переможе? Залежить від порядку підключення та специфічності — і це часто непередбачувано.

Ця проблема існувала роками: CSS-in-JS, BEM, CSS Modules — всі вони по-різному намагались її вирішити. @layer дає нативне рішення: явно оголошений пріоритет шарів.

Підтримка: Chrome 99+, Firefox 97+, Safari 15.4+

Синтаксис @layer

/* Крок 1: оголосити порядок шарів (від найнижчого до найвищого пріоритету) */
@layer base, components, utilities;

/* Крок 2: наповнити шари */
@layer base {
  /* Базові стилі, CSS Reset */
  * { box-sizing: border-box; }
  body { margin: 0; font-family: system-ui, sans-serif; }
  h1, h2, h3 { line-height: 1.2; }
}

@layer components {
  /* Стилі компонентів */
  .button {
    padding: 0.6em 1.2em;
    background: #6366f1;
    color: white;
    border-radius: 6px;
    border: none;
    cursor: pointer;
  }
}

@layer utilities {
  /* Утиліти мають найвищий пріоритет серед шарів */
  .hidden { display: none !important; }
  .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); }
}

/* Стилі поза @layer мають НАЙВИЩИЙ пріоритет — вище всіх шарів */
.button {
  background: #ec4899; /* Переможе компонент у @layer */
}
🔒localhost:3000

@layer для інтеграції сторонніх бібліотек

Найпрактичніший сценарій — ізолювати стилі бібліотеки у шарі з найнижчим пріоритетом:

/* Ваші власні шари — оголошені першими */
@layer reset, third-party, base, components, utilities;

/* Стилі бібліотеки — потрапляють у шар з низьким пріоритетом */
@import url('./normalize.css') layer(reset);
@import url('./bootstrap.css') layer(third-party);

/* Ваші компоненти ЗАВЖДИ перевизначать бібліотеку */
@layer components {
  .button {
    /* Ці стилі матимуть вищий пріоритет за Bootstrap .button,
       навіть без !important та без підвищення специфічності */
    background: var(--brand);
  }
}
Стилі, записані без@layer, мають пріоритет вище будь-якого шару незалежно від специфічності. Це дозволяє зберегти «аварійний вихід» для критичних перевизначень.

@scope — Обмеження стилів

Проблема: стилі, що «витікають»

CSS-селектори глобальні за замовчуванням. .title у кухонному таймері може вплинути на .title у модальному вікні, навіть якщо вони далеко у DOM. Це проблема, яку вирішують CSS Modules, Shadow DOM, BEM-конвенції. @scope — нативне вирішення.

Підтримка: Chrome 118+, Safari 17.4+, Firefox (у розробці)

/* Стилі активні ЛИШЕ всередині .card */
@scope (.card) {
  /* Тут можна писати короткі селектори без боязні конфліктів */
  .title {
    font-size: 1.25rem;
    color: #1e293b;
  }

  .body {
    color: #64748b;
  }

  /* Виключення: стилі НЕ торкнуться .nested-card та його нащадків */
  @scope (.card) to (.nested-card) {
    .title { color: #6366f1; }
  }
}
🔒localhost:3000

@property — Типізовані Custom Properties

Чому звичайні CSS змінні не можна анімувати

CSS Custom Properties (--color: #6366f1) — потужний інструмент, але вони мають обмеження: браузер не знає їх типу. Для нього --color — просто рядок. Тому transition: --color 0.3s не спрацює — браузер не знає, як інтерполювати «від рядка до рядка».

@property вирішує це, дозволяючи зареєструвати кастомну властивість із типом, початковим значенням та поведінкою наслідування.

Підтримка: Chrome 85+, Safari 16.4+, Firefox 128+

@property --hue {
  syntax: '<number>';         /* тип: число */
  inherits: false;            /* не наслідується від батька */
  initial-value: 0;           /* початкове значення */
}

@property --brand-color {
  syntax: '<color>';
  inherits: true;
  initial-value: #6366f1;
}

@property --progress {
  syntax: '<percentage>';
  inherits: false;
  initial-value: 0%;
}

Анімація Custom Properties через @property

🔒localhost:3000

Зверніть: animation: hue-rotate 4s linear infinite плавно анімує --hue від 240 до 600, а hsl(var(--hue), 80%, 60%) автоматично оновлює градієнт. Без @property такого не досягти — браузер не знав би, що --hue — це число, між яким можна інтерполювати.

@property для типізованих дефолтів

/* Без @property: якщо --size не задано, результат непередбачуваний */
.box {
  width: var(--size); /* undefined? '0'? */
}

/* З @property: завжди є initial-value */
@property --box-size {
  syntax: '<length>';
  inherits: false;
  initial-value: 100px; /* гарантований fallback */
}

.box {
  width: var(--box-size); /* завжди 100px, якщо не перевизначено */
}

/* Можна перевизначати як звичайно */
.box--large {
  --box-size: 200px;
}

Нативний CSS замість SCSS: практичне порівняння

🔒localhost:3000

Практика

Рівень 1 — Рефакторинг SCSS у Native CSS

Перепишіть цей SCSS у нативний CSS, зберігши всю функціональність:

.nav {
  display: flex;
  gap: 0.5rem;
  padding: 0.75rem;
  background: #1e293b;

  &__link {
    color: #94a3b8;
    padding: 0.5rem 1rem;
    border-radius: 6px;
    text-decoration: none;
    transition: all 0.15s;

    &:hover { color: white; background: rgba(255,255,255,0.1); }
    &.active { color: white; background: #6366f1; }
  }

  @include respond-to('sm') {
    flex-direction: column;
  }
}

Рівень 2 — @layer система з пріоритетами

Створіть CSS-систему для бібліотеки компонентів:

  1. Оголосіть шари: reset, tokens, components, overrides
  2. У reset — скидання margin, padding, box-sizing
  3. У tokens — змінні --color-primary, --spacing-base, --radius
  4. У components — компонент .card з базовими стилями
  5. У overrides — темна тема, що перевизначає кольори картки

Переконайтесь, що overrides перемагає components без !important.

Рівень 3 — Анімована картка через @property

Створіть картку товару з такими анімованими властивостями:

  • --card-elevation (тип <number>) — анімує box-shadow при hover
  • --card-accent (тип <color>) — анімує колір акцентного бордера від #e2e8f0 до #6366f1 при hover
  • --badge-progress (тип <percentage>) — запускає conic-gradient «прогрес» при завантаженні сторінки

Використайте @property + CSS Nesting для компактного коду.


Підсумок

🔗 CSS Nesting

& посилається на батьківський селектор. Вкладайте псевдокласи, псевдоелементи, модифікатори та @media прямо у правило компонента. Chrome 112+, FF 117+, Safari 17+.

📚 @layer

Явний пріоритет каскадного шару: @layer base, components, utilities. Стилі вище у списку — нижчий пріоритет. Без шару — найвищий. Ізолюйте бібліотеки через @import ... layer(third-party).

🔒 @scope

@scope (.card) { .title { } } — стилі діють лише всередині .card. Замінює BEM-префікси та CSS Modules для базових сценаріїв. Chrome 118+, Safari 17.4+.

⚡ @property

Типізовані custom properties: syntax, inherits, initial-value. Дозволяє анімувати var(--color), var(--progress), var(--angle). Без @property — лише статичні значення.