HTML & CSS

CSS для форм та інтерактивних станів

Повне керівництво зі стилізації HTML-форм: псевдокласи :focus-visible, :user-valid, :has(), accent-color, кастомні checkbox/radio/select, floating label патерн, валідація без JavaScript та нативний CSS-only дизайн форм.

CSS для форм та інтерактивних станів

Форми — найстресовіша частина верстки

«Зробіть форму гарною» — це речення, яке розробники та дизайнери трактують діаметрально протилежно. Дизайнер бачить чисті, мінімалістичні поля з плавними переходами. Розробник знає правду: браузери мають глибоко вкорінені стилі форм, які активно опираються CSS. Checkbox на macOS виглядає інакше, ніж на Windows. <select> майже неможливо стилізувати. <input type="range"> — окремий кошмар.

До 2021 року єдиним шляхом була повна заміна нативних елементів на div-обгортки з JavaScript. Тепер CSS надає справжні нативні інструменти: accent-color, appearance, field-sizing, псевдокласи :user-valid/:user-invalid, потужний :has() для реактивних форм.

Розберемо все по порядку — від базових станів до просунутих патернів.

Loading diagram...
graph TD
    A["HTML Form Element"] --> B["Базові стани<br/>:focus :hover :disabled"]
    A --> C["Валідаційні стани<br/>:valid :invalid :user-valid"]
    A --> D["Стани вмісту<br/>:checked :placeholder-shown<br/>:read-only :required"]
    A --> E[":has() — реакція батька<br/>на стан дочірнього"]
    B --> F["Кастомний дизайн<br/>appearance: none<br/>accent-color"]
    C --> G["CSS-only валідація<br/>без JavaScript"]
    style A fill:#6366f1,color:#fff
    style F fill:#10b981,color:#fff
    style G fill:#ec4899,color:#fff

Скидання стилів браузера: appearance і accent-color

appearance: none — чистий аркуш

Кожен браузер має власний User Agent Stylesheet — набір дефолтних стилів для всіх HTML-елементів. Для форм ці стилі особливо агресивні та важко перевизначаються. appearance: none каже браузеру: «анулюй всі свої нативні стилі для цього елемента».

/* Скидання для кнопок */
button {
  appearance: none;
  border: none;
  background: none;
  cursor: pointer;
  font: inherit;
}

/* Скидання для inputs */
input, textarea, select {
  appearance: none;
  border: none;
  outline: none; /* потім додамо власний outline! */
  font: inherit;
}
Після appearance: none не забудьте додати власний:focus-visible стиль. Видалення дефолтного outline без заміни — порушення доступності. Людям, що навігують клавіатурою, потрібен видимий індикатор фокусу.

accent-color — найпростіший спосіб стилізувати форми

accent-color — однорядкова властивість, яка змінює колір нативних форм-елементів: checkbox, radio, range, progress. Не ідеально, але буквально одна рядок замінює сотні рядків CSS з appearance: none.

🔒localhost:3000

Практична робота: Стилізація нативних елементів через accent-color та appearance

🎯 Очікуваний результат: Створення красивої панелі налаштувань користувача, яка містить кастомні чекбокси, радіокнопки, повзунок яскравості (range) та шкалу завантаження (progress). Ми використаємо accent-color для надзвичайно швидкої та нативної зміни акцентних кольорів без написання сотень рядків складного CSS, а також приберемо стандартні браузерні стилі за допомогою appearance: none.

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

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

<!DOCTYPE html>
<html lang="uk">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Практична робота: appearance та accent-color</title>
    <link rel="stylesheet" href="resets.css">
</head>
<body>
    <div class="settings-panel">
        <h2>Налаштування системи 🛠️</h2>
        
        <!-- Чекбокси з кастомним акцентом -->
        <div class="setting-group">
            <span class="group-title">Сповіщення:</span>
            <label class="setting-item">
                <input type="checkbox" checked class="accent-input">
                <span>Електронна пошта</span>
            </label>
            <label class="setting-item">
                <input type="checkbox" class="accent-input">
                <span>SMS-повідомлення</span>
            </label>
        </div>

        <!-- Радіокнопки з іншим акцентом -->
        <div class="setting-group theme-section">
            <span class="group-title">Колірна тема:</span>
            <label class="setting-item">
                <input type="radio" name="theme" checked class="theme-input">
                <span>Світла тема</span>
            </label>
            <label class="setting-item">
                <input type="radio" name="theme" class="theme-input">
                <span>Темна тема</span>
            </label>
        </div>

        <!-- Повзунок Range -->
        <div class="setting-group">
            <span class="group-title">Рівень гучності:</span>
            <input type="range" min="0" max="100" value="70" class="volume-slider">
        </div>

        <!-- Прогрес-бар -->
        <div class="setting-group">
            <span class="group-title">Синхронізація хмари:</span>
            <progress value="65" max="100" class="cloud-progress"></progress>
        </div>
    </div>
</body>
</html>

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

Створіть у тій же папці файл resets.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;
}

.settings-panel {
    background-color: #1e293b;
    border: 1px solid #334155;
    border-radius: 16px;
    padding: 2rem;
    width: 100%;
    max-width: 400px;
    box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
}

h2 {
    margin-top: 0;
    margin-bottom: 1.5rem;
    font-size: 1.5rem;
    color: #e2e8f0;
    border-bottom: 1px solid #334155;
    padding-bottom: 0.75rem;
}

.setting-group {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    margin-bottom: 1.5rem;

    &:last-child {
        margin-bottom: 0;
    }
}

