4 способа, которыми CSS :has() может сделать ваши HTML-формы еще лучше

4 способа, которыми CSS :has() может сделать ваши HTML-формы еще лучше

24 декабря 2022 г.

https://www.youtube.com/watch?v=oVuVa9I-B04?embedable =правда

В последнее время было много шума вокруг CSS :has() псевдокласс. И правильно! По сути, это «родительский селектор», о котором мы просили годами. Сегодня я хочу сосредоточиться на том, как мы можем использовать :has() для создания HTML. формы еще лучше.

Предисловие

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

<div class="control">
    <label for="name">Name</label>
    <div class="control__input">
        <input type="text" id="name" name="name" required>
    </div>
</div>

<div>, обертывающий весь элемент управления, <label> для сохранения ввода accessible, обертка <div> вокруг элемента ввода и элемент <input> сам. Элемент ввода не будет иметь границы или фона. Элемент <div> «control__input» будет иметь стиль, который будет служить визуальным представлением элемента управления формы.

.control :where(input, select, textarea) {
  border: 0;
  background-color: transparent;
}
.control__input {
  display: flex;
  align-items: center;
  gap: .125rem;
  border: .125rem solid;
  border-radius: .25rem;
  padding: .25rem;
}

An input component with a black border and the label "Name" above it.

Граница находится вокруг div, а внутри где-то есть небольшой ввод. Поверь мне.

Этот подход позволяет нам добавлять небольшие украшения дизайна, такие как символ «at», когда тип ввода — «электронная почта».

An input component with a black border and the label "Email" above it. The input has an @ symbol decorating the inside of it.

Наличие элементов в DOM упрощает применять к ним стили или анимацию. Это также будет полезно для примеров в этом посте.

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

CSS предоставляет множество хороших псевдоклассов для стилизации элементов. Один из них, особенно полезный для ввода формы, — это :focus-visible, который позволяет нам применять стили к элементам, когда они сфокусированы с помощью навигации с помощью клавиатуры.

Когда наш ввод получает фокус, было бы лучше применить стили фокуса к оболочке. Мы можем сделать это с помощью :has():

.control__input:has(:focus-visible) {
  outline: 3px solid plum;
}

An input component with a black border and the label "Name" above it. The input has a purple outline.

Отзыв о встроенной проверке

Что, если бы мы хотели предоставить пользователю обратную связь, является ли ввод правильным или недействительным? Мы можем сделать это с помощью псевдоклассов :valid и :invalid.

Давайте добавим пару SVG-файлов внутрь нашей оболочки ввода, чтобы обеспечить визуальную обратную связь. Мы установим для них значение display:none по умолчанию, чтобы вы их не видели.

<div class="control">
    <label for="name">Name</label>
    <div class="control__input">
        <input type="text" id="name" name="name" required>
        <svg class="icon icon-check" role="presentation">
            <use href="#icon-check"></use>
        </svg>
        <svg class="icon icon-cancel" role="presentation">
            <use href="#icon-cancel"></use>
        </svg>
    </div>
</div>

Используя псевдокласс :has(), мы можем отобразить соответствующий SVG, а также добавить цвет всему элементу управления формой; зеленый для действительного отзыва и красный для недействительного отзыва.

Чтобы избежать одновременного предоставления слишком большого количества отзывов, мы также будем использовать псевдоклассы :focus, чтобы применять стили только к тому вводу, который в данный момент находится в фокусе.

.control__input is(.icon-cancel, .icon-check) {
  display: none;
}
.control:has(:focus:invalid) .icon-cancel,
.control:has(:focus:valid) .icon-check {
  display: unset
}
.control:has(:focus:invalid) {
  color: tomato;
}
.control:has(:focus:valid) {
  color: limegreen;
}
.control:has(:focus-visible:invalid) .control__input {
  outline-color: pink;
}
.control:has(:focus-visible:valid) .control__input {
  outline-color: palegreen;
}

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

An input component with a red border and the label "Name" above it. The input has a "cancel" icon inside it. The text, icon, and outline is red. An input component with a green border and the label "Name" above it. The input has a "check" icon inside it. The text, icon, and outline is green.

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

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

