Создание компонента настроек

Базовый обзор того, как создать компонент настроек из ползунков и флажков.

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

Демоверсия

Если вы предпочитаете видео или хотите посмотреть, как будет выглядеть интерфейс, посмотрите короткий ролик на YouTube:

Обзор

Ниже приведены разделы, посвященные различным аспектам этого компонента:

  1. Макеты
  2. Цвет
  3. Введите свой диапазон
  4. Специальный флажок
  5. Специальные возможности
  6. JavaScript

Макеты

Это первая демонстрация GUI Challenge, в которой используется только CSS Grid. Вот как выглядит каждая сетка, выделенная с помощью инструментов разработчика Chrome:

Цветные контуры и наложения пробелов, которые помогают показать все поля, составляющие макет настроек.

Только для разрыва

Самый распространенный макет:

foo {
  display: grid;
  gap: var(--something);
}

Я называю этот макет "просто для пробела", потому что он использует сетку только для добавления пробелов между блоками.

Эту стратегию используют пять макетов. Вот они:

Вертикальные макеты сетки, выделенные контурами и заполненными пробелами

Элемент fieldset, который содержит каждую группу ввода (.fieldset-item), использует gap: 1px для создания тонких границ между элементами. Никаких сложных решений!

Заполненный пробел
.grid {
  display: grid;
  gap: 1px;
  background: var(--bg-surface-1);

  & > .fieldset-item {
    background: var(--bg-surface-2);
  }
}
Трюк с границей
.grid {
  display: grid;

  & > .fieldset-item {
    background: var(--bg-surface-2);

    &:not(:last-child) {
      border-bottom: 1px solid var(--bg-surface-1);
    }
  }
}

Естественный перенос сетки

Самым сложным оказался макромакет – логическая система макетов для экранов шириной от <main> до <form>.

Центрирование контента с переносом

Flexbox и Grid позволяют align-items или align-content, а при работе с элементами переноса выравнивание макета content распределяет пространство между дочерними элементами как группой.

main {
  display: grid;
  gap: var(--space-xl);
  place-content: center;
}

Для основного элемента используется place-content: center сокращенное свойство выравнивания, чтобы дочерние элементы были выровнены по центру по вертикали и горизонтали в макетах с одним и двумя столбцами.

Посмотрите в видео выше, как контент остается по центру, даже если происходит перенос.

Повторить автоподбор minmax

В <form> используется адаптивный макет сетки для каждого раздела. Этот макет переключается с одного на два столбца в зависимости от доступного пространства.

form {
  display: grid;
  gap: var(--space-xl) var(--space-xxl);
  grid-template-columns: repeat(auto-fit, minmax(min(10ch, 100%), 35ch));
  align-items: flex-start;
  max-width: 89vw;
}

В этой сетке значение row-gap (--space-xl) отличается от column-gap (--space-xxl), чтобы придать адаптивному макету индивидуальность. Когда столбцы расположены друг под другом, нам нужен большой зазор, но не такой большой, как на широком экране.

Свойство grid-template-columns использует три функции CSS: repeat(), minmax() и min(). Уна Кравец (Una Kravets) написала отличную статью об этом, назвав такой подход RAM.

В нашем макете есть три специальных дополнения, если сравнить его с макетом Уны:

  • Мы передаем дополнительную функцию min().
  • Мы указываем align-items: flex-start.
  • Доступен стиль "max-width: 89vw".

Дополнительная функция min() хорошо описана Эваном Минто в его блоге в статье Intrinsically Responsive CSS Grid with minmax() and min(). Рекомендую ее прочитать. Коррекция выравнивания flex-start убирает эффект растягивания по умолчанию, поэтому дочерним элементам этого макета не нужно иметь одинаковую высоту, они могут иметь естественную, внутреннюю высоту. В видео на YouTube кратко рассказывается об этом дополнении.

В этой статье мы подробно рассмотрим max-width: 89vw. Вот как выглядит макет с примененным стилем и без него:

