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, реагуючи на стани дочірніх елементів:

🔒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 та базових стилів — вони не будуть конфліктувати зі стилями компонентів.

🔒localhost:3000

Практична робота: Інтерактивна картка з :has() та :where()

🎯 Очікуваний результат: Створення картки підписки на розсилку, яка динамічно підсвічує свої межі червоним при невалідному email та синім — при фокусуванні на полі, використовуючи :has(). Заголовки картки будуть оформлені через :where(), щоб дозволити легке перевизначення тем оформлення.

Крок 1: Створення структури HTML

Створіть файл index.html у вашому робочому каталозі та додайте наступну розмітку:

<!DOCTYPE html>
<html lang="uk">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Практична робота: :has() та :where()</title>
        <link rel="stylesheet" href="style.css" />
    </head>
    <body>
        <div class="card">
            <h3 class="card-title">Отримайте свіжі костилі</h3>
            <p class="card-desc">Лише корисні хаки та оновлення безпосередньо у вашу поштову скриньку.</p>
            <form class="card-form">
                <input type="email" placeholder="Введіть ваш email" required />
                <button type="submit">Підписатися</button>
            </form>
        </div>
    </body>
</html>

Крок 2: Додавання стилів CSS

Створіть у тій же папці файл style.css та додайте такі стилі:

/* Базові налаштування */
body {
    background-color: #0f172a;
    font-family: system-ui, sans-serif;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    margin: 0;
}

/* Використовуємо :where() для базової стилізації заголовка та тексту, 
   щоб їх можна було легко перевизначити пізніше за потреби */
:where(.card-title) {
    margin: 0 0 0.5rem;
    color: #f8fafc;
    font-size: 1.25rem;
}

:where(.card-desc) {
    margin: 0 0 1.5rem;
    color: #94a3b8;
    font-size: 0.875rem;
    line-height: 1.5;
}

/* Базова картка */
.card {
    background-color: #1e293b;
    border: 2px solid #334155;
    border-radius: 12px;
    padding: 1.5rem;
    width: 320px;
    transition:
        border-color 0.3s,
        box-shadow 0.3s;
}

/* Магія :has() №1: підсвічуємо картку, якщо інпут у фокусі */
.card:has(input:focus) {
    border-color: #3b82f6;
    box-shadow: 0 0 15px rgba(59, 130, 246, 0.2);
}

/* Магія :has() №2: підсвічуємо картку червоним, якщо email невалідний (коли почали вводити) */
.card:has(input:invalid:not(:placeholder-shown)) {
    border-color: #ef4444;
    box-shadow: 0 0 15px rgba(239, 68, 68, 0.2);
}

/* Стилізація форми та полів */
.card-form {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
}

input {
    background-color: #0f172a;
    border: 1px solid #334155;
    border-radius: 6px;
    color: white;
    padding: 0.6rem;
    outline: none;
    font-size: 0.875rem;
    transition: border-color 0.2s;
}

input:focus {
    border-color: #3b82f6;
}

button {
    background-color: #3b82f6;
    color: white;
    border: none;
    border-radius: 6px;
    padding: 0.6rem;
    font-weight: 600;
    cursor: pointer;
    transition: background-color 0.2s;
}

button:hover {
    background-color: #2563eb;
}

Крок 3: Перевірка та аналіз результату

  1. Відкрийте файл index.html у вашому веб-браузері.
  2. Натисніть на поле вводу пошти. Зверніть увагу, як рамка всієї картки плавно змінить колір на синій завдяки селектору :has(input:focus).
  3. Почніть вводити невалідну адресу (наприклад, просто символи abc без @). Рамка картки автоматично стане червоною (спрацював :has(input:invalid:not(:placeholder-shown))).

Проблема 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; /* Стартує з прозорого при появі */
    }
}
🔒localhost:3000

Практична робота: Плавне випливаюче діалогове вікно з @starting-style

🎯 Очікуваний результат: Побудова вишуканого діалогового вікна (Modal), яке при активації плавно з'являється (opacity від 0 до 1, transform від легкого зсуву вгору до оригінальної позиції), а при закритті — плавно розчиняється. Це реалізується за допомогою @starting-style та transition-behavior: allow-discrete без складних JS бібліотек анімації.

Крок 1: Створення структури HTML

Створіть файл modal.html у вашому робочому каталозі та додайте наступну розмітку:

<!DOCTYPE html>
<html lang="uk">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Практична робота: Анімація появи дискретних елементів</title>
        <link rel="stylesheet" href="modal.css" />
    </head>
    <body>
        <button class="open-btn" id="openModalBtn">Відкрити модальне вікно</button>

        <div class="modal-overlay" id="modalOverlay">
            <div class="modal-content">
                <h3>Вітаємо на борту! 🚀</h3>
                <p>
                    Ви успішно активували нативну анімацію дискретного стану `display: block` за допомогою сучасного
                    CSS.
                </p>
                <button class="close-btn" id="closeModalBtn">Зрозуміло</button>
            </div>
        </div>

        <script>
            const openBtn = document.getElementById('openModalBtn')
            const closeBtn = document.getElementById('closeModalBtn')
            const overlay = document.getElementById('modalOverlay')

            openBtn.addEventListener('click', () => {
                overlay.classList.add('active')
            })

            closeBtn.addEventListener('click', () => {
                overlay.classList.remove('active')
            })
        </script>
    </body>
</html>

Крок 2: Додавання стилів CSS

Створіть у тій же папці файл modal.css та додайте такі стилі:

/* Базовий темний фон */
body {
    background-color: #0f172a;
    font-family: system-ui, sans-serif;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    margin: 0;
}