.group-title {
    font-size: 0.85rem;
    font-weight: 700;
    color: #94a3b8;
    text-transform: uppercase;
    letter-spacing: 0.05em;
}

.setting-item {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    cursor: pointer;
    font-size: 0.95rem;
    user-select: none;
}

/* Крок 3: Застосування accent-color для нативних чекбоксів та радіо */
.accent-input {
    accent-color: #3b82f6; /* Нативний привабливий синій колір */
    width: 1.15rem;
    height: 1.15rem;
}

.theme-input {
    accent-color: #10b981; /* Зелений колір для радіокнопок теми */
    width: 1.15rem;
    height: 1.15rem;
}

/* Крок 4: Стилізація Range та Progress */
.volume-slider {
    accent-color: #ec4899; /* Рожевий повзунок яскравості/гучності */
    width: 100%;
    cursor: pointer;
}

.cloud-progress {
    accent-color: #f59e0b; /* Помаранчева шкала виконання прогресу */
    width: 100%;
    height: 10px;
    border-radius: 100px;
    overflow: hidden;
    background-color: #334155;
}

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

  1. Відкрийте файл resets.html у вашому веб-браузері.
  2. Спробуйте поклацати по чекбоксах, радіокнопках та перемістити повзунок гучності.
  3. Зверніть увагу, наскільки чисто та контрастно браузери (Chrome/Safari/Firefox) зафарбували активні стани елементів у відповідні відтінки без нашого втручання в appearance та без кастомної збірки елементів з div і span. Це дає ідеальний UX з мінімальними накладними витратами на підтримку коду!

Псевдокласи стану форм

CSS надає багатий набір псевдокласів, що реагують на стан форм-елементів без жодного JavaScript.

Базові стани: :focus, :focus-visible, :focus-within

ПсевдокласКоли активний
:focusБудь-який фокус (клавіатура і миша)
:focus-visibleЛише коли фокус «видимий» (зазвичай — тільки клавіатура)
:focus-withinЯкщо будь-який нащадок має фокус

:focus-visible — ключовий для доступності: він показує outline при навігації клавіатурою, але не при кліку мишею (що часто дратує).

🔒localhost:3000

Стани вмісту: :checked, :placeholder-shown, :read-only

🔒localhost:3000

Валідаційні псевдокласи: :valid, :invalid, :user-valid, :user-invalid

Ключова різниця: :invalid спрацьовує одразу при завантаженні сторінки, навіть до того, як користувач щось ввів. :user-invalid — лише після першої взаємодії. Це critical UX-деталь.

🔒localhost:3000

Практична робота: Інтерактивне поле введення з підтримкою :focus-visible та делікатною валідацією :user-invalid

🎯 Очікуваний результат: Розробка доступної, зручної для користувача та клавіатурної навігації форми входу з використанням :focus-visible та :user-invalid / :user-valid. Завдяки цим псевдокласам, стилізація помилок (червоний колір, допоміжне повідомлення) з'явиться лише після того, як користувач спробував ввести дані та покинув поле (а не миттєво при завантаженні сторінки), а кільце фокусу виділятиметься лише при навігації за допомогою кнопки Tab.

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

Створіть файл validation.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="validation.css">
</head>
<body>
    <form class="login-form" novalidate>
        <h2>Вхід до кабінету 🔒</h2>
        
        <!-- Поле Email -->
        <div class="field-container">
            <label for="email">Ваш Email:</label>
            <input type="email" id="email" required placeholder="name@domain.com" class="form-input">
            <span class="helper-text error-msg">⚠️ Будь ласка, вкажіть правильну адресу пошти.</span>
            <span class="helper-text success-msg">✓ Email виглядає чудово!</span>
        </div>

        <!-- Поле Пароль -->
        <div class="field-container">
            <label for="password">Пароль (мін. 6 символів):</label>
            <input type="password" id="password" required minlength="6" placeholder="Введіть ваш пароль" class="form-input">
            <span class="helper-text error-msg">⚠️ Пароль має бути не менше 6 символів.</span>
            <span class="helper-text success-msg">✓ Пароль валідний.</span>
        </div>

        <button type="submit" class="submit-btn">Увійти</button>
    </form>
</body>
</html>

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

Створіть у тій же папці файл validation.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;
}

.login-form {
    background-color: #111827;
    border: 1px solid #1e293b;
    border-radius: 12px;
    padding: 2rem;
    width: 100%;
    max-width: 360px;
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
}

h2 {
    margin-top: 0;
    margin-bottom: 1.5rem;
    color: #3b82f6;
    font-size: 1.35rem;
    text-align: center;
}

.field-container {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
    margin-bottom: 1.25rem;
    position: relative;
}

.field-container label {
    font-size: 0.8rem;
    font-weight: 600;
    color: #94a3b8;
}

/* Крок 3: Стилізація полів та кастомного фокусу */
.form-input {
    appearance: none;
    background-color: #1e293b;
    border: 1.5px solid #334155;
    border-radius: 8px;
    padding: 0.6rem 0.85rem;
    color: white;
    font-size: 0.95rem;
    font-family: inherit;
    outline: none; /* Скидаємо дефолтний контур браузера */
    transition: all 0.15s ease-in-out;
}

/* :focus-visible — показуємо підсвічування ТІЛЬКИ при навігації з клавіатури */
.form-input:focus-visible {
    border-color: #3b82f6;
    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
}

/* Крок 4: Делікатна валідація за допомогою :user-invalid та :user-valid */
.helper-text {
    font-size: 0.75rem;
    display: none; /* За замовчуванням приховано */
    margin-top: 0.15rem;
}