Что случилось? Когда задано свойство max-width, оно предоставляет контекст, явный или определенный размер, чтобы алгоритм макета auto-fit знал, сколько повторений может поместиться в пространстве. Хотя кажется очевидным, что пространство имеет "полную ширину", согласно спецификации сетки CSS, должен быть указан определенный размер или максимальный размер. Я указал максимальный размер.

Почему именно 89vw? Потому что это "сработало" для моего макета. Мы с коллегами из команды Chrome пытаемся понять, почему более разумного значения, например 100vw, недостаточно и не является ли это ошибкой.

Использование пробелов.

Большая часть гармонии этого макета обусловлена ограниченной палитрой интервалов, а именно 7.

:root {
  --space-xxs: .25rem;
  --space-xs:  .5rem;
  --space-sm:  1rem;
  --space-md:  1.5rem;
  --space-lg:  2rem;
  --space-xl:  3rem;
  --space-xxl: 6rem;
}

Использование этих потоков действительно хорошо сочетается с сеткой, CSS @nest и синтаксисом уровня 5 @media. Вот пример полностью <main> набора стилей макета.

main {
  display: grid;
  gap: var(--space-xl);
  place-content: center;
  padding: var(--space-sm);

  @media (width >= 540px) {
    & {
      padding: var(--space-lg);
    }
  }

  @media (width >= 800px) {
    & {
      padding: var(--space-xl);
    }
  }
}

Сетка с контентом по центру и умеренными полями по умолчанию (как на мобильных устройствах). Но по мере того, как становится доступно больше места в области просмотра, оно расширяется за счет увеличения отступов. CSS 2021 года выглядит довольно хорошо!

Помните макет, который мы рассматривали ранее, "только для зазора"? Вот более полная версия того, как они выглядят в этом компоненте:

header {
  display: grid;
  gap: var(--space-xxs);
}

section {
  display: grid;
  gap: var(--space-md);
}

Цвет

Контролируемое использование цвета помогло этому дизайну выделиться как выразительному, но минималистичному. Я делаю это так:

:root {
  --surface1: lch(10 0 0);
  --surface2: lch(15 0 0);
  --surface3: lch(20 0 0);
  --surface4: lch(25 0 0);

  --text1: lch(95 0 0);
  --text2: lch(75 0 0);
}

Я называю цвета фона и текста цифрами, а не словами, например surface-dark и surface-darker, потому что в запросе медиа я буду их менять, и слова "светлый" и "темный" не будут иметь значения.

Я переворачиваю их в медиа-запросе предпочтений следующим образом:

:root {
  ...

  @media (prefers-color-scheme: light) {
    & {
      --surface1: lch(90 0 0);
      --surface2: lch(100 0 0);
      --surface3: lch(98 0 0);
      --surface4: lch(85 0 0);

      --text1: lch(20 0 0);
      --text2: lch(40 0 0);
    }
  }
}

Прежде чем углубляться в детали синтаксиса цвета, важно быстро взглянуть на общую картину и стратегию. Но я немного забежал вперед. Давайте вернемся назад.

LCH?

Не вдаваясь в теорию цвета, LCH – это ориентированный на человека синтаксис, который учитывает то, как мы воспринимаем цвет, а не то, как мы измеряем цвет с помощью математики (например, 255). Это даёт ему явное преимущество, поскольку людям легче писать его, а другие люди будут в гармонии с этими корректировками.

Скриншот веб-страницы pod.link/csspodcast с открытым выпуском Color 2: Perception
Узнайте больше о перцептивном цвете и не только в подкасте о CSS.

Сегодня в этой демонстрации давайте сосредоточимся на синтаксисе и значениях, которые я переворачиваю, чтобы сделать светлым и темным. Рассмотрим один цвет поверхности и один цвет текста:

:root {
  --surface1: lch(10 0 0);
  --text1:    lch(95 0 0);

  @media (prefers-color-scheme: light) {
    & {
      --surface1: lch(90 0 0);
      --text1:    lch(40 0 0);
    }
  }
}