/* Кнопка відкриття */
.open-btn {
    background: linear-gradient(135deg, #ec4899, #8b5cf6);
    color: white;
    border: none;
    border-radius: 8px;
    padding: 0.8rem 1.5rem;
    font-size: 1rem;
    font-weight: 600;
    cursor: pointer;
}

/* Оверлей (задній напівпрозорий фон) */
.modal-overlay {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(15, 23, 42, 0.7);
    backdrop-filter: blur(4px);
    display: none; /* За замовчуванням прихований */
    justify-content: center;
    align-items: center;
    z-index: 1000;

    /* Налаштовуємо перехід для дискретної властивості display та opacity */
    transition:
        display 0.4s allow-discrete,
        opacity 0.4s ease;
    opacity: 0;
}

/* Стан, коли модальне вікно активне */
.modal-overlay.active {
    display: flex;
    opacity: 1;
}

/* Вміст вікна */
.modal-content {
    background-color: #1e293b;
    border: 1px solid #334155;
    border-radius: 16px;
    padding: 2rem;
    width: 340px;
    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3);
    text-align: center;
    color: white;

    /* Одночасно анімуємо контент */
    transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
    transform: translateY(20px) scale(0.95);
}

.modal-overlay.active .modal-content {
    transform: translateY(0) scale(1);
}

/* Магія @starting-style: вказуємо початковий стан під час 
   переходу елемента від display: none до display: flex */
@starting-style {
    .modal-overlay.active {
        opacity: 0;
    }

    .modal-overlay.active .modal-content {
        transform: translateY(20px) scale(0.95);
    }
}

/* Елементи всередині вікна */
.modal-content h3 {
    margin: 0 0 0.75rem;
    font-size: 1.5rem;
}

.modal-content p {
    color: #94a3b8;
    font-size: 0.9rem;
    line-height: 1.5;
    margin: 0 0 1.5rem;
}

.close-btn {
    background-color: #ef4444;
    color: white;
    border: none;
    border-radius: 6px;
    padding: 0.6rem 1.2rem;
    font-weight: 600;
    cursor: pointer;
    transition: background-color 0.2s;
}

.close-btn:hover {
    background-color: #dc2626;
}

Крок 3: Перевірка та аналіз результату

  1. Відкрийте файл modal.html у вашому веб-браузері.
  2. Натисніть кнопку "Відкрити модальне вікно".
  3. Зауважте, що хоча ми перемикаємо оверлей з display: none на display: flex, його поява відбувається неймовірно плавно та з еластичним ефектом завдяки поєднанню @starting-style та allow-discrete.

Проблема 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%;
    }
}
🔒localhost:3000

Практична робота: Індикатор читання та картки, що проявляються при скролі

🎯 Очікуваний результат: Створення довгої веб-сторінки з індикатором прогресу читання (progressbar) зверху, який заповнюється по мірі прокрутки сторінки, та сітки карток, які плавно випливають і збільшуються в масштабі, коли з'являються в області видимості. Все це працює без жодного рядка JavaScript.

Крок 1: Створення структури HTML

Створіть файл scroll.html у вашому робочому каталозі та додайте наступну розмітку:

<!DOCTYPE html>
<html lang="uk">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Практична робота: Scroll-Driven Animations</title>
        <link rel="stylesheet" href="scroll.css" />
    </head>
    <body>
        <!-- Нативний індикатор скролу -->
        <div class="scroll-progress"></div>

        <header class="hero">
            <h1>Космічний Блог 🌌</h1>
            <p>Прокрутіть вниз, щоб побачити магію прокручуваних анімацій CSS</p>
        </header>

        <main class="content-grid">
            <div class="scroll-card">
                <div class="card-icon">🚀</div>
                <h3>Дослідження Марсу</h3>
                <p>
                    Марсохід Perseverance продовжує збирати зразки породи в кратері Єзеро для майбутнього повернення на
                    Землю.
                </p>
            </div>
            <div class="scroll-card">
                <div class="card-icon">🪐</div>
                <h3>Кільця Сатурну</h3>
                <p>
                    Нові дослідження підтверджують, що кільця Сатурна набагато молодші, ніж вважалося раніше — їм не
                    більше 400 млн років.
                </p>
            </div>
            <div class="scroll-card">
                <div class="card-icon">☄️</div>
                <h3>Міжзоряні комети</h3>
                <p>
                    Астрономи відстежують нові об'єкти поза нашою Сонячною системою, які несуть унікальні хімічні
                    підписи.
                </p>
            </div>
            <div class="scroll-card">
                <div class="card-icon">📡</div>
                <h3>Сигнали з глибин</h3>
                <p>
                    Нові радіотелескопи фіксують регулярні швидкі радіосплески з сусідньої галактики, що кидають виклик
                    теоріям.
                </p>
            </div>
        </main>
    </body>
</html>

Крок 2: Додавання стилів CSS

Створіть у тій же папці файл scroll.css та додайте такі стилі:

/* Базові налаштування */
body {
    background-color: #0b0f19;
    color: #f1f5f9;
    font-family: system-ui, sans-serif;
    margin: 0;
    padding-bottom: 50vh; /* Додатковий простір для скролу */
}

