CSS-хитрощі. Як нові властивості змінюють розробку адаптивних інтерфейсів

💡 Усі статті, обговорення, новини про Front-end — в одному місці. Приєднуйтесь до Front-end спільноти!

Привіт. Мене звати Христина, я обіймаю позицію Front-End розробника у компанії Langate Software. Часто працюю саме з CSS-кодом та стикаюсь зі складнощами реалізації макетів або оптимізації коду.

Тому хотіла б поділитись своїм досвідом та деякими, на мою думку, цікавими випадками, коли доводилось шукати нові підходи CSS або поєднувати між собою ті, які вже давно популярні.


Хоча основи CSS відомі багатьом, існує ряд властивостей, які залишаються недооціненими, маловідомими чи забутими. Крім того, кожен новий реліз також приносить нові можливості CSS. Більшість із цих інновацій залишається поза увагою через те, що вони не так широко використовуються. Однак саме ці маловідомі властивості часто мають прихований потенціал для розв’язання специфічних або оптимізації звичайних завдань.

Як уникнути впливу інтерфейсу браузера на адаптивну висоту елементів

Мабуть, багато розробників стикалися з цією проблемою, особливо стилізуючи елементи для мобільних пристроїв. Я була не винятком — коректно прописати висоту елементів (особливо модальних вікон) для мобільних пристроїв було нелегко. Різні пристрої мають свою поведінку: на деяких смартфонах при взаємодії з екраном з’являється або зникає адресний рядок, через що змінюється доступна висота вікна браузера. На інших цього не відбувається, і висота залишається незмінною.

Використовувати стандартні відносні одиниці вимірювання, такі як vh або %, було недостатньо. Річ у тім, що vh (viewport height) завжди враховує поточну видиму висоту вікна, але не реагує на динамічні зміни інтерфейсу браузера. Наприклад, на появу або зникнення під час скролу адресного рядка.

Використання відсотків (%) також не було ідеальним рішенням, оскільки вони залежать від висоти батьківського елемента. Якщо у батьківського елемента, як-то або

, нема заданої явної висоти, відсоткові значення для модального вікна не можуть коректно адаптуватися до динамічної висоти екрана.

Орієнтовно у 2022 році у CSS з’явилися три нові величини для властивості висоти: lvh, svh і dvh. Це дозволило точніше контролювати адаптивність елементів, враховуючи різні сценарії взаємодії з вікном браузера і при цьому забезпечуючи більш адаптивний дизайн.

  • lvh (Large Viewport Height) — ця одиниця відображає повну висоту екрана, коли інтерфейс браузера не впливає на доступну висоту. Наприклад, на мобільних пристроях після прокручування адресний рядок зникає, звільняючи більше місця для відображення контенту. І ось якраз одиниця вимірювання lvh дозволяє врахувати цю велику висоту екрана, коли браузер мінімізує свої елементи.
  • svh (Small Viewport Height) — на відміну від lvh, ця одиниця вимірює мінімальну можливу висоту екрану, коли інтерфейс браузера займає більше простору. Наприклад, при першому завантаженні сторінки на мобільному телефоні, коли видимий адресний рядок ще не прихований. svh корисна для тих випадків, коли необхідно адаптувати дизайн до ситуації, де доступної висоти менше.
  • dvh (Dynamic Viewport Height) — це динамічна одиниця, яка враховує зміни в інтерфейсі браузера в режимі реального часу. Особисто я її використовую найчастіше, оскільки ця одиниця — свого роду динамічне поєднання двох попередніх. Наприклад, коли користувач взаємодіє з екраном, і адресний рядок зникає або з’являється, dvh автоматично підлаштовує висоту елементів під ці зміни.

Я не могла говорити про dvh, не пригадавши lvh і svh, бо всі три з’явились у CSS одночасно. Але оскільки знайшла розв’язання своєї проблеми саме за допомогою одиниці dvh, хочу наочно показати різницю між нею та vh-величиною.

<header style="height: 100vh">
    

<h1>Секція з використанням vh одиниці</h1>
    <div class="section-description">
        
Додаткова інформація секції, яку щоб побачити повністю потрібно проскролити трішки вниз, якщо у браузері мобільного пристрою буде присутній адресний рядок.

    </div>

</header>