--surface1: lch(10 0 0) соответствует 10% светлоты, 0 насыщенности и 0 оттенка – очень темный бесцветный серый. Затем в медиазапросе для светлого режима яркость меняется на 90% с помощью --surface1: lch(90 0 0);. Вот и вся суть стратегии. Начните с изменения яркости между двумя темами, сохраняя коэффициенты контрастности, предусмотренные дизайном, или те, которые могут поддерживать доступность.

Преимущество lch() в том, что светлота ориентирована на человека, и мы можем быть уверены, что ее изменение на % будет восприниматься и ощущаться именно как изменение на %. Например, hsl() не так надежен.

Если вы хотите узнать больше о цветовых пространствах и lch(), прочитайте эту статью. Оно приближается!

Сейчас CSS не может получить доступ к этим цветам. Повторю: у нас нет доступа к трети цветов на большинстве современных мониторов. Причем это не просто цвета, а самые яркие цвета, которые может отображать экран. Наши сайты выглядят блеклыми, потому что аппаратное обеспечение мониторов развивалось быстрее, чем спецификации CSS и реализации браузеров.

Леа Веру (Lea Verou)

Адаптивные элементы управления формой с цветовой схемой

Многие браузеры, например Safari и Chromium, поддерживают управление тёмной темой, но вам нужно указать в CSS или HTML, что ваш дизайн использует их.

На изображении выше показан эффект свойства на панели "Стили" в Инструментах разработчика. В демо-версии используется тег HTML, который, на мой взгляд, в целом является лучшим местом:

<meta name="color-scheme" content="dark light">

Подробнее об этом читайте в color-schemeстатье Томаса Штайнера (Thomas Steiner). Вы можете получить гораздо больше, чем просто темные флажки!

ССЦ accent-color

Недавно было предложено использовать свойство accent-color для элементов форм. Это единый стиль CSS, который позволяет изменить оттенок цвета, используемый в элементе ввода браузера. Подробнее об этом можно прочитать на GitHub. Я включил его в свои стили для этого компонента. По мере того, как браузеры будут поддерживать это, мои флажки будут больше соответствовать теме с розовыми и фиолетовыми цветами.

input[type="checkbox"] {
  accent-color: var(--brand);
}

Скриншот Chromium в Linux с розовыми флажками

Всплывающие окна с фиксированными градиентами и фокусом внутри

Цвет лучше всего выделяется, когда он используется экономно, и один из способов, которым мне нравится этого достигать, – это красочные взаимодействия с пользовательским интерфейсом.

В видео выше показано несколько уровней обратной связи и взаимодействия с интерфейсом, которые помогают придать взаимодействию индивидуальность:

  • Выделение контекста.
  • Отображение в интерфейсе того, насколько значение близко к границам диапазона.
  • Оповещение о том, что поле принимает ввод.

Чтобы обеспечить обратную связь при взаимодействии с элементом, CSS использует псевдокласс :focus-within для изменения внешнего вида различных элементов. Давайте разберем .fieldset-item, это очень интересно:

.fieldset-item {
  ...

  &:focus-within {
    background: var(--surface2);

    & svg {
      fill: white;
    }

    & picture {
      clip-path: circle(50%);
      background: var(--brand-bg-gradient) fixed;
    }
  }
}

Когда один из дочерних элементов этого элемента имеет фокус:

  1. Фону .fieldset-item назначается более контрастный цвет поверхности.
  2. Вложенный элемент svg залит белым цветом для повышения контрастности.
  3. Вложенный элемент <picture> clip-path расширяется до полного круга, а фон заполняется ярким фиксированным градиентом.

Другой диапазон дат

Ниже показано, как я настроил внешний вид элемента ввода HTML.

<input type="range">

Этот элемент состоит из трех частей, которые нужно настроить:

  1. Элемент/контейнер диапазона
  2. Отслеживание
  3. Thumb

Стили элементов диапазона