/* Індикатор прогресу скролу */
.scroll-progress {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 6px;
    background: linear-gradient(90deg, #3b82f6, #ec4899);
    transform-origin: 0 50%;
    z-index: 100;

    /* Прив'язуємо анімацію масштабування по осі X до скролу всієї сторінки */
    animation: grow-progress linear;
    animation-timeline: scroll();
}

@keyframes grow-progress {
    from {
        transform: scaleX(0);
    }
    to {
        transform: scaleX(1);
    }
}

/* Обкладинка блогу */
.hero {
    height: 80vh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    text-align: center;
    background: radial-gradient(circle at center, #1e1b4b 0%, #0b0f19 100%);
    padding: 1rem;
}

.hero h1 {
    font-size: 3rem;
    margin: 0 0 1rem;
}

.hero p {
    color: #94a3b8;
    font-size: 1.2rem;
}

/* Сітка карток */
.content-grid {
    max-width: 800px;
    margin: 0 auto;
    padding: 2rem 1rem;
    display: flex;
    flex-direction: column;
    gap: 3rem;
}

/* Картка з анімацією випливання */
.scroll-card {
    background: #111827;
    border: 1px solid #1f2937;
    border-radius: 16px;
    padding: 2rem;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);

    /* Реєструємо анімацію випливання елемента на екрані */
    animation: reveal-card linear both;
    animation-timeline: view();
    animation-range: entry 10% cover 45%;
}

.card-icon {
    font-size: 2.5rem;
    margin-bottom: 0.5rem;
}

.scroll-card h3 {
    margin: 0;
    font-size: 1.5rem;
    color: #3b82f6;
}

.scroll-card p {
    color: #94a3b8;
    line-height: 1.6;
    margin: 0;
}

/* Анімація появи: картка починає напівпрозорою та зміщеною вниз,
   а в процесі скролу вирівнюється */
@keyframes reveal-card {
    from {
        opacity: 0;
        transform: translateY(60px) scale(0.9);
    }
    to {
        opacity: 1;
        transform: translateY(0) scale(1);
    }
}

Крок 3: Перевірка та аналіз результату

  1. Відкрийте файл scroll.html у вашому веб-браузері.
  2. Почніть скролити вниз. Зверніть увагу на верхню межу вікна — градієнтна лінія буде заповнюватися синхронно з вашим рухом по сторінці (animation-timeline: scroll()).
  3. По мірі прокручування ви побачите, як кожна картка плавно піднімається, збільшується до оригінального масштабу та розкривається (animation-timeline: view()).

Проблема 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.

🔒localhost:3000

Практична робота: Нативний тултіп з автовирівнюванням через Anchor Positioning

🎯 Очікуваний результат: Створення кнопки та випливаючої підказки (tooltip), яка нативно прикріплюється до нижнього центру кнопки за допомогою CSS Anchor Positioning, та автоматично перестрибує вгору (@position-try), якщо при прокручуванні вікна знизу не залишається вільного місця.

Крок 1: Створення структури HTML

Створіть файл anchor.html у вашому робочому каталозі та додайте наступну розмітку:

<!DOCTYPE html>
<html lang="uk">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Практична робота: CSS Anchor Positioning</title>
        <link rel="stylesheet" href="anchor.css" />
    </head>
    <body>
        <div class="scroll-container">
            <p class="scroll-instruction">
                Прокрутіть це вікно вниз або вгору, щоб побачити автопереворот тултіпа при виході за межі екрану 👇
            </p>

            <div class="anchor-area">
                <!-- Елемент-якір -->
                <button class="anchor-button" id="myAnchorBtn">Наведіть на мене</button>

                <!-- Позиціонований тултіп -->
                <div class="anchor-tooltip" id="myTooltip">💡 Я прив'язаний до кнопки через CSS!</div>
            </div>
        </div>
    </body>
</html>

Крок 2: Додавання стилів CSS

Створіть у тій же папці файл anchor.css та додайте такі стилі:

/* Базові налаштування */
body {
    background-color: #0f172a;
    color: white;
    font-family: system-ui, sans-serif;
    margin: 0;
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
}

/* Контейнер зі скролом для тестування */
.scroll-container {
    height: 150vh;
    width: 100%;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 20rem;
}

.scroll-instruction {
    color: #94a3b8;
    font-size: 0.9rem;
    max-width: 300px;
    text-align: center;
}

.anchor-area {
    position: relative;
}

/* Крок 1: Реєструємо унікальне ім'я якоря для кнопки */
.anchor-button {
    anchor-name: --action-button;
    background-color: #4f46e5;
    color: white;
    border: none;
    border-radius: 8px;
    padding: 0.75rem 1.5rem;
    font-size: 1rem;
    font-weight: 600;
    cursor: pointer;
    transition: background-color 0.2s;
}

.anchor-button:hover {
    background-color: #4338ca;
}

/* Крок 2: Описуємо правило автоматичного перенесення */
@position-try --flip-up {
    top: auto;
    bottom: anchor(top);
    margin-bottom: 8px;
}

/* Крок 3: Позиціонуємо підказку відносно якоря */
.anchor-tooltip {
    background-color: #1e293b;
    border: 1px solid #334155;
    color: #f1f5f9;
    padding: 0.5rem 0.75rem;
    border-radius: 6px;
    font-size: 0.8rem;
    white-space: nowrap;
    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3);

    /* Робимо тултіп видимим при наведенні на кнопку */
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.2s ease;
}

/* Відображаємо при наведенні */
.anchor-button:hover + .anchor-tooltip {
    opacity: 1;
}

/* Застосовуємо якірне позиціонування тільки у разі підтримки браузером */
@supports (position-anchor: --action-button) {
    .anchor-tooltip {
        position: absolute;

        /* Прив'язуємось до зареєстрованого імені якоря */
        position-anchor: --action-button;

        /* Стандартне розміщення знизу по центру кнопки */
        top: anchor(bottom);
        left: anchor(center);
        transform: translateX(-50%);
        margin-top: 8px;

        /* Вказуємо браузеру перевертати вгору, якщо знизу немає місця */
        position-try-fallbacks: --flip-up;
    }
}

/* Простий fallback для браузерів без підтримки */
@supports not (position-anchor: --action-button) {
    .anchor-tooltip {
        opacity: 1;
        position: relative;
        margin-top: 10px;
        text-align: center;
        background-color: #312e81;
        border-color: #4338ca;
    }
}

Крок 3: Перевірка та аналіз результату

  1. Відкрийте файл anchor.html в браузері (рекомендується остання версія Chrome або Edge).
  2. Наведіть курсор на кнопку — тултіп з'явиться знизу.
  3. Прокрутіть сторінку так, щоб кнопка опинилася біля самого низу екрана, та наведіть на неї знову. Браузер нативно перемістить підказку вгору над кнопкою (спрацювало правило --flip-up).

Проблема 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.

🔒localhost:3000

Практична робота: Динамічна колірна палітра з color-mix()