Як ми бачимо, текст нижньої секції можна прочитати без прокрутки вниз, коли адресного рядка нема. Коли ж він присутній, то аби текст був видимим повністю, потрібно проскролити трохи вниз.

<header style="height: 100dvh">
    

<h1>Секція з використанням dvh одиниці</h1>
    <div class="section-description">
        
Додаткова інформація секції, яку щоб побачити повністю потрібно проскролити трішки вниз, якщо у браузері мобільного пристрою буде присутній адресний рядок.

    </div>

</header>

У випадку використання одиниці вимірювання dvh висота контенту буде «підлаштовуватись» до появи або зникнення адресного рядка браузера, що є доволі корисним.

Ці нові відносні одиниці саме для покращення адаптивного дизайну. На екрані ноутбука чи комп’ютера навряд можна буде побачити їх переваги, оскільки на цих пристроях адресний рядок браузера зазвичай статичний, а не динамічний при скролі.

Як стилізувати батьківський елемент, базуючись на його дочірніх

Це питання «сиділо» у мене в голові ще з моменту початку шляху у Front-End розробці. Насправді це було цілою проблемою і я не розуміла, чому функціонал для того, аби вибірково стилізувати дочірні елементи є (:first/last/nth-child, :first/last/nth-of-type), а для зворотного процесу — нема. Маю на увазі саме ті ситуації, коли у нас нема унікальних селекторів, щоб звернутись до них напряму.

У мене в одному з доволі немаленьких проєктів було багато popup-елементів, які реалізовувались через сторонню UI-бібліотеку. Відповідно HTML-розмітка разом з усіма селекторами генерувалась нею. Можливість була лише керувати (і, відповідно, використовувати свої селектори) розміткою всередині самого popup-елементу — контентом. Одному з popup-елементів потрібно було присвоїти специфічне позиціювання.

Але постало питання — як це зробити, якщо позиціювання стоїть на «обгортці», а це означає — однаковий селектор для всіх popup-елементів платформи. На щастя, у 2022 році браузерами почав підтримуватись псевдоклас :has(). Я вважаю, це доволі потужний інструмент, який вирішив дуже багато важких, а іноді й неможливих для реалізації випадків.

Використовується він, як і всі псевдокласи — ставиться двокрапка, пишеться слово has, а в круглих дужках вказується селектор, на наявність якого має орієнтуватись батьківський елемент:

.popup:has(.hamburger-menu) {
    left: unset;
    right: 20px;
}

Таким чином, всі popup-елементи на проєкті мали позиціювання, визначене зовнішньою бібліотекою, тоді як для popup-елемента, що використовувався для гамбургер-меню, можна було встановити інше, індивідуальне позиціювання.

Розумні стилі або додавання логіки у стилізацію

Працюючи над великим чи малим проєктом, іноді можна зіткнутися з проблемою керованості стилів. Це може бути пов’язано як з функціональною навантаженістю, так і зі специфікою дизайну.

@property: як створити власні CSS-змінні з контролем поведінки

Працювала я якось над інтерфейсом e-commerce платформи, де кожен продукт мав динамічні блоки з інформацією про знижки. Щоб зробити UI цікавішим, дизайнери вирішили додати плавну анімацію для відображення цієї знижки: колір фону продукту змінювався залежно від того, наскільки вона велика — від легкого зеленого до насиченого червоного.

У традиційному CSS анімування змінних не працює належним чином, оскільки вони за замовчуванням мають тип ’string’, що не дозволяє безпосередньо анімувати їхні числові значення. Проте анімувати змінну, яка контролює відсоток знижки, і плавно змінювати колір залежно від значення вдалось за допомогою @property:

/* Оголошення власної CSS-змінної з визначенням типу та можливістю анімації */
@property --discount {
  syntax: '<number>';
  inherits: false;
  initial-value: 0;
}

/* Стилізація блоку продукту з динамічним кольором фону */
.product {
  --discount: 0; /* Початкове значення */
  background-color: hsl(calc(120 - var(--discount) * 1.2), 70%, 50%);
  transition: --discount 1s ease-in-out;
}

/* Актуалізація значення змінної для різних рівнів знижки */
.product[data-discount='10'] {
  --discount: 10;
}

.product[data-discount='50'] {
  --discount: 50;
}

.product[data-discount='90'] {
  --discount: 90;
}

