HTML & CSS

Сучасний CSS 2023–2025: Нові можливості

Огляд найсвіжіших CSS-специфікацій: :has() selector, CSS Anchor Positioning, Scroll-Driven Animations, @starting-style, color-mix(), light-dark(), View Transitions API, @scope, relative color syntax, і багато іншого.

Сучасний CSS 2023–2025: Нові можливості

CSS розвивається швидше, ніж будь-коли

Якщо ви вивчали CSS п'ять років тому і думаєте, що знаєте мову — вас очікує приємний сюрприз. Починаючи з 2023 року, CSS отримав більше нових можливостей, ніж за попередні десять років разом узяті. Interop 2022–2024 — спільний проєкт Chrome, Firefox та Safari щодо синхронізованої підтримки стандартів — прискорив впровадження десятків специфікацій.

Ця стаття — не "як писати CSS". Це "що CSS вміє зараз, чого не вмів раніше". Деякі з цих можливостей замінять ваш JavaScript. Деякі — ваш Sass. Деякі — просто відкриють нові дизайнерські рішення, що раніше були неможливі.

Розглянемо можливості, згруповані за тим, яку проблему вони вирішують.


Проблема 1: "Батьківський селектор" — 15 років очікування

Протягом усієї історії CSS існував запит, якому відмовляли: вибрати батьківський елемент залежно від його дочірніх. Як стилізувати <li>, якщо він містить <ul> (тобто є вкладеним пунктом)? Як стилізувати <label>, якщо його <input> має фокус?

Відповідь завжди була: "через JavaScript". До 2023 року.

:has() — Relational Pseudo-class

:has() — найважливіший CSS-селектор за останнє десятиліття. Він дозволяє вибрати елемент залежно від наявності певних нащадків або сусідів. Через це його неофіційно називають "батьківський селектор", хоча насправді він ширший.

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

/* Стилізуй <li>, якщо він містить <ul> — вкладений список */
li:has(> ul) {
    font-weight: bold;
    border-left: 3px solid #6366f1;
}

/* Стилізуй <label>, якщо його input у стані :focus */
label:has(+ input:focus) {
    color: #6366f1;
}

/* Стилізуй <form>, якщо у ньому є хоча б один невалідний input */
form:has(input:invalid) {
    border: 2px solid #ef4444;
}

/* <article> має зображення — дай йому особливий layout */
article:has(img) {
    display: grid;
    grid-template-columns: 200px 1fr;
}

/* Стилізуй картку-контейнер, якщо у ній більше 3 елементів */
.grid:has(> :nth-child(3)) {
    grid-template-columns: repeat(3, 1fr);
}

Логіка роботи: .a:has(.b) означає "вибрати .a, якщо в ньому є .b". Усередині :has() можна використовувати будь-який валідний CSS-селектор, включаючи >, ~, + та складені селектори.

:has() як умовний CSS без JavaScript

До появи :has() перемикання класів на батьківських елементах вимагало JavaScript. Тепер це можна робити через CSS, реагуючи на стани дочірніх елементів:

Preview
×
🔒localhost:3000

:is() та :where() — спрощення складних селекторів

Поки :has() — найгучніша новинка, :is() та :where() також суттєво спрощують CSS:

/* Без :is() — довгий список */
h1 a,
h2 a,
h3 a,
h4 a,
h5 a,
h6 a {
    color: inherit;
}

/* З :is() — коротко і зрозуміло */
:is(h1, h2, h3, h4, h5, h6) a {
    color: inherit;
}

/* :is() наслідує специфічність найбільш специфічного аргументу */
/* :where() — специфічність завжди 0, легко перевизначити */
:where(h1, h2, h3) {
    line-height: 1.2; /* Специфічність: 0,0,0 — легко перевизначити */
}

:where() ідеальний для CSS Reset та базових стилів — вони не будуть конфліктувати зі стилями компонентів.


Проблема 2: Анімація при появі/зникненні елементів

Протягом десятиліть розробники використовували JavaScript, щоб анімувати появу та зникнення елементів — бо CSS не міг анімувати display: none ↔ block. Тепер ситуація змінилась.

@starting-style — анімація при першій появі

@starting-style дозволяє визначити початковий стан елемента саме для моменту його першої появи в DOM або переходу з display: none. Без @starting-style transition не спрацьовував — браузер не міг інтерполювати "від нічого".

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