🎯 Очікуваний результат: Створення кнопок та інформаційних карток (alerts), кольорові стани яких (hover, active, напівпрозорі фони) розраховуються браузером динамічно на основі однієї CSS-змінної брендового кольору за допомогою функції color-mix().

Крок 1: Створення структури HTML

Створіть файл colors.html у вашому робочому каталозі та додайте наступну розмітку:

<!DOCTYPE html>
<html lang="uk">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Практична робота: CSS color-mix()</title>
        <link rel="stylesheet" href="colors.css" />
    </head>
    <body>
        <div class="palette-card">
            <h3>Генератор UI-компонентів 🎨</h3>
            <p>
                Змініть лише одну змінну <code>--brand-color</code> у CSS, і вся тема з кнопками та повідомленнями
                адаптується автоматично!
            </p>

            <!-- Кнопки із динамічними станами ховеру -->
            <div class="button-group">
                <button class="btn btn-primary">Основна дія</button>
                <button class="btn btn-secondary">Другорядна дія</button>
            </div>

            <!-- Повідомлення із динамічним пастельним фоном та яскравою рамкою -->
            <div class="alert alert-info">
                <strong>Інформація:</strong> Стан повідомлення згенеровано за допомогою змішування кольору бренду з
                білим кольором у пропорції 90% до 10%.
            </div>
        </div>
    </body>
</html>

Крок 2: Додавання стилів CSS

Створіть у тій же папці файл colors.css та додайте такі стилі:

/* Базові налаштування теми */
:root {
    /* Наш єдиний базовий колір бренду.
       Спробуйте замінити його на #10b981 (зелений) або #f59e0b (помаранчевий)! */
    --brand-color: #3b82f6;

    background-color: #0f172a;
    color: white;
    font-family: system-ui, sans-serif;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    margin: 0;
}

.palette-card {
    background-color: #1e293b;
    border: 1px solid #334155;
    border-radius: 12px;
    padding: 2rem;
    width: 380px;
    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3);
}

.palette-card h3 {
    margin: 0 0 0.5rem;
}

.palette-card p {
    color: #94a3b8;
    font-size: 0.85rem;
    line-height: 1.5;
    margin: 0 0 1.5rem;
}

.palette-card p code {
    background: #0f172a;
    padding: 0.1rem 0.3rem;
    border-radius: 4px;
    color: #e2e8f0;
}

/* Група кнопок */
.button-group {
    display: flex;
    gap: 0.75rem;
    margin-bottom: 1.5rem;
}

.btn {
    padding: 0.6rem 1.2rem;
    border: none;
    border-radius: 6px;
    font-weight: 600;
    cursor: pointer;
    font-size: 0.875rem;
    transition:
        background-color 0.2s,
        border-color 0.2s;
}

/* Крок 1: Розраховуємо колір при наведенні (затемнення на 15%)
   та колір при кліку (затемнення на 30%) за допомогою color-mix() */
.btn-primary {
    background-color: var(--brand-color);
    color: white;
}

.btn-primary:hover {
    background-color: color-mix(in srgb, var(--brand-color) 85%, black);
}

.btn-primary:active {
    background-color: color-mix(in srgb, var(--brand-color) 70%, black);
}

/* Крок 2: Другорядна кнопка з напівпрозорими тонами бренду */
.btn-secondary {
    background-color: color-mix(in srgb, var(--brand-color) 15%, transparent);
    color: color-mix(in srgb, var(--brand-color) 80%, white);
    border: 1px solid color-mix(in srgb, var(--brand-color) 30%, transparent);
}

.btn-secondary:hover {
    background-color: color-mix(in srgb, var(--brand-color) 25%, transparent);
}