Яким чином це буде працювати: @property дозволяє оголосити змінну —discount як тип number, що означає, що її можна анімувати й головне — робити анімацію плавною. В цьому прикладі я використовувала кольорову модель HSL для динамічної зміни кольору фону, де насиченість та відтінок змінювались відповідно до величини знижки.

Варто зауважити, що у цьому прикладі використовувати потрібно саме HSL-модель, оскільки за допомогою неї можна легко маніпулювати відтінком (hue), насиченістю (saturation), і світлістю (lightness). Адже ці параметри представлені як окремі числові значення, що дає змогу використовувати такі CSS-функції, як calc(), для динамічних змін.

Формат rgb() (або rgba(), якщо враховувати прозорість) використовує три або чотири значення для червоного (red), зеленого (green), синього (blue) і альфа-каналу (прозорість). Оскільки rgb() не розбивається на відокремлені параметри, такі як у HSL, маніпулювати кожним компонентом окремо за допомогою calc() неможливо.

@container: як адаптувати стилі до розміру контейнера, а не екрану

Одного разу на своєму я проєкті зіткнулась із такою ситуацією — додавали ми нашу сторінку як віджет і відповідно вона займала якусь певну частинку в основній програмі, не весь viewport. І ось проблема — адаптив прописаний через media queries і стилі дивляться на ширину екрана, а з ним все добре — відкрита собі сторінка браузера у десктопному розмірі.

А от стилі нашого віджета уже «поїхали», бо для них з viewport все добре і ніяких адаптивних стилей застосовувати не потрібно. Тоді я і познайомилась із CSS Container Queries, які дозволили «дивитись» стилям на розмір контейнера віджета і відповідно робити дизайн гарним та адаптивним.

.activityWidget {
    container-type: inline-size;
    container-name: activityMainSection;
}

.media-block {
    margin-inline: 20px;

    /* Запит до контейнера activityMainSection */
    @container activityMainSection (max-width: 575px) {
        margin-left: unset;
    }
}

Коли оголошуємо контейнер, на який, власне, і повинен орієнтуватись контент, потрібно використовувати дві властивості: container-type та container-name. З останньою все зрозуміло — це назва для контейнера, яка використовується в синтаксисі @container для визначення, до якого саме контейнера будуть застосовуватись стилі. Щодо властивості container-type — вона визначає тип контейнера, який буде використовуватися для контейнерних запитів.

inline-size означає, що запити на стилі будуть враховувати лише ширину контейнера. Це аналогічно тому, як працюють медіазапити з min-width або max-width, але тут це застосовується до контейнера, а не до вікна браузера. Можна використовувати інше значення для цієї властивості — size: в такому разі буде враховуватись як ширина, так і висота контейнера (тобто адаптація стилів може залежати від обох вимірів). Це потрібно обирати залежно від потреб.

CSS-властивості

Формування фігури одним рядком

Ця властивість не є новою, вона була представлена у 2014 році. Але чомусь дуже рідко маю нагоду спостерігати, як нею користуються. Можливо, вона не всім відома не через те, що надто нова і ще не підтримується всіма браузерами, а навпаки — бо вважається застарілою.

Мова йде про властивість «clip-path». Я з нею познайомилась відносно давно, коли ще здобувала свій перший комерційний досвід. Тоді довелось реалізовувати доволі креативний дизайн лендінгу з багатьма декоративними елементами у вигляді геометричних фігур. Вони мали бути заанімовані й з часом змінювати одну форму на іншу.

/* Елемент для анімації */
.shape {
  width: 200px;
  height: 200px;
  background-color: #ff6f61;
  clip-path: polygon(50% 0, 50% 0, 100% 100%, 0 100%, 0 100%); /* Трикутник з зайвими точками */
  animation: morphing 2s ease-in-out infinite;
}

/* Анімація зміни форми */
@keyframes morphing {
  0% {
    clip-path: polygon(50% 0, 50% 0, 100% 100%, 0 100%, 0 100%); /* Трикутник */
  }
  50% {
    clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%, 0 100%); /* Чотирикутник */
  }
  100% {
    clip-path: polygon(50% 0, 50% 0, 100% 100%, 0 100%, 0 100%); /* Повернення до трикутника */
  }
}