input[type="range"] {
  /* style setting variables */
  --track-height: .5ex;
  --track-fill: 0%;
  --thumb-size: 3ex;
  --thumb-offset: -1.25ex;
  --thumb-highlight-size: 0px;

  appearance: none;         /* clear styles, make way for mine */
  display: block;
  inline-size: 100%;        /* fill container */
  margin: 1ex 0;            /* ensure thumb isn't colliding with sibling content */
  background: transparent;  /* bg is in the track */
  outline-offset: 5px;      /* focus styles have space */
}

Первые несколько строк CSS – это настраиваемые части стилей, и я надеюсь, что их четкая маркировка поможет. Остальные стили в основном являются стилями сброса, чтобы обеспечить согласованную основу для создания сложных частей компонента.

Отслеживайте стили

input[type="range"]::-webkit-slider-runnable-track {
  appearance: none; /* clear styles, make way for mine */
  block-size: var(--track-height);
  border-radius: 5ex;
  background:
    /* hard stop gradient:
        - half transparent (where colorful fill we be)
        - half dark track fill
        - 1st background image is on top
    */
    linear-gradient(
      to right,
      transparent var(--track-fill),
      var(--surface1) 0%
    ),
    /* colorful fill effect, behind track surface fill */
    var(--brand-bg-gradient) fixed;
}

Секрет в том, чтобы "раскрыть" яркий цвет заливки. Это делается с помощью градиента жесткой остановки сверху. Градиент прозрачен до процента заполнения, а после этого используется цвет незаполненной поверхности дорожки. За этой незаполненной поверхностью находится цвет во всю ширину, который ждет, когда прозрачность раскроет его.

Стиль заливки дорожки

Для сохранения стиля заполнения в моем дизайне требуется JavaScript. Есть стратегии, основанные только на CSS, но они требуют, чтобы элемент ползунка был той же высоты, что и дорожка, и я не смог найти гармонию в этих пределах.

/* grab sliders on page */
const sliders = document.querySelectorAll('input[type="range"]')

/* take a slider element, return a percentage string for use in CSS */
const rangeToPercent = slider => {
  const max = slider.getAttribute('max') || 10;
  const percent = slider.value / max * 100;

  return `${parseInt(percent)}%`;
};

/* on page load, set the fill amount */
sliders.forEach(slider => {
  slider.style.setProperty('--track-fill', rangeToPercent(slider));

  /* when a slider changes, update the fill prop */
  slider.addEventListener('input', e => {
    e.target.style.setProperty('--track-fill', rangeToPercent(e.target));
  })
})

Думаю, это хорошее визуальное улучшение. Ползунок отлично работает без JavaScript, свойство --track-fill не является обязательным, просто без него не будет стиля заливки. Если JavaScript доступен, заполните дополнительный параметр, отслеживая изменения, внесенные пользователем, и синхронизируя дополнительный параметр со значением.

Вот отличная статья на CSS-Tricks от Аны Тюдор, в которой показано решение для заполнения дорожки только с помощью CSS. Ещё меня очень вдохновил range элемент.

Стили значков

input[type="range"]::-webkit-slider-thumb {
  appearance: none; /* clear styles, make way for mine */
  cursor: ew-resize; /* cursor style to support drag direction */
  border: 3px solid var(--surface3);
  block-size: var(--thumb-size);
  inline-size: var(--thumb-size);
  margin-top: var(--thumb-offset);
  border-radius: 50%;
  background: var(--brand-bg-gradient) fixed;
}

Большинство этих стилей предназначены для создания красивого круга. Здесь вы снова видите фиксированный фоновый градиент, который объединяет динамические цвета ползунков, дорожек и связанных элементов SVG. Я разделил стили для взаимодействия, чтобы помочь изолировать технику box-shadow, используемую для выделения при наведении:

@custom-media --motionOK (prefers-reduced-motion: no-preference);

::-webkit-slider-thumb {
  …

  /* shadow spread is initally 0 */
  box-shadow: 0 0 0 var(--thumb-highlight-size) var(--thumb-highlight-color);

  /* if motion is OK, transition the box-shadow change */
  @media (--motionOK) {
    & {
      transition: box-shadow .1s ease;
    }
  }

  /* on hover/active state of parent, increase size prop */
  @nest input[type="range"]:is(:hover,:active) & {
    --thumb-highlight-size: 10px;
  }
}