/* Крок 3: Створюємо вишукане пастельне попередження */
.alert {
    padding: 1rem;
    border-radius: 8px;
    font-size: 0.8rem;
    line-height: 1.4;

    /* Фон — бренд змішаний з великою кількістю білого */
    background-color: color-mix(in srgb, var(--brand-color) 10%, #ffffff);
    /* Текст — бренд змішаний із чорним для кращої читабельності */
    color: color-mix(in srgb, var(--brand-color) 80%, #000000);
    /* Рамка — яскравий брендовий відтінок */
    border: 1px solid color-mix(in srgb, var(--brand-color) 40%, #ffffff);
}

Крок 3: Перевірка та аналіз результату

  1. Відкрийте файл colors.html у вашому веб-браузері.
  2. Наведіть курсор на кнопки та натисніть на них. Зверніть увагу, як плавно та природно темнішає кнопка (працює змішування з чорним кольором black).
  3. Спробуйте змінити значення --brand-color в CSS на зелений #10b981 і зберегти файл. Всі другорядні кнопки та рамки з фоном повідомлень оновляться самостійно, зберігаючи чудовий контраст!

Проблема 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').

🔒localhost:3000

Практична робота: Нативний Dropdown-меню профілю через Popover API

🎯 Очікуваний результат: Створення меню профілю користувача, яке відкривається при натисканні на аватар, та автоматично закривається при натисканні поза його межами (light dismiss) або клавіші Escape, без використання JavaScript.

Крок 1: Створення структури HTML

Створіть файл popover.html у вашому робочому каталозі та додайте наступну розмітку:

<!DOCTYPE html>
<html lang="uk">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Практична робота: Popover API</title>
        <link rel="stylesheet" href="popover.css" />
    </head>
    <body>
        <div class="navbar">
            <div class="logo">MyDashboard 📊</div>

            <div class="profile-container">
                <!-- Крок 1: Вказуємо popovertarget з id нашого меню -->
                <button class="avatar-btn" popovertarget="profileMenu">
                    <span class="avatar-text">JD</span>
                </button>

                <!-- Крок 2: Додаємо атрибут popover="auto" -->
                <div id="profileMenu" popover="auto" class="dropdown-menu">
                    <div class="menu-header">
                        <h4>John Doe</h4>
                        <p>admin@example.com</p>
                    </div>
                    <hr />
                    <ul class="menu-list">
                        <li><a href="#profile">Мій профіль</a></li>
                        <li><a href="#settings">Налаштування</a></li>
                        <li><a href="#logout" class="logout-link">Вийти</a></li>
                    </ul>
                </div>
            </div>
        </div>
    </body>
</html>

Крок 2: Додавання стилів CSS

Створіть у тій же папці файл popover.css та додайте такі стилі:

/* Базові стилі */
body {
    background-color: #0f172a;
    color: white;
    font-family: system-ui, sans-serif;
    margin: 0;
    min-height: 100vh;
}

/* Навігаційна панель */
.navbar {
    background-color: #1e293b;
    border-bottom: 1px solid #334155;
    padding: 0.75rem 1.5rem;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.logo {
    font-weight: 700;
    font-size: 1.1rem;
}

.profile-container {
    position: relative;
}

/* Кнопка аватару */
.avatar-btn {
    background-color: #4f46e5;
    color: white;
    border: none;
    width: 40px;
    height: 40px;
    border-radius: 50%;
    cursor: pointer;
    display: flex;
    justify-content: center;
    align-items: center;
    font-weight: 700;
    font-size: 0.85rem;
    transition: transform 0.2s;
}

.avatar-btn:hover {
    transform: scale(1.05);
}

/* Крок 3: Стилізація нативного поповера */
.dropdown-menu {
    /* Нативний поповер має стандартні стилі браузера (рамка, фони, тіні), 
       які ми повністю перевизначаємо: */
    border: 1px solid #334155;
    border-radius: 12px;
    padding: 1rem;
    background-color: #1e293b;
    color: white;
    width: 200px;
    box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.4);

    /* Зміщуємо меню під аватар на сторінці */
    position: absolute;
    top: 55px;
    right: 15px;
    margin: 0;
}

/* Запобігаємо автоматичним відступам */
.dropdown-menu::backdrop {
    /* backdrop="auto" має нативний прозорий оверлей, 
       ми можемо його стилізувати або вимкнути */
    background: transparent;
}

/* Контент меню */
.menu-header h4 {
    margin: 0;
    font-size: 0.95rem;
}

.menu-header p {
    margin: 0.2rem 0 0;
    font-size: 0.75rem;
    color: #94a3b8;
}

hr {
    border: 0;
    border-top: 1px solid #334155;
    margin: 0.75rem 0;
}

.menu-list {
    list-style: none;
    padding: 0;
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.menu-list a {
    color: #e2e8f0;
    text-decoration: none;
    font-size: 0.85rem;
    display: block;
    padding: 0.4rem;
    border-radius: 6px;
    transition: background-color 0.15s;
}

.menu-list a:hover {
    background-color: #334155;
}

.menu-list a.logout-link {
    color: #f87171;
}

.menu-list a.logout-link:hover {
    background-color: rgba(248, 113, 113, 0.15);
}

Крок 3: Перевірка та аналіз результату

  1. Відкрийте файл popover.html у вашому веб-браузері.
  2. Натисніть на круглий синій аватар. Випадаюче меню відкриється нативно без жодної помилки JavaScript.
  3. Клацніть у будь-якому порожньому місці сторінки або натисніть клавішу Escape. Меню автоматично закриється (спрацювала вбудована функція light dismiss).

Проблема 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.

🔒localhost:3000

Cross-Document View Transitions (MPA)

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

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

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

Практична робота: Плавне перемикання станів сторінки з View Transitions API

🎯 Очікуваний результат: Створення інтерактивної галереї, де при перемиканні фотографій поточне зображення плавно «перетікає» на місце нового, масштабуючись та переміщуючись, без складних розрахунків координат у JS. Браузер сам розрахує траєкторію анімації завдяки властивості view-transition-name.

Крок 1: Створення структури HTML

Створіть файл transitions.html у вашому робочому каталозі та додайте наступну розмітку:

<!DOCTYPE html>
<html lang="uk">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Практична робота: View Transitions API</title>
        <link rel="stylesheet" href="transitions.css" />
    </head>
    <body>
        <div class="gallery-container">
            <h2>Галерея космічних знімків 🔭</h2>

            <!-- Центральне велике фото -->
            <div class="main-photo-wrapper">
                <img
                    id="mainImage"
                    class="main-photo"
                    src="https://images.unsplash.com/photo-1506318137071-a8e063b4bec0?w=600&auto=format&fit=crop"
                    alt="Космос"
                />
            </div>

            <!-- Перемикачі під великим фото -->
            <div class="thumbnails">
                <button
                    class="thumb-btn active"
                    data-src="https://images.unsplash.com/photo-1506318137071-a8e063b4bec0?w=600&auto=format&fit=crop"
                >
                    Знімок 1
                </button>
                <button
                    class="thumb-btn"
                    data-src="https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=600&auto=format&fit=crop"
                >
                    Знімок 2
                </button>
                <button
                    class="thumb-btn"
                    data-src="https://images.unsplash.com/photo-1462331940025-496dfbfc7564?w=600&auto=format&fit=crop"
                >
                    Знімок 3
                </button>
            </div>
        </div>

        <script>
            const mainImg = document.getElementById('mainImage')
            const buttons = document.querySelectorAll('.thumb-btn')

            buttons.forEach((btn) => {
                btn.addEventListener('click', (e) => {
                    const newSrc = btn.getAttribute('data-src')

                    // Перевіряємо, чи підтримує поточний браузер View Transitions API
                    if (!document.startViewTransition) {
                        // Звичайне миттєве оновлення, якщо не підтримується
                        mainImg.src = newSrc
                        updateActiveBtn(btn)
                        return
                    }

                    // Запускаємо нативний перехід
                    document.startViewTransition(() => {
                        mainImg.src = newSrc
                        updateActiveBtn(btn)
                    })
                })
            })

            function updateActiveBtn(activeBtn) {
                buttons.forEach((b) => b.classList.remove('active'))
                activeBtn.classList.add('active')
            }
        </script>
    </body>
</html>

Крок 2: Додавання стилів CSS

Створіть у тій же папці файл transitions.css та додайте такі стилі:

/* Базові налаштування */
body {
    background-color: #0b0f19;
    color: white;
    font-family: system-ui, sans-serif;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    margin: 0;
}

.gallery-container {
    text-align: center;
    width: 100%;
    max-width: 500px;
    padding: 1.5rem;
}

h2 {
    margin: 0 0 1.5rem;
    font-weight: 500;
}

/* Обертка для зображення */
.main-photo-wrapper {
    background-color: #111827;
    border: 2px solid #1f2937;
    border-radius: 16px;
    overflow: hidden;
    height: 300px;
    margin-bottom: 1.5rem;
}

.main-photo {
    width: 100%;
    height: 100%;
    object-fit: cover;

    /* Крок 1: Надаємо зображенню унікальне ім'я переходу.
       Браузер стежитиме за станом елемента з цим ім'ям */
    view-transition-name: active-space-photo;
}

/* Кнопки перемикання */
.thumbnails {
    display: flex;
    justify-content: center;
    gap: 0.75rem;
}

.thumb-btn {
    background-color: #1e293b;
    border: 1px solid #334155;
    color: #94a3b8;
    padding: 0.5rem 1rem;
    border-radius: 8px;
    cursor: pointer;
    font-weight: 600;
    font-size: 0.85rem;
    transition: all 0.2s ease;
}

.thumb-btn:hover {
    color: white;
    border-color: #3b82f6;
}

.thumb-btn.active {
    background-color: #3b82f6;
    color: white;
    border-color: #3b82f6;
}

/* Крок 2: Налаштовуємо кастомну анімацію для переходу (за бажанням) */
::view-transition-old(active-space-photo),
::view-transition-new(active-space-photo) {
    /* Робимо анімацію тривалістю в 0.5 секунд із плавною кривою */
    animation-duration: 0.5s;
    animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}

Крок 3: Перевірка та аналіз результату

  1. Відкрийте файл transitions.html у вашому веб-браузері.
  2. Спробуйте по черзі натискати кнопки під великим фото.
  3. Зверніть увагу, що коли спрацьовує document.startViewTransition(), нове зображення не просто з'являється поверх старого, а відбувається безшовне перетікання (старе фото плавно зникає, а нове випливає) за рахунок нативного крос-фейду.

Проблема 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. Елементи автоматично розміщуються у "колонки", заповнюючи вільний простір.

Практична робота: Побудва Pinterest-сітки через grid-template-rows: masonry

🎯 Очікуваний результат: Створення стильної мозаїчної фотогалереї (Masonry Layout), де блоки з різною висотою контенту автоматично «прилипають» один до одного знизу, усуваючи непривабливі великі порожнечі, без підключення важких JS бібліотек на кшталт Masonry.js.

Крок 1: Створення структури HTML

Створіть файл masonry.html у вашому робочому каталозі та додайте наступну розмітку:

<!DOCTYPE html>
<html lang="uk">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Практична робота: CSS Grid Masonry</title>
        <link rel="stylesheet" href="masonry.css" />
    </head>
    <body>
        <div class="gallery-wrapper">
            <h2>Мозаїчна галерея Pinterest (Masonry) 🖼️</h2>
            <p class="warning-banner">
                ⚠️ Нативна підтримка Masonry є експериментальною. Найкраще переглядати в Firefox зі встановленим
                прапорцем <code>layout.css.grid-template-masonry-value.enabled</code> в <code>about:config</code>.
            </p>

            <div class="masonry-container">
                <div class="masonry-item item-short">
                    <span class="badge">Урбаністика</span>
                    <p>Короткий опис міської архітектури майбутнього.</p>
                </div>
                <div class="masonry-item item-tall">
                    <span class="badge">Природа</span>
                    <p>
                        Тут розміщено набагато більше тексту для тестування того, як високий блок заповнює простір у
                        першій колонці. Високі блоки зазвичай ламають класичний Grid.
                    </p>
                </div>
                <div class="masonry-item item-medium">
                    <span class="badge">Космос</span>
                    <p>Неймовірні подорожі до далеких туманностей та чорних дір.</p>
                </div>
                <div class="masonry-item item-tall">
                    <span class="badge">Технології</span>
                    <p>
                        Штучний інтелект продовжує завойовувати серця веброзробників по всьому світу завдяки швидким та
                        нативним рішенням у браузерах.
                    </p>
                </div>
                <div class="masonry-item item-short">
                    <span class="badge">Дизайн</span>
                    <p>Естетичний мінімалізм у кожній деталі.</p>
                </div>
                <div class="masonry-item item-medium">
                    <span class="badge">Подорожі</span>
                    <p>Гірські пейзажі та дикі стежки далеких та затишних Карпат.</p>
                </div>
            </div>
        </div>
    </body>
</html>

Крок 2: Додавання стилів CSS

Створіть у тій же папці файл masonry.css та додайте такі стилі:

/* Базові налаштування */
body {
    background-color: #0f172a;
    color: white;
    font-family: system-ui, sans-serif;
    margin: 0;
    padding: 2rem 1rem;
}

.gallery-wrapper {
    max-width: 900px;
    margin: 0 auto;
}

h2 {
    margin: 0 0 0.5rem;
}

.warning-banner {
    background-color: rgba(245, 158, 11, 0.15);
    border: 1px solid #f59e0b;
    color: #f59e0b;
    padding: 0.75rem 1rem;
    border-radius: 8px;
    font-size: 0.85rem;
    margin: 0 0 2rem;
    line-height: 1.5;
}

.warning-banner code {
    background-color: rgba(245, 158, 11, 0.25);
    padding: 0.1rem 0.3rem;
    border-radius: 4px;
}

/* Крок 1: Оголошуємо масонрі-сітку */
.masonry-container {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));

    /* Магічне нативне ключове слово */
    grid-template-rows: masonry;
    gap: 1.25rem;
}

/* Крок 2: Стилізуємо картки */
.masonry-item {
    background-color: #1e293b;
    border: 1px solid #334155;
    border-radius: 12px;
    padding: 1.5rem;
    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
}

/* Різна висота контенту для демонстрації */
.item-short {
    min-height: 120px;
}
.item-medium {
    min-height: 180px;
}
.item-tall {
    min-height: 250px;
}

.badge {
    align-self: flex-start;
    background-color: #4f46e5;
    font-size: 0.7rem;
    font-weight: 700;
    padding: 0.25rem 0.6rem;
    border-radius: 20px;
    text-transform: uppercase;
    margin-bottom: 1rem;
}

.masonry-item p {
    color: #94a3b8;
    font-size: 0.9rem;
    line-height: 1.5;
    margin: 0;
}

Крок 3: Перевірка та аналіз результату

  1. Запустіть останню версію Firefox, відкрийте вкладку about:config та перемкніть параметр layout.css.grid-template-masonry-value.enabled на true.
  2. Відкрийте файл masonry.html.
  3. Зверніть увагу, що картки з різною висотою ідеально підтягнулися одна під одну. Між ними немає горизонтальних розривів або порожніх рядків, які зазвичай утворюються у звичайному display: grid.

Проблема 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;
}
🔒localhost:3000

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

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

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

🔒localhost:3000

Практична робота: Стилізація кнопкових селекторів файлів та заднього фону модальних вікон

🎯 Очікуваний результат: Створення стильної форми завантаження резюме користувача, яка містить завантажувач файлів із повністю кастомізованою кнопкою (::file-selector-button), та діалогове вікно подяки із розмитим напівпрозорим заднім фоном (::backdrop).

Крок 1: Створення структури HTML

Створіть файл inputs.html у вашому робочому каталозі та додайте наступну розмітку:

<!DOCTYPE html>
<html lang="uk">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Практична робота: Стилізація ::backdrop та ::file-selector-button</title>
        <link rel="stylesheet" href="inputs.css" />
    </head>
    <body>
        <div class="form-container">
            <h3>Подача заявки на вакансію 💼</h3>

            <form
                class="apply-form"
                onsubmit="event.preventDefault(); document.getElementById('thankYouDialog').showModal();"
            >
                <label class="file-label">Завантажте ваше резюме (PDF, DOCX):</label>
                <!-- Нативний інпут вибору файлу -->
                <input type="file" class="file-input" required />

                <button type="submit" class="submit-btn">Надіслати відгук</button>
            </form>
        </div>

        <!-- Нативний діалог подяки -->
        <dialog id="thankYouDialog" class="success-dialog">
            <h3>Дякуємо! 🎉</h3>
            <p>Вашу заявку та резюме успішно надіслано HR-відділу. Ми зв'яжемося з вами найближчим часом.</p>
            <button onclick="document.getElementById('thankYouDialog').close();" class="close-btn">Закрити</button>
        </dialog>
    </body>
</html>

Крок 2: Додавання стилів CSS

Створіть у тій же папці файл inputs.css та додайте такі стилі:

/* Базові налаштування */
body {
    background-color: #0f172a;
    color: white;
    font-family: system-ui, sans-serif;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    margin: 0;
}

.form-container {
    background-color: #1e293b;
    border: 1px solid #334155;
    border-radius: 12px;
    padding: 2rem;
    width: 350px;
    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3);
}