Це найпростіший приклад анімування фігур, реалізованих через clip-path властивість. Анімації можна поєднувати з різними властивостями: transform, background, opacity і так далі. Також значення polygon дуже важко прописувати — саме усі відсотки у дужках. Для цього є онлайн платформи-генератори, де можна візуально побачити й підібрати необхідні параметри. Я користуюсь цією — bennettfeely.com/clippy.

Загалом я люблю використовувати clip-path, ця властивість часто рятує від сидіння і вирізання зображення певної форми у фотошопі (наприклад, зірочкою).

Найчастіше доводилось працювати з нею під час реалізації нестандартного дизайну. Проте цю властивість також можна побачити серед інших у наборі стилів селектора visuallyhidden, який широко використовується у вебдоступності.

Універсальна властивість

Ймовірно, кожен розробник, перш ніж почати писати стилі, хоча б раз додавав до свого проєкту файл reset.css для скидання всіх дефолтних стилів браузера.

Зазвичай скидаються margin, padding, border, задається box-sizing і так далі. Але це все можна прописати однією властивістю — all: unset. Властивість «all» теж не нова. Вона з’явилась, як і попередня — ще у 2014 році.

Властивість all: unset буде «скидати» наступні значення: розміри, стилі й товщину шрифтів (font-size, font-family, line-height, font-weight, font-style, text-transform), колір і стилізацію тексту (color, text-decoration, text-align), розміри елементів (width, height, margin, padding, border). Тобто файл reset.css може виглядати так:

html {
    all: unset;  /* Скидання всіх стандартних стилів */
    box-sizing: border-box;  /* Встановлення коректного підрахунку ширини та висоти */
}

Важливо враховувати, що ця властивість скидає фактично всі стилі. Включно зі стилями для елементів, які зазвичай не відображаються на екрані, таких як текст з тегу title, який знаходиться у вас в тегові head. Залежно від браузера може показуватись непотрібна інформація, яка зазвичай прихована. Тому потрібно обережно використовувати цю властивість, особливо коли ви захочете її застосувати до загального селектора (*). І обов’язково перевіряти коректність її роботи.

Адаптивні font-size значення через CSS-змінні

На мою думку, це доцільніше використовувати на великих проєктах, хоча і для односторінкового сайту такий підхід може полегшити стилізацію адаптивної версії. Щоб для кожного елементу або групи елементів не прописувати значення font-size, це можна зробити через застосування CSS-змінних у :root. А потім достатньо один раз присвоїти відповідне значення для елементу. Також варто зазначити, що цей підхід я писала, використовуючи синтаксис SCSS-препроцесора, щоб був доступний механізм для перебору масиву або циклу.

:root {
    @for $i from 6 through 70 {
      --text-font-size-#{$i}: #{$i}px;
    }
}

@media (min-width: 1400px) {
    :root {
        @for $i from 6 through 70 {
            --text-font-size-#{$i}: #{($i + 2)}px;
        }
    }
}

Для селектора :root потрібно визначити CSS-змінні, які представляють різні розміри шрифтів. Внутрішній цикл @for проходить через числа від 6 до 70 (можна використовувати будь-який необхідний діапазон розмірів шрифтів) і для кожного з цих чисел створює CSS-змінну. Назва цієї змінної формується шляхом вставлення значення цифри у форматі —text-font-size-#{$i}, а саме значення дорівнює розміру шрифту в пікселях (наприклад, —text-font-size-6 отримує значення 6px, а —text-font-size-70 — 70px).

Далі у коді є медіазапит, який активується при ширині вікна перегляду від 1400 пікселів і більше. Коли ця умова виконується, для тих самих змінних знову створюються значення, але тепер з додаванням двох пікселів до початкових розмірів. Це забезпечує адаптивність дизайну: на великих екранах розміри шрифтів стають більшими, що покращує читабельність тексту.
Саме такий медіазапит у 1400px і більше був необхідний для мого конкретного випадку. Але, звісно ж, можна використовувати будь-які брейкпойнти, які вимагає дизайн.

Потім по проєкту достатньо присвоювати необхідні змінні для селекторів один раз і вони будуть змінюватись від зміни ширини екрана, без прописування медіазапит для кожного з них:

p {
    font-size: var(--text-font-size-14);
}