/* Якщо поле не пройти валідацію ПІСЛЯ взаємодії */
.form-input:user-invalid {
    border-color: #ef4444;
    background-color: rgba(239, 68, 68, 0.05);
}

/* Показуємо помилку лише для невалідного стану */
.form-input:user-invalid ~ .error-msg {
    display: block;
    color: #fca5a5;
}

/* Коли дані введені вірно */
.form-input:user-valid {
    border-color: #10b981;
    background-color: rgba(16, 185, 129, 0.05);
}

/* Показуємо успіх лише для валідного стану */
.form-input:user-valid ~ .success-msg {
    display: block;
    color: #86efac;
}

/* Кнопка */
.submit-btn {
    width: 100%;
    padding: 0.7rem;
    background-color: #3b82f6;
    color: white;
    border: none;
    border-radius: 8px;
    font-weight: 600;
    cursor: pointer;
    font-family: inherit;
    transition: background-color 0.15s;
    margin-top: 0.5rem;

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

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

  1. Відкрийте файл validation.html у браузері. Зверніть увагу: поля порожні, але жодних червоних рамок чи повідомлень про помилки немає. Це робить перший досвід завантаження форми спокійним.
  2. Клацніть мишкою у поле Email та вийдіть без заповнення. На відміну від звичайного :invalid, червоний колір з'явиться лише після того, як ви спробували взаємодіяти з полем (:user-invalid).
  3. Почніть вводити пошту. Як тільки вона стане коректною, рамка перетвориться на ніжну зелену, та з'явиться підтверджувальне повідомлення.
  4. Перейдіть до поля за допомогою клавіші Tab. Ви помітите чіткий синій ореол фокусу, проте при звичайному кліку мишкою він залишається прихованим. Це створює ідеальний баланс між естетикою та доступністю (Accessibility)!

:has() для розумних форм

:has() відкриває нову еру реактивних форм без JavaScript. Батьківський елемент може реагувати на стан дочірнього.

🔒localhost:3000

Практична робота: Створення розумного опитування з умовним вибором та реактивним підсвічуванням картки через :has()

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

  1. Якщо користувач обирає преміальний план, уся батьківська картка-контейнер отримує яскраве золоте підсвічування та анімований ефект.
  2. Секція вибору криптовалютного гаманця з'являється на екрані тільки у випадку, якщо в радіокнопках обрано спосіб оплати "Crypto" (:has(input[value="crypto"]:checked)).

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

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

<!DOCTYPE html>
<html lang="uk">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Практична робота: Розумна форма на :has()</title>
    <link rel="stylesheet" href="smart-form.css">
</head>
<body>
    <div class="checkout-card">
        <h2>Оформлення підписки 🚀</h2>
        
        <!-- Картка вибору плану -->
        <fieldset class="plan-fieldset">
            <legend>Оберіть тариф:</legend>
            
            <label class="plan-option">
                <input type="radio" name="plan" value="standard" checked>
                <div class="plan-details">
                    <span class="plan-name">Стандарт</span>
                    <span class="plan-price">$9/міс</span>
                </div>
            </label>

            <label class="plan-option premium">
                <input type="radio" name="plan" value="ultra">
                <div class="plan-details">
                    <span class="plan-name">Ультра Преміум 🔥</span>
                    <span class="plan-price">$29/міс</span>
                </div>
            </label>
        </fieldset>

        <!-- Картка способу оплати -->
        <fieldset class="payment-fieldset">
            <legend>Спосіб оплати:</legend>
            
            <label class="payment-option">
                <input type="radio" name="payment" value="card" checked>
                <span>Банківська картка 💳</span>
            </label>

            <label class="payment-option">
                <input type="radio" name="payment" value="crypto">
                <span>Криптовалюта 🪙</span>
            </label>
        </fieldset>

        <!-- Умовний блок оплати Crypto (прихований за замовчуванням) -->
        <div class="crypto-address-block">
            <label for="wallet">Адреса вашого гаманця USDT (TRC-20):</label>
            <input type="text" id="wallet" placeholder="T9yD14Nj9yD14Nj..." class="wallet-input">
            <p class="crypto-warning">Перекажіть точну суму на вказаний рахунок для активації плану.</p>
        </div>

        <button type="submit" class="checkout-btn">Продовжити оплату</button>
    </div>
</body>
</html>

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

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

/* Загальне оформлення */
body {
    background-color: #0b1329;
    color: white;
    font-family: system-ui, sans-serif;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    margin: 0;
}

.checkout-card {
    background-color: #111a36;
    border: 2px solid #1e294b;
    border-radius: 16px;
    padding: 2rem;
    width: 100%;
    max-width: 400px;
    box-shadow: 0 15px 35px rgba(0, 0, 0, 0.4);
    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

h2 {
    margin-top: 0;
    margin-bottom: 1.5rem;
    font-size: 1.4rem;
    text-align: center;
    color: #e2e8f0;
}

fieldset {
    border: 1px solid #1e294b;
    border-radius: 10px;
    padding: 1.25rem;
    margin-bottom: 1.5rem;
}

legend {
    font-size: 0.8rem;
    font-weight: 700;
    color: #64748b;
    text-transform: uppercase;
    padding: 0 0.5rem;
}

.plan-option, .payment-option {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    background-color: #1e294b;
    border: 1.5px solid transparent;
    padding: 0.75rem 1rem;
    border-radius: 8px;
    cursor: pointer;
    margin-bottom: 0.75rem;
    transition: all 0.2s;

    &:last-child {
        margin-bottom: 0;
    }

    &:hover {
        background-color: #27375e;
    }
}

.plan-details {
    display: flex;
    justify-content: space-between;
    width: 100%;
    font-size: 0.95rem;
    font-weight: 600;
}

.plan-price {
    color: #3b82f6;
}

/* Крок 3: Реакція БАТЬКА на стан дитини через :has() */

/* 1. Робимо рамку картки золотою, якщо всередині обрано преміум-радіокнопку */
.checkout-card:has(.premium input:checked) {
    border-color: #fbbf24;
    box-shadow: 0 0 25px rgba(251, 191, 36, 0.2);
}

/* Змінюємо колір заголовка форми на золотий */
.checkout-card:has(.premium input:checked) h2 {
    color: #fbbf24;
}

/* 2. Показуємо умовний крипто-блок тільки коли checked радіокнопка зі значенням 'crypto' */
.crypto-address-block {
    display: none; /* За замовчуванням сховано */
    background-color: rgba(59, 130, 246, 0.05);
    border: 1px dashed #1e294b;
    border-radius: 8px;
    padding: 1rem;
    margin-bottom: 1.5rem;
    animation: fadeIn 0.3s ease-out;
}

/* Використовуємо :has() для показу крипто-секції */
.checkout-card:has(input[value="crypto"]:checked) .crypto-address-block {
    display: block;
}

/* Стилі для вводу адреси */
.crypto-address-block label {
    font-size: 0.8rem;
    color: #94a3b8;
    display: block;
    margin-bottom: 0.4rem;
}

.wallet-input {
    width: 100%;
    background-color: #0b1329;
    border: 1.5px solid #1e294b;
    border-radius: 6px;
    padding: 0.5rem;
    color: white;
    font-family: monospace;
    box-sizing: border-box;
    outline: none;

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

.crypto-warning {
    font-size: 0.72rem;
    color: #f59e0b;
    margin: 0.5rem 0 0 0;
}

/* Кнопка */
.checkout-btn {
    width: 100%;
    padding: 0.75rem;
    background-color: #3b82f6;
    color: white;
    border: none;
    border-radius: 8px;
    font-weight: 700;
    cursor: pointer;
    transition: background-color 0.15s;

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

/* Плавна поява блоку */
@keyframes fadeIn {
    from {
        opacity: 0;
        transform: translateY(-5px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

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

  1. Відкрийте файл smart-form.html у браузері.
  2. Спробуйте змінити тарифний план на Ультра Преміум 🔥. Зверніть увагу: як тільки ви вибираєте преміум-варіант, вся форма (.checkout-card) динамічно змінює свій зовнішній вигляд, підсвічуючи рамку та змінюючи колір тексту. Це реалізовано за допомогою правила .checkout-card:has(.premium input:checked).
  3. Оберіть спосіб оплати Криптовалюта 🪙. Миттєво на екрані з'явиться поле вводу гаманця. При поверненні на банківську картку блок зникає. Раніше для цього був потрібен скрипт на кшталт element.addEventListener('change'), але тепер селектор :has(input[value="crypto"]:checked) повністю закриває цю потребу!

Кастомний Checkbox та Radio

appearance: none + ::before/::after + :checked — класична техніка для повністю кастомних форм-елементів.

🔒localhost:3000

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

🎯 Очікуваний результат: Створення повністю унікального блоку налаштувань підписки з двома елементами:

  1. Дизайнерський iOS-Style Toggle Switch (перемикач сповіщень).
  2. Набір карток-радіокнопок для вибору періоду оплати (Щомісячно / Щорічно зі знижкою). Обидва елементи створюються шляхом повного приховування нативних input[type="checkbox"] та input[type="radio"] за допомогою display: none; (або opacity: 0; position: absolute; для збереження доступності) та стилізації сусідніх елементів через селектор + (суміжний селектор) та стан :checked.

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

Створіть файл custom-buttons.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="custom-buttons.css">
</head>
<body>
    <div class="billing-panel">
        <h2>Період підписки 🗓️</h2>
        
        <!-- Toggle Switch -->
        <div class="toggle-section">
            <span class="toggle-title">Автоматичне подовження:</span>
            <label class="switch">
                <input type="checkbox" class="switch-checkbox" checked>
                <span class="switch-slider"></span>
            </label>
        </div>

        <!-- Radio Cards -->
        <div class="cards-section">
            <p class="section-subtitle">Оберіть варіант білінгу:</p>
            
            <div class="radio-cards-grid">
                <label class="card-option">
                    <input type="radio" name="billing-cycle" value="monthly" checked class="card-radio-input">
                    <div class="card-inner">
                        <span class="badge">Базовий</span>
                        <strong class="period">Щомісячно</strong>
                        <span class="price">$15/міс</span>
                    </div>
                </label>

                <label class="card-option">
                    <input type="radio" name="billing-cycle" value="yearly" class="card-radio-input">
                    <div class="card-inner">
                        <span class="badge discount">Знижка -20%</span>
                        <strong class="period">Щорічно</strong>
                        <span class="price">$12/міс</span>
                    </div>
                </label>
            </div>
        </div>
    </div>
</body>
</html>

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

Створіть у тій же папці файл custom-buttons.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;
}

.billing-panel {
    background-color: #111827;
    border: 1px solid #1e293b;
    border-radius: 16px;
    padding: 2rem;
    width: 100%;
    max-width: 420px;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
}

h2 {
    margin-top: 0;
    margin-bottom: 1.5rem;
    font-size: 1.3rem;
    color: #e2e8f0;
}

/* Крок 3: Стилізація iOS-style Toggle Switch */
.toggle-section {
    display: flex;
    justify-content: space-between;
    align-items: center;
    background-color: #1e293b;
    padding: 1rem;
    border-radius: 12px;
    margin-bottom: 1.75rem;
}

.toggle-title {
    font-size: 0.9rem;
    font-weight: 600;
    color: #94a3b8;
}

.switch {
    position: relative;
    display: inline-block;
    width: 50px;
    height: 28px;
}

/* Повністю ховаємо нативний чекбокс */
.switch-checkbox {
    opacity: 0;
    width: 0;
    height: 0;
}

/* Основа повзунка (трек) */
.switch-slider {
    position: absolute;
    cursor: pointer;
    inset: 0;
    background-color: #334155;
    border-radius: 34px;
    transition: background-color 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}

/* Круглий бігунок */
.switch-slider::before {
    content: "";
    position: absolute;
    height: 22px;
    width: 22px;
    left: 3px;
    bottom: 3px;
    background-color: white;
    border-radius: 50%;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
    transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}

/* Зміна кольору треку при checked */
.switch-checkbox:checked + .switch-slider {
    background-color: #10b981; /* Зелений колір активованого стану */
}

/* Переміщення кружечка вправо */
.switch-checkbox:checked + .switch-slider::before {
    transform: translateX(22px);
}

/* Крок 4: Стилізація Radio Cards */
.section-subtitle {
    font-size: 0.8rem;
    font-weight: 700;
    color: #64748b;
    text-transform: uppercase;
    margin-bottom: 0.75rem;
}

.radio-cards-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1rem;
}

/* Сховуємо нативне радіо, але зберігаємо можливість фокусування */
.card-radio-input {
    position: absolute;
    opacity: 0;
    width: 0;
    height: 0;
}

.card-option {
    cursor: pointer;
}

.card-inner {
    background-color: #1e293b;
    border: 2px solid #334155;
    border-radius: 12px;
    padding: 1.25rem 1rem;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.5rem;
    text-align: center;
    transition: all 0.2s ease-in-out;
}

.badge {
    font-size: 0.65rem;
    font-weight: 800;
    text-transform: uppercase;
    background-color: #334155;
    padding: 0.2rem 0.5rem;
    border-radius: 100px;
    color: #94a3b8;
}

.badge.discount {
    background-color: rgba(16, 185, 129, 0.15);
    color: #10b981;
}

.period {
    font-size: 1rem;
    color: white;
}

.price {
    font-size: 1.2rem;
    font-weight: 700;
    color: #3b82f6;
}

/* Реакція на вибір картки (:checked) */
.card-radio-input:checked + .card-inner {
    border-color: #3b82f6;
    background-color: rgba(59, 130, 246, 0.05);
    transform: translateY(-2px);
    box-shadow: 0 4px 15px rgba(59, 130, 246, 0.15);
}

/* Коли вибрано річний план зі знижкою */
.card-radio-input:checked + .card-inner:has(.discount) {
    border-color: #10b981;
    background-color: rgba(16, 185, 129, 0.05);
    box-shadow: 0 4px 15px rgba(16, 185, 129, 0.15);
}

.card-radio-input:checked + .card-inner .period {
    color: #3b82f6;
}

.card-radio-input:checked + .card-inner:has(.discount) .period {
    color: #10b981;
}

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

  1. Відкрийте файл custom-buttons.html у вашому веб-браузері.
  2. Спробуйте увімкнути та вимкнути слайдер автоподовження. Переконайтеся, що анімація зсуву кружечка та перефарбування тла відбувається плавно завдяки властивості transition у CSS.
  3. Клацніть по картках вибору періоду білінгу. Помітьте плавне підсвічування відповідної рамки, зміну кольорів шрифтів та легкий ефект підняття картки вгору за допомогою transform: translateY(-2px);.
  4. Оскільки мы повністю сховали нативні елементи, кастомна верстка дає розробникам 100% контроль над кожним пікселем дизайну форм на будь-якому пристрої!

Кастомний Select

<select> — один із найскладніших елементів для стилізації. appearance: none прибирає нативну стрілку, дозволяючи додати кастомну через background-image.

🔒localhost:3000

Практична робота: Створення випадаючого списку (Dropdown Select) з власною іконкою стрілки

🎯 Очікуваний результат: Створення повністю брендованого, чистого випадаючого списку <select> для вибору мови інтерфейсу або країни. Ми повністю позбудемося негарної системної стрілки операційної системи (яка виглядає по-різному на кожному пристрої) за допомогою appearance: none, та намалюємо власну векторну стрілку на чистому CSS через абсолютно позиціонований псевдоелемент на обгортці контейнера.

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

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

<!DOCTYPE html>
<html lang="uk">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Практична робота: Кастомний Select</title>
    <link rel="stylesheet" href="custom-select.css">
</head>
<body>
    <div class="form-card">
        <h2>Оберіть локалізацію 🌐</h2>
        
        <div class="select-field">
            <label for="lang-select" class="select-label">Мова інтерфейсу:</label>
            
            <!-- Спеціальна обгортка-контейнер для кастомної стрілочки -->
            <div class="custom-select-container">
                <select id="lang-select" class="native-select">
                    <option value="uk" selected>Українська (UA) 🇺🇦</option>
                    <option value="en">English (US) 🇺🇸</option>
                    <option value="pl">Polski (PL) 🇵🇱</option>
                    <option value="de">Deutsch (DE) 🇩🇪</option>
                </select>
            </div>
        </div>
        
        <button class="save-btn">Зберегти налаштування</button>
    </div>
</body>
</html>

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

Створіть у тій же папці файл custom-select.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;
}

.form-card {
    background-color: #111827;
    border: 1px solid #1e293b;
    border-radius: 12px;
    padding: 2rem;
    width: 100%;
    max-width: 340px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
}

h2 {
    margin-top: 0;
    margin-bottom: 1.5rem;
    font-size: 1.25rem;
    color: #e2e8f0;
}

.select-field {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    margin-bottom: 1.5rem;
}

.select-label {
    font-size: 0.8rem;
    font-weight: 600;
    color: #94a3b8;
}

/* Крок 3: Обгортка та малювання кастомної стрілки */
.custom-select-container {
    position: relative; /* Необхідно для абсолютного позиціонування стрілки */
    width: 100%;
}

/* Малюємо трикутник-стрілочку через псевдоелемент ::after */
.custom-select-container::after {
    content: "";
    position: absolute;
    right: 1rem;
    top: 50%;
    transform: translateY(-50%); /* Центруємо по вертикалі */
    
    /* Створюємо трикутник за допомогою класичної CSS-техніки border */
    border-left: 5px solid transparent;
    border-right: 5px solid transparent;
    border-top: 6px solid #94a3b8; /* Колір стрілочки */
    
    pointer-events: none; /* Пропускаємо кліки мишкою безпосередньо у <select> */
    transition: transform 0.2s ease, border-top-color 0.2s;
}

/* Крок 4: Скидання стилів нативного select та додавання власних */
.native-select {
    appearance: none; /* ПОВНІСТЮ скидає нативну стрілку операційної системи */
    -webkit-appearance: none; /* Скидання для Safari */
    -moz-appearance: none; /* Скидання для Firefox */
    
    width: 100%;
    background-color: #1e293b;
    border: 1.5px solid #334155;
    border-radius: 8px;
    padding: 0.65rem 2.5rem 0.65rem 0.85rem; /* Звичайно збільшений правий паддінг, щоб текст не наповзав на стрілку */
    color: white;
    font-size: 0.95rem;
    font-family: inherit;
    cursor: pointer;
    outline: none;
    transition: all 0.2s;
}

/* Ефекти фокусу */
.native-select:focus {
    border-color: #3b82f6;
    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.25);
}

/* Повертаємо стрілку вгору при розгортанні списку (підтримка у сучасних браузерах через :focus) */
.custom-select-container:focus-within::after {
    transform: translateY(-50%) rotate(180deg);
    border-top-color: #3b82f6;
}

/* Кнопка */
.save-btn {
    width: 100%;
    padding: 0.7rem;
    background-color: #3b82f6;
    color: white;
    border: none;
    border-radius: 8px;
    font-weight: 600;
    cursor: pointer;
    transition: background-color 0.15s;

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

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

  1. Відкрийте файл custom-select.html у браузері.
  2. Зверніть увагу на ідеально однакову стрілочку вибору мов. Вона виглядає витончено та підходить під загальний темний стиль інтерфейсу.
  3. Клацніть на селект та виберіть мову. Помітьте, як при отриманні фокусу стрілочка акуратно розгортається на 180 градусів за допомогою правила .custom-select-container:focus-within::after та набуває синього відтінку.
  4. Властивість pointer-events: none на псевдоелементі є критично важливою: без неї клік по самій стрілочці блокував би розгортання списку, оскільки вона фізично перекривала б собою зону кліку по <select>.

Floating Label патерн (CSS-only)

Floating Label — поле введення, де заповнювач «зависає» над полем при введенні тексту. Класичний патерн Material Design, реалізований лише на CSS через :placeholder-shown та :not(:placeholder-shown).

🔒localhost:3000

Практична робота: Створення преміального поля з плаваючим заголовком (Floating Label)

🎯 Очікуваний результат: Створення вишуканого текстового поля, заголовок якого виступає як плейсхолдер усередині поля, а під час отримання фокусу чи введення тексту — плавно зсувається вгору та зменшується у розмірах (Floating Label). Ми побудуємо цей ефект виключно на CSS без допомоги бібліотек чи JS-скриптів за допомогою поєднання властивостей :placeholder-shown, :focus та суміжних сусідніх селекторів (+).

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

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

<!DOCTYPE html>
<html lang="uk">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Практична робота: Floating Label</title>
    <link rel="stylesheet" href="floating.css">
</head>
<body>
    <div class="feedback-card">
        <h2>Залишити відгук 💬</h2>
        
        <!-- Контейнер для поля введення обов'язково має мати відносне позиціонування -->
        <div class="floating-field">
            <!-- Важливо: плейсхолдер має складатися з ОДНОГО ПРОБІЛУ " ", щоб коректно спрацьовувала властивість :placeholder-shown -->
            <input type="text" id="username" class="floating-input" placeholder=" " required autocomplete="off">
            <label for="username" class="floating-label">Ваше повне ім'я</label>
        </div>

        <div class="floating-field">
            <textarea id="message" class="floating-input floating-textarea" placeholder=" " required></textarea>
            <label for="message" class="floating-label">Текст вашого повідомлення</label>
        </div>

        <button class="send-btn">Надіслати відгук</button>
    </div>
</body>
</html>

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

Створіть у тій же папці файл floating.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;
}

.feedback-card {
    background-color: #1e293b;
    border: 1px solid #334155;
    border-radius: 16px;
    padding: 2.25rem 2rem;
    width: 100%;
    max-width: 360px;
    box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
}

h2 {
    margin-top: 0;
    margin-bottom: 2rem;
    font-size: 1.35rem;
    color: #f1f5f9;
    text-align: center;
}

/* Крок 3: Контейнер та базові стилі полів введення */
.floating-field {
    position: relative; /* Критично для абсолютного розміщення плаваючого тексту */
    margin-bottom: 1.5rem;
}

.floating-input {
    appearance: none;
    width: 100%;
    background-color: #0f172a;
    border: 1.5px solid #334155;
    border-radius: 8px;
    
    /* Збільшений верхній паддінг звільняє місце під зменшений плаваючий заголовок */
    padding: 1.4rem 0.85rem 0.5rem 0.85rem; 
    
    color: white;
    font-size: 0.95rem;
    font-family: inherit;
    outline: none;
    box-sizing: border-box;
    transition: border-color 0.2s, box-shadow 0.2s;
}

.floating-textarea {
    min-height: 100px;
    resize: vertical; /* Дозволяємо змінювати розмір по вертикалі */
}

/* Зміна кольору рамки при фокусі */
.floating-input:focus {
    border-color: #3b82f6;
    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
}

/* Крок 4: Магія Floating Label на чистому CSS */
.floating-label {
    position: absolute;
    left: 0.85rem;
    top: 0.95rem; /* Позиціонуємо label рівно по центру порожнього поля */
    
    font-size: 0.95rem;
    color: #64748b;
    pointer-events: none; /* Дозволяємо клікати крізь заголовок безпосередньо в інпут */
    
    /* Додаємо плавні переходи для всіх властивостей */
    transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
    transform-origin: left top; /* Точка трансформації для ідеально точного масштабування */
}

/* Коли інпут НЕ у фокусі та містить плейсхолдер (порожній), заголовок великий */
.floating-input:placeholder-shown + .floating-label {
    top: 0.95rem;
    font-size: 0.95rem;
    color: #64748b;
}

/* Коли інпут Отримує фокус АБО коли він НЕ порожній (:not(:placeholder-shown)) — заголовок злітає вгору! */
.floating-input:focus + .floating-label,
.floating-input:not(:placeholder-shown) + .floating-label {
    top: 0.35rem;
    transform: scale(0.75); /* Масштабуємо заголовок (зменшуємо до 75% розміру) */
    color: #3b82f6; /* Надаємо яскравого кольору */
    font-weight: 600;
}

/* Якщо поле містить невалідні дані (наприклад, порожнє обов'язкове поле) */
.floating-input:user-invalid {
    border-color: #ef4444;
}

.floating-input:user-invalid + .floating-label {
    color: #fca5a5;
}

/* Кнопка */
.send-btn {
    width: 100%;
    padding: 0.75rem;
    background-color: #3b82f6;
    color: white;
    border: none;
    border-radius: 8px;
    font-weight: 700;
    cursor: pointer;
    transition: background-color 0.15s;

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

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

  1. Відкрийте файл floating.html у вашому браузері.
  2. Клацніть всередину будь-якого поля. Побачте, як текст заголовка плавно переміщується вгору та зменшується, звільняючи місце під ввід даних.
  3. Введіть декілька літер та зніміть фокус (клацніть в інше місце). Зверніть увагу: оскільки поле тепер не є порожнім (плейсхолдер " " прихований, тому :placeholder-shown є хибним), заголовок надійно залишається у верхній позиції.
  4. Спробуйте видалити весь текст. Побачте, як плавний заголовок повертається до свого початкового вигляду великого плейсхолдера. Це і є чистий, декларативний CSS-патерн без використання важких JS-сценаріїв!

Кастомний Range Slider

🔒localhost:3000

Практична робота: Розробка аудіоплеєра з кастомним повзунком гучності (Range Slider)

🎯 Очікуваний результат: Створення повністю брендованого кастомного повзунка (Range Slider) для регулювання гучності або яскравості. Оскільки елемент <input type="range"> складається з двох окремих частин (треку та рухомого бігунка), а різні браузери (Chromium, Firefox, Safari) використовують несумісні псевдоелементи для їх стилізації, студенти створять кросбраузерний адаптивний слайдер. Ми стилізуємо круглий бігунок (::-webkit-slider-thumb для Chrome/Safari та ::-moz-range-thumb для Firefox), який інтерактивно збільшуватиметься при перетягуванні (:active).

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

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

<!DOCTYPE html>
<html lang="uk">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Практична робота: Кастомний Range Slider</title>
    <link rel="stylesheet" href="slider.css">
</head>
<body>
    <div class="player-card">
        <h2>Аудіоплеєр 🎵</h2>
        
        <!-- Контролер гучності -->
        <div class="slider-container">
            <div class="slider-header">
                <span class="volume-icon">🔊</span>
                <span class="volume-label">Гучність:</span>
                <span id="volume-value" class="volume-value">60%</span>
            </div>
            
            <input type="range" min="0" max="100" value="60" id="volume-range" class="player-slider"
                   oninput="document.getElementById('volume-value').textContent = this.value + '%'">
        </div>
        
        <p class="slider-tip">Спробуйте перетягнути бігунок — він плавно збільшиться при натисканні!</p>
    </div>
</body>
</html>

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

Створіть у тій же папці файл slider.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;
}

.player-card {
    background-color: #111827;
    border: 1px solid #1e293b;
    border-radius: 16px;
    padding: 2rem;
    width: 100%;
    max-width: 320px;
    box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
    text-align: center;
}

h2 {
    margin-top: 0;
    margin-bottom: 2rem;
    font-size: 1.3rem;
    color: #e2e8f0;
}

.slider-container {
    margin-bottom: 1.5rem;
}

.slider-header {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    font-size: 0.85rem;
    font-weight: 600;
    color: #94a3b8;
    margin-bottom: 0.75rem;
}

.volume-value {
    margin-left: auto; /* Притискає відсоток гучності до правого краю */
    color: #3b82f6;
    font-weight: 700;
}

/* Крок 3: Скидання стандартних стилів треку */
.player-slider {
    appearance: none;
    -webkit-appearance: none; /* Скидання для Safari/Chrome */
    width: 100%;
    height: 6px; /* Задаємо власну товщину треку */
    background-color: #334155; /* Колір неактивної частини треку */
    border-radius: 100px;
    outline: none;
    cursor: pointer;
    transition: background-color 0.2s;
}

.player-slider:focus-visible {
    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
}

/* Крок 4: Кросбраузерна стилізація бігунка (Thumb) */

/* Стилізація бігунка для WebKit-браузерів (Chrome, Safari, Edge, Opera) */
.player-slider::-webkit-slider-thumb {
    appearance: none;
    -webkit-appearance: none;
    
    height: 18px;
    width: 18px;
    border-radius: 50%;
    background-color: #ffffff;
    border: 3.5px solid #3b82f6; /* Синій обідок навколо білого ядра */
    box-shadow: 0 2px 6px rgba(59, 130, 246, 0.4);
    
    cursor: grab;
    
    /* Плавний ефект масштабування при взаємодії */
    transition: transform 0.15s cubic-bezier(0.4, 0, 0.2, 1), background-color 0.15s;
}

/* Стилізація бігунка для Gecko-браузерів (Firefox) */
.player-slider::-moz-range-thumb {
    height: 12px; /* У Firefox розмір рамки border сумується, тому робимо менший діаметр */
    width: 12px;
    border-radius: 50%;
    background-color: #ffffff;
    border: 3.5px solid #3b82f6;
    box-shadow: 0 2px 6px rgba(59, 130, 246, 0.4);
    
    cursor: grab;
    transition: transform 0.15s cubic-bezier(0.4, 0, 0.2, 1), background-color 0.15s;
}

/* Крок 5: Інтерактивний стан активного перетягування */

.player-slider::-webkit-slider-thumb:active {
    cursor: grabbing;
    transform: scale(1.25); /* Збільшуємо бігунок на 25% при затисканні миші */
    background-color: #3b82f6; /* Фарбуємо середину в синій */
}

.player-slider::-moz-range-thumb:active {
    cursor: grabbing;
    transform: scale(1.25);
    background-color: #3b82f6;
}

.slider-tip {
    font-size: 0.75rem;
    color: #64748b;
    line-height: 1.5;
    margin: 0;
}

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

  1. Відкрийте файл slider.html у браузері.
  2. Спробуйте потягнути бігунок. При натисканні на нього лівою кнопкою миші він збільшується та зафарбовується, створюючи приємний відгук на дію користувача (Active State).
  3. Зверніть увагу: ми розділили правила для ::-webkit-slider-thumb та ::-moz-range-thumb. Це критично важливо! Якщо спробувати об'єднати їх через кому в один селектор (наприклад, .slider::-webkit-slider-thumb, .slider::-moz-range-thumb), браузер, який не підтримує один із цих вендорних селекторів, повністю проігнорує та відкине все CSS-правило, залишивши ваш бігунок без стилів взагалі.

Практика

Рівень 1 — Базовий: стилізація стану форм

Надано HTML форми реєстрації. Додайте CSS, що:

  • Підсвічує поля зеленим при :user-valid та червоним при :user-invalid
  • Показує та іконки через ::after на обгортці поля залежно від стану
  • Прибирає нативний outline та додає власний box-shadow при :focus-visible

Рівень 2 — Логіка/Інтерактивність: реактивна форма через ()

Реалізуйте форму замовлення із smart-поведінкою лише на CSS:

  • Якщо вибрано <input type="radio" value="delivery"> — показати блок з адресою доставки (:has(:checked[value="delivery"]))
  • Якщо всі обов'язкові поля валідні — кнопка «Оформити» стає яскравою (:has(input:required:valid))
  • Label підсвічується рожевим при :user-invalid вкладеного input

Рівень 3 — Архітектура: Design System форм

Створіть повну систему стилів форм:

  1. Токени: --input-border, --input-focus-ring, --input-error, --input-success через CSS Custom Properties
  2. Компонент .field: floating label + error message + success state через Nesting
  3. Варіанти: --outlined (border), --filled (background), --underlined (лише нижній border)
  4. Теми: dark mode через @media (prefers-color-scheme: dark) що перевизначає токени
  5. Весь CSS — через @layer components + @property для анімованих токенів

Підсумок

🎨 accent-color

Одна властивість для нативного кольору checkbox, radio, range, progress. Мінімум CSS — максимум результату.

👁️ :focus-visible

Показує outline лише при навігації клавіатурою. Використовуйте замість :focus для кращого UX та доступності.

✅ :user-valid / :user-invalid

Валідація лише після взаємодії — без стресу від червоних полів одразу при завантаженні. Chrome 119+, Safari 16.5+, FF 88+.

🧠 :has() для форм

:has(input:focus) на label, :has(:checked) для умовного показу блоків, :has(:user-invalid) для підсвітки всієї форми — без JavaScript.

🔘 appearance: none

Скидає нативні стилі. Відправна точка для кастомних checkbox, radio, select. Завжди додавайте :focus-visible після скидання!

🏷️ Floating Label

:placeholder-shown + :not(:placeholder-shown) — чистий CSS-патерн без JavaScript. Placeholder повинен бути пробілом " " для коректної роботи.
Copyright © 2026