.form-container h3 {
    margin: 0 0 1.5rem;
}

.apply-form {
    display: flex;
    flex-direction: column;
    gap: 1.25rem;
}

.file-label {
    font-size: 0.85rem;
    color: #94a3b8;
}

/* Крок 1: Стилізація кнопки вибору файлу за допомогою ::file-selector-button */
.file-input::file-selector-button {
    background-color: #3b82f6;
    color: white;
    border: none;
    border-radius: 6px;
    padding: 0.5rem 1rem;
    font-weight: 600;
    font-size: 0.8rem;
    cursor: pointer;
    margin-right: 1rem;
    transition: background-color 0.2s;
}

.file-input::file-selector-button:hover {
    background-color: #2563eb;
}

.file-input {
    color: #94a3b8;
    font-size: 0.85rem;
}

.submit-btn {
    background-color: #10b981;
    color: white;
    border: none;
    border-radius: 6px;
    padding: 0.65rem;
    font-weight: 600;
    cursor: pointer;
    font-size: 0.9rem;
    transition: background-color 0.2s;
}

.submit-btn:hover {
    background-color: #059669;
}

/* Крок 2: Стилізація діалогового вікна */
.success-dialog {
    background-color: #1e293b;
    border: 1px solid #334155;
    border-radius: 16px;
    padding: 2rem;
    color: white;
    width: 320px;
    text-align: center;
    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
}