Цель состояла в том, чтобы создать простую в управлении и анимированную визуальную подсветку для отзывов пользователей. Используя тень блока, я могу избежать запуска макета с эффектом. Для этого я создаю тень, которая не размыта и соответствует круглой форме элемента ползунка. Затем я меняю и переношу размер его распространения при наведении.

Если бы только эффект выделения был так же прост для флажков…

Селекторы для разных браузеров

Чтобы обеспечить совместимость с разными браузерами, мне понадобились селекторы -webkit- и -moz-:

input[type="range"] {
  &::-webkit-slider-runnable-track {}
  &::-moz-range-track {}
  &::-webkit-slider-thumb {}
  &::-moz-range-thumb {}
}

Пользовательский флажок

Ниже показано, как я настроил внешний вид элемента ввода HTML.

<input type="checkbox">

Этот элемент состоит из трех частей, которые нужно настроить:

  1. Элемент "Флажок"
  2. Связанные ярлыки
  3. Эффект выделения

Элемент с флажком

input[type="checkbox"] {
  inline-size: var(--space-sm);   /* increase width */
  block-size: var(--space-sm);    /* increase height */
  outline-offset: 5px;            /* focus style enhancement */
  accent-color: var(--brand);     /* tint the input */
  position: relative;             /* prepare for an absolute pseudo element */
  transform-style: preserve-3d;   /* create a 3d z-space stacking context */
  margin: 0;
  cursor: pointer;
}

Стили transform-style и position подготавливают псевдоэлемент, который мы добавим позже, чтобы стилизовать выделение. В остальном это в основном незначительные стилистические вещи от меня. Мне нравится, когда курсор выглядит как указатель, а также когда есть смещение контура. Стандартные флажки слишком маленькие, и если accent-color поддерживается, то я хочу, чтобы флажки были оформлены в фирменных цветах.

Ярлыки флажков

Важно указывать ярлыки для флажков по двум причинам. Первый – это представление того, для чего используется значение флажка, чтобы ответить на вопрос "включено или выключено для чего?" Во-вторых, с точки зрения UX, веб-пользователи привыкли взаимодействовать с флажками через связанные с ними метки.

ввод
<input
  type="checkbox"
  id="text-notifications"
  name="text-notifications"
>
ярлык
<label for="text-notifications">
  <h3>Text Messages</h3>
  <small>Get notified about all text messages sent to your device</small>
</label>

Добавьте в ярлык атрибут for, указывающий на флажок по идентификатору: <label for="text-notifications">. Для флажка укажите одинаковые значения в атрибутах name и id, чтобы его можно было найти с помощью различных инструментов и технологий, например мыши или программы чтения с экрана: <input type="checkbox" id="text-notifications" name="text-notifications">. :hover, :active и другие функции доступны бесплатно. Они расширяют возможности взаимодействия с формой.

Выделение флажка

Я хочу, чтобы интерфейсы были единообразными, а у элемента ползунка есть хорошая подсветка миниатюры, которую я хочу использовать с флажком. Значок смог использовать box-shadow и его свойство spread, чтобы масштабировать тень вверх и вниз. Однако этот эффект здесь не работает, потому что наши флажки квадратные, и так и должно быть.

Мне удалось добиться того же визуального эффекта с помощью псевдоэлемента и большого количества сложного CSS:

@custom-media --motionOK (prefers-reduced-motion: no-preference);