.dialog {
    opacity: 1;
    transform: scale(1) translateY(0);
    transition:
        opacity 0.3s ease,
        transform 0.3s ease,
        display 0.3s ease allow-discrete; /* Вказуємо дискретне display */
}

/* Кінцевий стан при [hidden] або display:none */
.dialog[hidden] {
    opacity: 0;
    transform: scale(0.95) translateY(10px);
    display: none;
}

/* Початковий стан при ПОЯВІ (before-open state) */
@starting-style {
    .dialog {
        opacity: 0;
        transform: scale(0.95) translateY(10px);
    }
}

overlay та allow-discrete — анімація display

Нова властивість transition-behavior: allow-discrete дозволяє анімувати дискретні (непоступові) властивості: display, visibility, overlay. Без неї display: none вмикалося миттєво, ламаючи анімацію виходу.

.tooltip {
    opacity: 1;
    display: block;
    /* allow-discrete: transition чекає завершення перед застосуванням display:none */
    transition:
        opacity 0.2s ease,
        display 0.2s allow-discrete;
}

.tooltip.hidden {
    opacity: 0;
    display: none; /* Тепер чекатиме завершення opacity-transition */
}

@starting-style {
    .tooltip {
        opacity: 0; /* Стартує з прозорого при появі */
    }
}
Preview
×
🔒localhost:3000

Проблема 3: Анімація прив'язана до скролу

Раніше будь-яка анімація "при скролі" (parallax, sticky progress bar, reveal effects) вимагала JavaScript: IntersectionObserver, scroll event listener, бібліотеки типу GSAP. Тепер — тільки CSS.

Scroll-Driven Animations

Scroll-Driven Animations — одна з найпотужніших CSS-специфікацій 2023 року. Вона дозволяє пов'язати прогрес CSS-анімації зі скролом сторінки або контейнера.

Підтримка: Chrome 115+, Firefox 110+ (за прапором до 130), Safari 18+

Є два типи таймлайнів:

scroll() — прогрес анімації = позиція скролу контейнера:

@keyframes progress-fill {
    from {
        width: 0%;
    }
    to {
        width: 100%;
    }
}

.reading-progress {
    position: fixed;
    top: 0;
    left: 0;
    height: 4px;
    background: #6366f1;
    /* animation-timeline прив'язаний до скролу :root */
    animation: progress-fill linear;
    animation-timeline: scroll(root);
    animation-fill-mode: both;
}

view() — прогрес анімації = видимість елемента у viewport:

