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

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

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

Демо

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

Обзор

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

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

Макеты

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

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

Просто ради пробела

Самая распространенная планировка:

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

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

Повторить автоподбор минмакса

<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 использует 3 функции 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-сетки необходимо указать определенный размер или максимальный размер. Я указал максимальный размер.

Итак, почему 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;
}

Очень удобно использовать эти потоки с помощью Grid, 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/csspodcast с эпизодом «Цвет 2: Восприятие».
Узнайте о перцептивном цвете (и многом другом!) в подкасте CSS.

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

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

accent-color CSS

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

/* 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 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(), где данные формы показаны в таблице.

Заключение

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

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

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

  • @tomayac с их стилем области наведения меток флажков! В этой версии нет пробела при наведении курсора между элементами: demo и source .