.success-dialog h3 {
    margin: 0 0 0.5rem;
    color: #10b981;
}

.success-dialog p {
    color: #94a3b8;
    font-size: 0.9rem;
    line-height: 1.5;
    margin: 0 0 1.5rem;
}

.close-btn {
    background-color: #334155;
    color: white;
    border: none;
    border-radius: 6px;
    padding: 0.5rem 1rem;
    cursor: pointer;
    transition: background-color 0.2s;
}

.close-btn:hover {
    background-color: #475569;
}

/* Крок 3: Магія ::backdrop для красивого розмиття заднього фону */
.success-dialog::backdrop {
    background-color: rgba(15, 23, 42, 0.6);
    backdrop-filter: blur(8px); /* Розмиває весь вміст під модальним вікном */

    /* Додатковий ефект плавного розкриття */
    animation: fade-backdrop 0.3s ease-out;
}

@keyframes fade-backdrop {
    from {
        opacity: 0;
    }
    to {
        opacity: 1;
    }
}

Крок 3: Перевірка та аналіз результату

  1. Відкрийте файл inputs.html у вашому веб-браузері.
  2. Зверніть увагу на кнопку вибору файлу. Завдяки ::file-selector-button вона виглядає сучасно та гармонує із темним дизайном форми, усуваючи стандартні сірі кнопки браузерів.
  3. Оберіть файл, натисніть кнопку "Надіслати відгук" та проаналізуйте фон. Поза межами вікна подяки весь інтерфейс приємно затемнився та розмився (спрацював фільтр blur(8px) у ::backdrop).