@keyframes fade-up {
    from {
        opacity: 0;
        transform: translateY(30px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

.section {
    /* Анімується, коли section входить у viewport */
    animation: fade-up linear both;
    animation-timeline: view();
    /* Запуск при входженні в 10-40% viewport */
    animation-range: entry 0% cover 30%;
}

Зверніть: без animation-duration — тривалість визначається скролом, а не часом. Це принципово нова концепція: анімація не "грає" — вона "скрубується" пальцем.

animation-timeline з іменованими таймлайнами

Для більш складних сценаріїв — прокрутка дочірнього контейнера анімує батьківський елемент:

/* Контейнер реєструє себе як scroll timeline */
.carousel {
    overflow-x: scroll;
    scroll-timeline-name: --carousel-scroll;
    scroll-timeline-axis: x;
}

/* Індикатор реагує на прокрутку карусельного контейнера */
.carousel-indicator {
    animation: indicator-grow linear;
    animation-timeline: --carousel-scroll;
}

@keyframes indicator-grow {
    from {
        width: 0;
    }
    to {
        width: 100%;
    }
}

Проблема 4: Tooltip та Popover без JavaScript-позиціонування

Позиціонування tooltip, popover, dropdown відносно trigger-елемента завжди вимагало JavaScript: обчислення координат, обробка виходу за межі viewport, getBoundingClientRect(). CSS Anchor Positioning вирішує це нативно.

CSS Anchor Positioning

CSS Anchor Positioning дозволяє позиціонувати один елемент відносно іншого ("якоря") — навіть якщо вони знаходяться в різних місцях DOM.

Підтримка: Chrome 125+, Chromium-браузери. Firefox та Safari — в розробці.

/* Крок 1: зареєструвати якір */
.trigger-button {
    anchor-name: --my-anchor;
}

/* Крок 2: позиціонувати tooltip відносно якоря */
.tooltip {
    position: absolute;
    position-anchor: --my-anchor;

    /* Розмістити знизу-по-центру якоря */
    top: anchor(bottom);
    left: anchor(center);
    transform: translateX(-50%);
}

Магія: anchor(bottom) повертає координату нижньої межі якірного елемента. Доступні anchor(top), anchor(right), anchor(left), anchor(center), anchor(start), anchor(end).

Автоматична зміна позиції через @position-try

Найцікавіша частина — @position-try. Якщо tooltip виходить за межі viewport — браузер автоматично пробує альтернативні позиції:

@position-try --flip-up {
    /* Якщо знизу немає місця — показати зверху */
    top: auto;
    bottom: anchor(top);
}

.tooltip {
    position: absolute;
    position-anchor: --my-anchor;
    top: anchor(bottom);
    left: anchor(center);

    /* Автоматично flip, якщо не вміщується знизу */
    position-try-fallbacks: --flip-up;
}

Це замінює сотні рядків JavaScript у бібліотеках типу Floating UI та Popper.js.


Проблема 5: Кольори, що "розуміють" контекст

color-mix() — змішування кольорів у CSS

До color-mix() не можна було "затемнити колір на 20%" без JavaScript або Sass. Тепер:

Підтримка: Chrome 111+, Firefox 113+, Safari 16.2+

:root {
    --brand: #6366f1;

    /* Затемнений варіант: 80% brand + 20% чорний */
    --brand-dark: color-mix(in srgb, var(--brand) 80%, black);

    /* Освітлений: 70% brand + 30% білий */
    --brand-light: color-mix(in srgb, var(--brand) 70%, white);

    /* Напівпрозорий: 50% brand + 50% transparent */
    --brand-20: color-mix(in srgb, var(--brand) 20%, transparent);
}

/* Динамічний hover без Sass */
.btn-primary {
    background: var(--brand);
}
.btn-primary:hover {
    background: color-mix(in srgb, var(--brand) 85%, black);
}

Простір кольорів in srgb можна замінити на in oklch, in hsl, in display-p3 — різні простори дають різний результат змішування, деякі більш "природні" для людського зору.

light-dark() — автоматичний вибір для темної/світлої теми

light-dark() — чистий CSS-синтаксис для оголошення двох значень: одне для light, інше для dark тем. Замінює громіздкі @media блоки:

Підтримка: Chrome 123+, Firefox 120+, Safari 17.5+

/* Увімкнути підтримку color-scheme */
:root {
    color-scheme: light dark;
}

.card {
    /* light-dark(світле_значення, темне_значення) */
    background: light-dark(#ffffff, #1e293b);
    color: light-dark(#0f172a, #f1f5f9);
    border-color: light-dark(#e2e8f0, #334155);
}

.btn-primary {
    background: light-dark(#6366f1, #818cf8);
    color: light-dark(white, #1e293b);
}

Без жодного @media (prefers-color-scheme: dark) блоку. Один рядок — дві теми.

Relative Color Syntax — відносне перетворення кольорів

Відносний синтаксис кольорів дозволяє взяти існуючий колір і модифікувати окремі канали:

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

:root {
    --brand: #6366f1;
}

.element {
    /* Взяти --brand, перевести в hsl, змінити тільки lightness */
    background: hsl(from var(--brand) h s 80%);

    /* Взяти --brand в oklch, зробити прозорим на 50% */
    border-color: oklch(from var(--brand) l c h / 50%);

    /* Зменшити насиченість у два рази */
    color: hsl(from var(--brand) h calc(s / 2) l);
}

Де h, s, l — це автоматично розкладені канали вихідного кольору. Тепер генерація палітр — чистий CSS.

Preview
×
🔒localhost:3000

Проблема 6: Popover без JavaScript

HTML popover атрибут + CSS :popover-open — нативний механізм для tooltip, dropdown, modal без жодного JavaScript для базового відкриття/закриття.

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

<!-- popovertarget вказує id popover-елемента -->
<button popovertarget="my-menu">Відкрити меню</button>

<!-- popover="auto": закривається при кліку поза ним -->
<div id="my-menu" popover="auto">
    <ul>
        <li><a href="#">Пункт 1</a></li>
        <li><a href="#">Пункт 2</a></li>
    </ul>
</div>
/* Стилізація закритого стану */
[popover] {
    border: 1px solid #e2e8f0;
    border-radius: 10px;
    padding: 0.5rem;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);

    /* Анімація появи через @starting-style */
    transition:
        opacity 0.2s ease,
        transform 0.2s ease,
        display 0.2s allow-discrete,
        overlay 0.2s allow-discrete;
    opacity: 1;
    transform: translateY(0);
}

/* Закритий стан */
[popover]:not(:popover-open) {
    opacity: 0;
    transform: translateY(-8px);
    display: none;
}

/* Початковий стан анімації при появі */
@starting-style {
    [popover]:popover-open {
        opacity: 0;
        transform: translateY(-8px);
    }
}

popover="auto" — один попап за раз, закривається при кліку поза ним або Escape. popover="manual" — повний контроль вручну. Жодного event.stopPropagation(), жодних document.addEventListener('click').


Проблема 7: Плавні переходи між сторінками

View Transitions API

View Transitions — CSS-механізм для плавних переходів між різними станами DOM. Спочатку розроблявся для SPA, але тепер підтримує і звичайні навігації між HTML-сторінками (Cross-Document View Transitions).

Підтримка (Same-Document): Chrome 111+, Safari 18+, Firefox 130+

// Same-Document: анімований перехід між станами
document.startViewTransition(() => {
    // Будь-яке DOM-оновлення тут
    mainContent.innerHTML = newPageHTML
})
/* За замовчуванням — cross-fade. Можна кастомізувати: */
::view-transition-old(root) {
    animation: slide-out 0.3s ease-in forwards;
}

::view-transition-new(root) {
    animation: slide-in 0.3s ease-out forwards;
}

@keyframes slide-out {
    to {
        transform: translateX(-100%);
    }
}

@keyframes slide-in {
    from {
        transform: translateX(100%);
    }
}

Для конкретних елементів — іменовані view-transition:

.product-card {
    /* Цей елемент матиме плавну морфінг-анімацію між сторінками */
    view-transition-name: product-hero;
    contain: layout;
}

При переході на сторінку деталей товару — картка "перетікає" в hero-зображення. Як у нативних iOS-застосунках, але засобами CSS та HTML.

Cross-Document View Transitions (MPA)

Підтримка: Chrome 126+

/* У CSS — вмикаємо для всіх переходів між сторінками сайту */
@view-transition {
    navigation: auto;
}

Один рядок CSS — і всі переходи між сторінками вашого сайту стають плавними cross-fade. Без жодного JavaScript, без SPA.


Проблема 8: Масонрі-Layout нарешті нативний

Masonry layout (мозаїчна сітка, як на Pinterest) — перпендикулярна сітка, де елементи різної висоти "заповнюють" простір. Роками це вимагало JavaScript-бібліотек.

CSS Grid Masonry

Підтримка: Firefox 126+ (через layout.css.grid-template-masonry-value.enabled), Safari Technology Preview, Chrome — в Origin Trial

.masonry-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    /* masonry — нове ключове слово для grid-template-rows */
    grid-template-rows: masonry;
    gap: 1rem;
}

Один рядок замість сотень рядків JavaScript. Елементи автоматично розміщуються у "колонки", заповнюючи вільний простір.


Проблема 9: Стилізація що завгодно

:nth-child() з фільтром за класом

Оновлений :nth-child() тепер приймає аргумент-фільтр:

/* Раніше: nth-child рахував всіх дітей, навіть з іншими класами */
.list > li:nth-child(2n) {
    /* рахує ВСІ li, включаючи .special */
}

/* Тепер: рахуємо тільки .post серед братів */
.list > li:nth-child(2n of .post) {
    background: #f0fdf4;
}

Це вирішує давню проблему: якщо між .post елементами є інші елементи — старий :nth-child рахував їх, що ламало логіку.

CSS text-wrap: balance та text-wrap: pretty

Кінець "сироти" на останньому рядку тексту:

Підтримка: Chrome 114+ (balance), Chrome 117+ (pretty)

/* Балансує розподіл тексту по рядках — заголовки */
h1,
h2,
h3 {
    text-wrap: balance; /* Max 4 рядки для продуктивності */
}

/* Запобігає одиночним словам наприкінці — для параграфів */
p {
    text-wrap: pretty;
}
Preview
×
🔒localhost:3000

field-sizing: content — поле що росте з текстом

textarea {
    field-sizing: content; /* Розміщується за вмістом */
    max-height: 300px; /* Але не більше */
    overflow-y: auto;
}

Підтримка: Chrome 123+. Раніше — тільки через JavaScript auto-resize textarea.


Проблема 10: Логічні властивості та Subgrid

CSS Logical Properties — інтернаціоналізація

Замість margin-left/margin-rightmargin-inline-start/margin-inline-end. Ці властивості адаптуються до напрямку тексту (LTR/RTL):

/* Замість: */
.element {
    margin-left: 1rem; /* ❌ Зламається в RTL (арабська, іврит) */
}

/* Використовуйте: */
.element {
    margin-inline-start: 1rem; /* ✅ LTR = зліва, RTL = справа */
}

/* Таблиця відповідностей: */
/* margin-top    → margin-block-start  */
/* margin-bottom → margin-block-end    */
/* margin-left   → margin-inline-start */
/* margin-right  → margin-inline-end   */
/* width         → inline-size         */
/* height        → block-size          */

CSS Subgrid — вирівнювання вкладених сіток

Subgrid дозволяє вкладеним елементам Grid успадковувати треки батьківської сітки — вирівнювати елементи у картках незалежно від їх вмісту:

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

.cards-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    grid-template-rows: auto;
}

.card {
    display: grid;
    /* Успадкувати рядки від батьківської сітки */
    grid-row: span 3;
    grid-template-rows: subgrid;
}

/* Тепер всі .card__header вирівняні між картками,
   навіть якщо заголовки різної довжини */
.card__header {
}
.card__body {
}
.card__footer {
}
Preview
×
🔒localhost:3000

Таблиця підтримки: що використовувати зараз

ФункціяChromeFirefoxSafariБезпечно в продакшні?
:has()105+121+15.4+✅ Так
color-mix()111+113+16.2+✅ Так
light-dark()123+120+17.5+✅ Так
@layer99+97+15.4+✅ Так
CSS Nesting112+117+17.2+✅ Так
text-wrap: balance114+121+17.4+✅ Так
Container Queries105+110+16+✅ Так
Subgrid117+71+16+✅ Так
@starting-style117+129+17.5+⚠️ Майже
popover API114+125+17+⚠️ Майже
Relative Color Syntax119+128+16.4+⚠️ Майже
Scroll-Driven Animations115+110+*18+⚠️ Перевіряйте
CSS Anchor Positioning125+⚠️ Тільки Chrome
View Transitions (SPA)111+130+18+⚠️ Майже
View Transitions (MPA)126+18+⚠️ Progressive
field-sizing: content123+⚠️ Тільки Chrome
Grid Masonry126+*Preview❌ Ще рано

* — за прапором або в Origin Trial


Progressive Enhancement: нові функції без ризику

Нові CSS-функції можна використовувати безпечно через @supports — старіші браузери просто ігнорують невідомі блоки:

/* Базовий стиль — для всіх */
.card {
    display: flex;
    flex-direction: column;
}

/* Покращення для браузерів з підтримкою :has() */
@supports selector(:has(img)) {
    .card:has(img) {
        flex-direction: row;
    }
}

/* Покращення для Scroll-Driven Animations */
@supports (animation-timeline: scroll()) {
    .reading-progress {
        animation: progress-fill linear;
        animation-timeline: scroll(root);
    }
}

/* Покращення для color-mix() */
@supports (color: color-mix(in srgb, red, blue)) {
    .btn:hover {
        background: color-mix(in srgb, var(--accent) 85%, black);
    }
}

Ключовий принцип: нові CSS-функції додають покращення, а не критичну функціональність. Базовий досвід має працювати скрізь.


Mindmap: нові можливості CSS 2023–2025

Loading diagram...
mindmap
    root((Сучасний CSS))
        Селектори
            :has — батьківський
            :is та :where
            :nth-child of .class
        Кольори
            color-mix
            light-dark
            Relative Color Syntax
            oklch color space
        Анімації
            Scroll-Driven Animations
            @starting-style
            allow-discrete
            View Transitions API
        Layout
            CSS Subgrid
            Grid Masonry
            CSS Anchor Positioning
        UI без JS
            popover API
            :popover-open
        Типографіка
            text-wrap balance
            text-wrap pretty
            field-sizing content
        Інтернаціоналізація
            Logical Properties
            inline-size block-size

Завдання для самоперевірки


Попередня стаття: CSS Custom Properties. Методології. Сучасний CSS

Copyright © 2026