.control option {
  color: initial;
}

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

Джей Томпкинс еще больше развил эту концепцию, используя скрытые заполнители и тег :placeholder-show псевдокласс, чтобы добавлять обратную связь только после того, как пользователь взаимодействует с вводом. Посмотрите его потрясающую демонстрацию здесь: https://codepen.io/jh3y/pen/yLKMOBm

Мне нравится эта концепция, но прием :placeholder-show всегда казался мне немного хакерским, поскольку он убирает функциональность заполнителей. В будущем у нас должен быть :user-invalid, который предназначен именно для этого варианта использования. В настоящее время он поддерживается только в Firefox.

Параметры ввода карточек

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

A form control with the label, "What's your fave frontend language". There are three options in the shape of card elements with an icon, a title, and a description. The first option is, "HTML; The bones of any good website". The second is, "CSS; Styles to make your mamma proud". The third is, "JavaScript; Fancy-pantsy movements and stuff"

К сожалению, в Интернете для нас нет опции <input type="card">. Но мы разработчики, поэтому мы создадим его, используя то, что доступно.

Поскольку в пользовательском интерфейсе представлен вопрос, на который может быть только один ответ, лучшим инструментом для этой работы являются три элемента <input type="radio">. Мы будем использовать <fieldset> для семантической группировки входных данных. Для входных данных требуется <label>. Может показаться заманчивым обернуть всю карточку элементом label, чтобы пользователи могли щелкнуть карточку и выбрать ввод, но тогда все содержимое карточки будет прочитано средствами чтения с экрана, а я бы хотел этого избежать. Вместо этого имеет смысл использовать название языка в качестве метки. Позже мы разберемся, как сделать кликабельной всю карточку. Наконец, поскольку в пользовательском интерфейсе уже есть небольшое описание, мы могли бы также связать его с вводом с помощью атрибута aria-describedby.

<fieldset>
  <legend>What's your fave frontend language</legend>
  <div class="cards">
    <div class="card card--html">
      <img src="/img/logo-html.svg" alt="HTML logo" width="48" height="48">
      <label for="html">HTML</label>
      <input id="html" type="radio" name="fe-fave" aria-describedby="html-description" class="visually-hidden">
      <p id="html-description">The bones of any good website</p>
    </div>

    <! – CSS card markup – >

    <! – JavaScript card markup – >
  </div>
</fieldset>

Судя по дизайну, вы можете заметить очевидное отсутствие радиовходов. Первое, что мы хотим сделать со стилями, это убедиться, что наши переключатели не видны, но все еще доступны. Мы не можем использовать display:none, потому что это удалит его из документа. Тогда он не будет кликабельным или доступным с клавиатуры. Вместо этого мы будем использовать общий шаблон с классом называется «зрительно-скрытым». Это выглядит так:

.visually-hidden {
  border: 0;
  clip: rect(0 0 0 0);
  height: auto;
  margin: 0;
  overflow: hidden;
  padding: 0;
  position: absolute !important;
  width: 1px;
  white-space: nowrap;
}

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

Мы можем добавить :after псевдоэлемент на метку и расположите его так, чтобы он закрывал всю карточку. Это приводит к тому, что <label> ведет себя так, как будто он покрывает всю карточку. Пользователь может щелкнуть в любом месте карты, что приведет к попаданию в элемент метки, который активирует связанный ввод.

.card {
  position: relative;
}
.card label:after {
  content: '';
  position: absolute;
  inset: 0;
}
.card a {
  position: relative;
}

Другие интерактивные элементы на карточке должны иметь position:relative, чтобы по ним можно было щелкнуть.

Хорошо, о пользователях мыши позаботились, давайте также учтем пользователей клавиатуры.

Мы хорошо поработали, сделав элементы ввода доступными для клавиатуры, но пользователи не увидят их контуры фокуса, поскольку они визуально скрыты.

Вот здесь нам может помочь :has().

Мы можем настроить таргетинг на любую карточку, ввод которой имеет псевдокласс :focus-visible, и применить к ней контур.