Проблема 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 {
}
🔒localhost:3000

Практична робота: Компонент карток відгуків із налаштуванням сітки через Subgrid та RTL-підтримкою

🎯 Очікуваний результат: Створення горизонтальної сітки з відгуками користувачів, в яких аватар, заголовок, текст відгуку та нижня кнопка ідеально вирівняні по вертикалі за допомогою grid-template-rows: subgrid. Також ми адаптуємо картки до RTL (напрямок тексту справа наліво), використовуючи виключно логічні властивості.

Крок 1: Створення структури HTML

Створіть файл subgrid.html у вашому робочому каталозі та додайте наступну розмітку:

<!DOCTYPE html>
<html lang="uk">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Практична робота: CSS Subgrid та Logical Properties</title>
    <link rel="stylesheet" href="subgrid.css">
</head>
<body>
    <!-- Додайте атрибут dir="rtl" до body нижче, щоб протестувати логічні властивості! -->
    <div class="feedback-layout">
        <h2>Відгуки клієнтів 💬</h2>
        
        <div class="feedback-grid">
            <!-- Картка 1 -->
            <div class="feedback-card">
                <div class="card-header">
                    <div class="avatar">👨‍💻</div>
                    <h4>Олександр К.</h4>
                </div>
                <div class="card-body">
                    <p>Subgrid повністю вирішив проблему з вирівнюванням кнопок. Тепер верстка виглядає неймовірно професійно!</p>
                </div>
                <div class="card-footer">
                    <button class="more-btn">Переглянути</button>
                </div>
            </div>

            <!-- Картка 2 (з великим текстом) -->
            <div class="feedback-card">
                <div class="card-header">
                    <div class="avatar">👩‍💼</div>
                    <h4>Ірина М.</h4>
                </div>
                <div class="card-body">
                    <p>Чудовий досвід. Завдяки логічним властивостям ми адаптували наш інтерфейс під арабську мову (RTL) за лічені хвилини без дублювання стилів. Рекомендую всім розробникам!</p>
                </div>
                <div class="card-footer">
                    <button class="more-btn">Переглянути</button>
                </div>
            </div>

            <!-- Картка 3 -->
            <div class="feedback-card">
                <div class="card-header">
                    <div class="avatar">🕵️‍♂️</div>
                    <h4>Максим Д.</h4>
                </div>
                <div class="card-body">
                    <p>Просто та зручно. Надійне рішення.</p>
                </div>
                <div class="card-footer">
                    <button class="more-btn">Переглянути</button>
                </div>
            </div>
        </div>
    </div>
</body>
</html>

Крок 2: Додавання стилів CSS

Створіть у тій же папці файл subgrid.css та додайте такі стилі:

/* Базові налаштування */
body {
    background-color: #0f172a;
    color: white;
    font-family: system-ui, sans-serif;
    margin: 0;
    padding: 2rem 1rem;
}

.feedback-layout {
    max-width: 1000px;
    margin: 0 auto;
}

/* Крок 1: Батьківська сітка для карток */
.feedback-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    grid-template-rows: auto;
    gap: 1.5rem;
}

/* Крок 2: Картка, що використовує субгрідинг */
.feedback-card {
    background-color: #1e293b;
    border: 1px solid #334155;
    border-radius: 12px;
    padding: 1.5rem;
    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
    
    /* Оголошуємо субгрідинг на 3 рядки */
    display: grid;
    grid-row: span 3;
    grid-template-rows: subgrid;
    gap: 1rem;
}

/* Крок 3: Використовуємо логічні властивості для RTL адаптивності */
.card-header {
    display: flex;
    align-items: center;
    border-bottom: 1px solid #334155;
    padding-block-end: 0.75rem; /* Замість padding-bottom */
}

.avatar {
    font-size: 1.5rem;
    /* Замість margin-right використовуємо margin-inline-end, 
       щоб відступ автоматично перестрибнув ліворуч у RTL режимі */
    margin-inline-end: 0.75rem;
}

.card-header h4 {
    margin: 0;
    font-size: 1rem;
}

.card-body p {
    color: #94a3b8;
    font-size: 0.9rem;
    line-height: 1.5;
    margin: 0;
}

.card-footer {
    display: flex;
    justify-content: flex-end;
}

.more-btn {
    background-color: #4f46e5;
    color: white;
    border: none;
    border-radius: 6px;
    padding: 0.5rem 1rem;
    font-size: 0.8rem;
    font-weight: 600;
    cursor: pointer;
    transition: background-color 0.2s;
}

.more-btn:hover {
    background-color: #4338ca;
}

Крок 3: Перевірка та аналіз результату

  1. Відкрийте файл subgrid.html у вашому веб-браузері.
  2. Зверніть увагу, як плавно та ідеально кнопки «Переглянути» вирівняні по горизонталі між усіма трьома картками, незважаючи на кардинально різну кількість тексту у середній картці (спрацював subgrid).
  3. Додайте атрибут dir="rtl" до тегу <html> або <body> у файлі subgrid.html. Збережіть та оновіть сторінку. Текст, аватари та кнопки автоматично віддзеркаляться, а відступи залишаться абсолютно правильними завдяки margin-inline-end та padding-block-end.

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

Функція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Наступна стаття: Що таке Tailwind CSS і навіщо він потрібен

Copyright © 2026