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

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

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

Демо

Если вы предпочитаете видео или хотите увидеть предварительный UI/UX-обзор того, что мы создаем, вот более короткий обзор на YouTube:

Обзор

Я разбил аспекты этого компонента на следующие разделы:

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

Макеты

Это первая демонстрация GUI Challenge, полностью основанная на CSS Grid ! Вот каждая сетка, выделенная с помощью Chrome DevTools for Grid :

Красочные контуры и наложения с зазорами помогают показать все поля, из которых состоит макет настроек.

Просто для перерыва

Наиболее распространённая схема:

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 altitude, благодаря которой дочерние элементы выравниваются по центру по вертикали и горизонтали как в одноколоночной, так и в двухколоночной компоновке.

Посмотрите на видео выше, как «контент» остается по центру, даже несмотря на то, что произошло обтекание.

Повторить автоподбор 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() . Уна Кравец написала отличный пост в блоге о макетах , посвящённый этой функции, под названием RAM .

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

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

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

max-width: 89vw заслуживает небольшого анализа в этом посте. Позвольте мне показать вам макет с применённым стилем и без него:

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

Итак, почему 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 — это ориентированный на человека синтаксис, учитывающий то, как мы воспринимаем цвет, а не то, как мы измеряем его математически (например, 255). Это даёт ему неоспоримое преимущество: людям проще записывать его, и другие люди будут учитывать эти изменения.

Скриншот веб-страницы подкаста pod.link/css с открытым эпизодом 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 и реализации браузеров.

Леа Веру

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

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

Выше показано действие свойства из панели «Стили» в DevTools. В демо используется HTML-тег, который, на мой взгляд, в целом более удачное расположение:

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

Узнайте всё об этом в этой статье Томаса Штайнера color-scheme . Здесь есть гораздо больше преимуществ, чем просто тёмные флажки!

CSS 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. Вложенный clip-path <picture> расширяется до полного круга, а фон заполняется ярким фиксированным градиентом.

Индивидуальный диапазон

Учитывая следующий элемент ввода HTML, я покажу вам, как я настроил его внешний вид:

<input type="range">

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

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

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

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, но они требуют, чтобы элемент thumb был той же высоты, что и track, и мне не удалось найти гармонию в этих рамках.

/* 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"> . Для флажка удвойте имя и идентификатор, чтобы его можно было найти с помощью различных инструментов и технологий, таких как мышь или программа чтения с экрана: <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">

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

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 с их стилем в отношении области наведения курсора для надписей флажков! В этой версии нет зазора между элементами: демо и исходный код .