.card:has(input:focus-visible) {
  outline: 3px solid plum;
}

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

A form control with the label, "What's your fave frontend language". There are three options in the shape of card elements with an icon, a title, and a description. The first option is, "HTML; The bones of any good website". The second is, "CSS; Styles to make your mamma proud". The third is, "JavaScript; Fancy-pantsy movements and stuff". There is a purple outline around the HTML card.

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

Если карточка содержит проверенный ввод, применить стили; в данном случае это box-shadow.

.card:has(:checked) {
  box-shadow: inset 0 0 0 .25em mediumpurple;
}

A form control with the label, "What's your fave frontend language". There are three options in the shape of card elements with an icon, a title, and a description. The first option is, "HTML; The bones of any good website". The second is, "CSS; Styles to make your mamma proud". The third is, "JavaScript; Fancy-pantsy movements and stuff". There is a purple, inset box-shadow on the HTML card.

(Мне нужно было поставить флажки, чтобы выбрать все три)

Возможно, этот пример меньше всего зависит от :has(), потому что без него не так сложно выполнить то же самое. Вам нужно будет переместить ввод перед элементом карты, а затем использовать родственный элемент CSS combinator для таргетинга на карточку (input:checked + .card { /* styles */ }).

Поэтому, хотя этот пример, вероятно, легче всего будет прожить без :has(), его наличие облегчает нашу жизнь за счет совместного размещения входных данных и метки в DOM.

Условный рендеринг контента

Следующая интересная вещь, которую я хочу продемонстрировать, — это отображение и скрытие различных частей модели DOM с помощью :has().

Рассмотрите этот пользовательский интерфейс:

A fieldset form control with the label, "Favorite Starter Pokemon" and the radio options, "Bulbasaur", "Charmander", and "Squirtle"

Еще раз у нас есть вопрос с несколькими вариантами, но можно выбрать только один. Итак, мы снова будем использовать <fieldset> с некоторыми радио-вводами.

<fieldset>
  <legend>Favorite Starter Pokemon</legend>
  <div>
    <input id="bulbasaur" type="radio" name="poke" value="bulbasaur" />
    <label for="bulbasaur">Bulbasaur</label>
  </div>
  <! – charmander form control – >
  <! – bulbasaur form control – >
</fieldset>

Пока ничего особенного. Но все становится интереснее, когда мы выбираем один из вариантов. Мы можем раскрывать контент на основе того, какой выбор был сделан.

A fieldset form control with the label, "Favorite Starter Pokemon" and the radio options, "Bulbasaur", "Charmander", and "Squirtle". The "Bulbasaur" input is selected. Below is a picture of Bulbasaur along with the text, "Bulbasaur can be seen napping in bright sunlight. There is a seed on its back. By soaking up the sun's rays, the seed grows progressively larger. Height: 2ft 04in Weight: 15.2 lbs Type: Grass/Poison Weaknesses: Fire, Psychic, Flying, Ice"

Допустим, где-то еще в форме у нас есть элементы, которые мы хотим раскрыть.

<form>
  <div class="pokemon pokemon--bulbasaur">
    <img src="img/bulbasaur.png" width="300" height="300" alt="Bulbasaur" />
    <p>Bulbasaur can be seen napping in bright sunlight. There is a seed on its back. By soaking up the sun's rays, the seed grows progressively larger.</p>
    <ul>
      <li>Height: 2' 04"</li>
      <li>Weight: 15.2 lbs</li>
      <li>Type: Grass/Poison</li>
      <li>Weaknesses: Fire, Psychic, Flying, Ice</li>
    </ul>
  </div>
  <! – charmander details – >
  <! – bulbasaur details – >
</form>

Мы можем скрыть эти элементы по умолчанию, а затем использовать :has(), чтобы найти конкретный вход, который был проверен, и показать соответствующий элемент где-то еще в форме.