Єдиний нюанс — такий загальний підхід має підходити для наданого вам дизайну, бо якщо ви у CSS-реалізації пропишете, що для 768px вказаний розмір шрифту буде зменшуватись від вказаного на два пікселі, задасте елементу font-size: var(—text-font-size-18). Відповідно для вказаного брейкпоінта він вирахується як 16px. А у вас в дизайні буде промальований 15px.

В такому разі все-таки доведеться перевизначати розмір шрифту окремо через медіазапит. Тому потрібно бути уважним і, якщо є можливість — узгодити це заздалегідь з дизайнером. Або якщо дизайнер, наприклад, буде використовувати бібліотечну схему (на кшталт Material scheme), це теж підійде. Бо у бібліотеках дизайну використовується схожий підхід.

Анімація градієнтів

Мабуть, багато хто стикався з проблемою реалізації плавної анімації при наведенні на елемент, який має градієнт. Тобто властивість transition не діє на властивість background зі значенням градієнта.

Насправді анімація градієнтів у CSS може бути досягнута без використання JavaScript. Хоча transition не працює на градієнтах безпосередньо, ми можемо анімувати їх, змінюючи background-position:

.gradient-button {
    padding: 20px 40px;
    border: none;
    border-radius: 7px;
    color: white;
    font-size: 30px;
    cursor: pointer;
    background: linear-gradient(90deg, #ff6e7f, #149de6);
    background-size: 200% 100%; /* Розширюємо розмір градієнта для анімації */
    transition: background-position 1s ease; /* Анімація позиції фону */
}
/* Стиль при наведенні */
.gradient-button:hover {
    background-position: 100% 0; /* Змінюємо позицію градієнта при наведенні */
}

Використовуючи такий підхід, я змогла досягти бажаної плавності анімації навіть, коли колір фону кнопки був реалізований за допомогою градієнта.

Висновки

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

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

Використання маловідомих властивостей та функцій CSS може значно спростити процес реалізації складних макетів і оптимізувати код. Однак не менш важливо звертати увагу на підтримку властивостей у різних браузерах. Це допоможе уникнути непередбачуваних проблем з відображенням на різних платформах, забезпечуючи тим самим кросбраузерну сумісність.
Отже, активно досліджуючи нові можливості CSS та адаптуючи їх до своїх потреб, ви зможете підвищити якість свого коду та створити вебдизайни, які вразять користувачів.

👍ПодобаєтьсяСподобалось12
До обраногоВ обраному4
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Ймовірно, кожен розробник, перш ніж почати писати стилі, хоча б раз додавав до свого проєкту файл reset.css для скидання всіх дефолтних стилів браузера.

Цей треш тягнеться з часів IE6, коли кожен браузер мав власне розуміння того, як «правильно». З тих пір дефолтні стилі стандартизували та потреби в ресеті немає.

В мене колись була комічна ситуація на одній із співбесід. На запитання «як ви робите ресет?» я сказав, що не роблю його взагалі. Мене здивовано запитали, а як я тоді розробку веду? А я запитав у відповідь, а розкажіть мені, навіщо вам перевизначати стилі, щоб потім їх назад повертати? Просто втановіть нові й все! Інтервʼювери не знайшлися що відповісти...

Дякую за статтю.

Десь лінк загубився

Для цього є онлайн платформи-генератори, де можна візуально побачити й підібрати необхідні параметри. Я користуюсь цією.

І не зовсім зрозумів речення в розділі про unset

Включно зі стилями для елементів, які зазвичай не відображаються на екрані, таких як текст з , який знаходиться у вас в .

Відредагувала і додала посилання. Ще продублюю ось тут — bennettfeely.com/clippy
Стосовно речення про unset також внесла виправлення до статті і замінила знаки гегів «<>» на слово «тег», бо інакше не хотіло показувати слова і речення було незрозумілим.
Дуже дякую за увагу до статті і за коментарі. Якщо будуть ще якісь запитання — з радістю відповім.

Для font-size використовую приблизно такий підхід, нормально працює font-size: clamp($text-22, 1rem + 3vw, $text-36);

Гарна стаття — техніки, приклади та оформлення взагалі.
Єдине, з чим би я трохи посперечався — це «Адаптивні font-size значення через CSS-змінні».
Імхо, це трохи переускладнене рішення. Як на мене, класичний The 62.5% Font Size Trick працює плюс мінус схоже, але набагато простіше.

Супер, хороший підхід і справді набагато простіший.
Дуже дякую.

Підписатись на коментарі