input[type="checkbox"]::before {
  --thumb-scale: .01;                        /* initial scale of highlight */
  --thumb-highlight-size: var(--space-xl);

  content: "";
  inline-size: var(--thumb-highlight-size);
  block-size: var(--thumb-highlight-size);
  clip-path: circle(50%);                     /* circle shape */
  position: absolute;                         /* this is why position relative on parent */
  top: 50%;                                   /* pop and plop technique (https://web.dev/centering-in-css#5-pop-and-plop) */
  left: 50%;
  background: var(--thumb-highlight-color);
  transform-origin: center center;            /* goal is a centered scaling circle */
  transform:                                  /* order here matters!! */
    translateX(-50%)                          /* counter balances left: 50% */
    translateY(-50%)                          /* counter balances top: 50% */
    translateZ(-1px)                          /* PUTS IT BEHIND THE CHECKBOX */
    scale(var(--thumb-scale))                 /* value we toggle for animation */
  ;
  will-change: transform;

  @media (--motionOK) {                       /* transition only if motion is OK */
    & {
      transition: transform .2s ease;
    }
  }
}

/* on hover, set scale custom property to "in" state */
input[type="checkbox"]:hover::before {
  --thumb-scale: 1;
}

Создать псевдоэлемент в виде круга несложно, но разместить его за элементом, к которому он прикреплен, было сложнее. Вот как это выглядело до и после того, как я это исправил:

Это определенно микро-взаимодействие, но для меня важно сохранить визуальную согласованность. Техника масштабирования анимации такая же, как и в других местах. Мы задаем новое значение для пользовательского свойства и позволяем CSS переходить к нему на основе настроек движения. Ключевая функция – translateZ(-1px). Родительский элемент создал трехмерное пространство, а дочерний псевдоэлемент воспользовался этим, немного сместившись назад по оси Z.

Специальные возможности

В видео на YouTube отлично показано, как взаимодействовать с этим компонентом настроек с помощью мыши, клавиатуры и программы чтения с экрана. Я выделю некоторые детали здесь.

Варианты элементов HTML

<form>
<header>
<fieldset>
<picture>
<label>
<input>

Каждый из них содержит подсказки и советы для браузера пользователя. Некоторые элементы предоставляют подсказки по взаимодействию, некоторые подключают интерактивность, а некоторые помогают формировать дерево доступности, по которому перемещается программа чтения с экрана.

HTML-атрибуты

Мы можем скрыть элементы, которые не нужны программам чтения с экрана, в данном случае значок рядом с ползунком:

<picture aria-hidden="true">

В видео выше показано, как работает программа чтения с экрана в macOS. Обратите внимание, как фокус ввода перемещается прямо от одного ползунка к другому. Это связано с тем, что мы скрыли значок, который мог быть остановкой на пути к следующему ползунку. Без этого атрибута пользователю придется остановиться, прослушать описание и перейти к следующему элементу.

SVG – это набор математических формул. Давайте добавим элемент <title>, чтобы при наведении указателя мыши появлялась подсказка с названием, и понятный комментарий о том, что создают эти формулы.

<svg viewBox="0 0 24 24">
  <title>A note icon</title>
  <path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/>
</svg>

Кроме того, мы использовали достаточное количество четко размеченного HTML-кода, чтобы форма хорошо работала с мышью, клавиатурой, контроллерами видеоигр и программами чтения с экрана.

JavaScript

Я уже рассказывал, как цвет заливки дорожки управляется из JavaScript, поэтому давайте теперь посмотрим на JavaScript, связанный с <form>:

const form = document.querySelector('form');

form.addEventListener('input', event => {
  const formData = Object.fromEntries(new FormData(form));
  console.table(formData);
})

Каждый раз, когда пользователь взаимодействует с формой и изменяет ее, консоль регистрирует форму как объект в таблице, чтобы ее было легко проверить перед отправкой на сервер.

Скриншот результатов console.table(), где данные формы показаны в таблице

Заключение

Теперь, когда вы знаете, как это сделала я, как бы вы это сделали?! Это делает архитектуру компонентов более интересной! Кто первым создаст версию со слотами в своем любимом фреймворке? 🙂

Давайте разнообразим наши подходы и изучим все способы создания веб-сайтов. Создайте демоверсию, отправьте мне ссылку в Твиттере, и я добавлю ее в раздел Ремиксы сообщества ниже.

Ремиксы сообщества

  • @tomayac за его стиль, касающийся области наведения для меток флажков! В этой версии нет зазора между элементами при наведении указателя: демо и исходный код.