.pokemon {
  display: none;
}
form:has(#bulbasaur:checked) .pokemon--bulbasaur,
form:has(#charmander:checked) .pokemon--charmander,
form:has(#squirtle:checked) .pokemon--squirtle {
  display: block;
}

Когда я выбираю Бульбазавра, я вижу детали Бульбазавра. Когда я выбираю Чармандера, я вижу детали Чармандера. И когда я выбираю Сквиртла, я вижу сведения о Сквиртле.

(Кстати, Бульбазавр объективно является лучшим стартером: только один с несколькими типами. Лучший тип против первых лидеров спортзалов. И в игре есть более сильные типы огня и воды, но Венозавр — самый сильный тип растений, поэтому вы можете построить лучшую команду позже)

Этот шаблон также может работать с <select> или флажками. Например, если вы хотите сделать форму заказа пиццы, вы можете предложить начинку в качестве входных данных флажка. Затем в области просмотра заказа вы можете перечислить выбранные начинки. Довольно круто!

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

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

Стоит также отметить, что всякий раз, когда вы показываете или скрываете контент, вам следует подумать о доступности. В этом сценарии содержимое идет сразу после интерактивного элемента (управление формой). Это позволяет пользователям вспомогательных технологий легко обнаружить, что изменилось. Но если контент находится перед элементом управления или далеко от него, может потребоваться JavaScript для улучшения взаимодействия с помощью таких инструментов, как aria-expanded или aria-controls.. р>

В любом случае, моей целью было показать вам, что возможно. Это здорово!

Подсказки по проверке уровня

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

An unchecked checkbox and a button with the text "Submit". The checkbox is labeled, "I learned something cool!" The button is grey and slightly opaque. A checked checkbox and a button with the text "Submit". The checkbox is labeled, "I learned something cool!" The button is purple with white text.

Это можно сделать с помощью :has()

<form>
  <div class="control">
    <input id="checkme" type="checkbox" name="learned" required />
    <label for="checkme">I learned something cool!</label>
  </div>

  <button type="submit">Submit</button>
</form>

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

Мы можем проверить, есть ли в форме какие-либо недопустимые данные, и соответствующим образом применить стили к кнопке отправки.

form:has(:invalid) :where(button:not([type]), button[type="submit"]) {
  opacity: 0.7;
  color: black;
  background: whitesmoke;
  cursor: not-allowed;
}

Мы нацеливаемся только на кнопки, у которых отсутствует атрибут type или чей атрибут type имеет значение «отправить». Таким образом мы случайно не применяем стили к <button type="button">.

На самом деле это не помешает отправить форму, и это хорошо для проверки и доступности. Вместо этого мы добавляем несколько дополнительных подсказок, чтобы сообщить визуальным пользователям: «Эй, вы можете просмотреть форму еще раз». CSS отлично подходит для многих вещей. Фактическая проверка не входит в их число (пока).

В любом случае, когда у меня есть действующая форма, я могу показать, что кнопка готова к работе. Если у меня недействительная форма, я могу показать, что кнопка не готова.

Я думаю, это очень круто.

Заключительные мысли

В дополнение ко всем другим интересным местам для использования :has(), формы предлагают некоторые из моих любимых вариантов использования. Многие вещи, которые раньше требовали JavaScript, теперь можно сделать с помощью только CSS.

К сожалению, :has() не имеет поддержки браузера< /strong> нам нужно сегодня, чтобы он был готов к работе.

К счастью, во многих приведенных выше примерах не обязательно использовать :has(). Используя разные комбинаторы разметки и одноуровневых комбинаторов, вы можете добиться того же или чего-то близкого. Эти методы немного сложнее поддерживать, но мы недалеко ушли от более простого способа с помощью :has().

Я очень рад этому.

Если у вас есть другие идеи или интересные способы использования :has(), особенно если это в формах, но не исключительно, дайте мне знать. строят с ним.

Большое спасибо за прочтение. Если вам понравилась эта статья, поделитесь ею. Это один из лучших способов поддержать меня. Вы также можете подписаться на мою рассылку или подпишитесь на меня в Твиттере, если хотите знать, когда публикуются новые статьи.


:::информация Первоначально опубликовано здесь.

:::


Оригинал