构建设置组件

关于如何构建滑块和复选框设置组件的基础概述。

在这篇文章中,我想分享关于为 Web 构建响应式、支持多种设备输入并在各种浏览器中运行的“设置”组件的思考过程。试试演示

演示

如果您喜欢视频,或者想要预览我们正在构建的 UI/UX,请观看 YouTube 上的简短演练

概述

我已将此组件的各个方面分解为以下部分

  1. 布局
  2. 颜色
  3. 自定义范围输入
  4. 自定义复选框输入
  5. 无障碍功能注意事项
  6. JavaScript

布局

这是第一个完全使用 CSS Grid 的 GUI 挑战演示!以下是使用 Chrome DevTools for grid 突出显示的每个网格

Colorful outlines and gap spacing overlays that help show all the boxes that make up the settings layout

仅用于间距

最常见的布局

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

我将此布局称为“仅用于间距”,因为它仅使用网格在块之间添加间距。

五个布局使用此策略,以下显示了所有布局

Vertical grid layouts highlighted with outlines and filled in gaps

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 和网格都提供 align-itemsalign-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 属性使用 3 个 CSS 函数:repeat()minmax()min()Una Kravets 有一篇关于此的出色的布局博客文章,称之为 RAM

与 Una 的布局相比,我们的布局中有 3 个特殊添加项

  • 我们传递了一个额外的 min() 函数。
  • 我们指定了 align-items: flex-start
  • 有一个 max-width: 89vw 样式。

Evan Minto 在其博客文章 使用 minmax() 和 min() 的固有响应式 CSS 网格 中很好地描述了额外的 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@media 级别 5 语法非常契合。这是一个示例,完整的 <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);
    }
  }
}

一个具有居中内容的网格,默认情况下适度填充(例如在移动设备上)。但是,随着更多视口空间可用,它会通过增加填充来展开。 2021 年的 CSS 看起来相当不错!

还记得早期的布局“仅用于间距”吗?以下是它们在此组件中的外观的更完整版本

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-darksurface-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)测量颜色的方式。这使其具有明显的优势,因为人类可以更轻松地编写它,而其他人将与这些调整保持一致。

A screenshot of pod.link/csspodcast webpage, with Color 2: Perception episode pulled up
CSS Podcast 上了解感知颜色(以及更多内容!)

今天,在此演示中,让我们关注语法以及我正在翻转以制作浅色和深色的值。让我们看一下 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);。这就是该策略的要点。首先,只需更改 2 个主题之间的亮度,保持设计要求的对比度或可以保持无障碍功能的对比度。

此处使用 lch() 的好处是亮度是以人为本的,我们可以对它的 % 变化感觉良好,它在感知上和一致性上将是 % 的差异。例如,hsl() 不太可靠

如果您有兴趣,可以了解更多关于颜色空间和 lch() 的信息。它即将到来!

CSS 现在根本无法访问这些颜色。让我重复一遍:我们无法访问大多数现代显示器中三分之一的颜色。 这些不仅仅是任何颜色,而是屏幕可以显示的最鲜艳的颜色。我们的网站被冲刷掉了,因为显示器硬件的发展速度快于 CSS 规范和浏览器实现。

Lea Verou

使用 color-scheme 的自适应表单控件

许多浏览器都附带了深色主题控件,目前是 Safari 和 Chromium,但您必须在 CSS 或 HTML 中指定您的设计使用它们。

上面演示了 DevTools 的“样式”面板中属性的效果。演示使用 HTML 标记,我认为这通常是更好的位置

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

Thomas Steiner 的这篇 color-scheme 文章 中了解所有相关信息。除了深色复选框输入之外,还有更多收获!

CSS accent-color

关于表单元素上的 accent-color,最近有最新动态,它是一种 CSS 样式,可以更改浏览器输入元素中使用的色调颜色。在此处 GitHub 上阅读更多相关信息。我已将其包含在我的组件样式中。随着浏览器支持它,我的复选框将更符合粉色和紫色配色方案。

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

A screenshot from Chromium on Linux of pink checkboxes

使用固定渐变和 focus-within 的颜色弹出

当颜色弹出被少量使用时,效果最佳,我喜欢实现它的一种方法是通过色彩鲜艳的 UI 交互。

上面的视频中有很多 UI 反馈和交互层,它们通过以下方式帮助赋予交互个性

  • 突出显示上下文。
  • 提供范围中值“有多满”的 UI 反馈。
  • 提供字段正在接受输入的 UI 反馈。

为了在与元素交互时提供反馈,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;
    }
  }
}

当此元素的子元素之一具有 focus-within 时

  1. .fieldset-item 背景被分配了更高对比度的表面颜色。
  2. 嵌套的 svg 填充为白色以获得更高的对比度。
  3. 嵌套的 <picture> clip-path 扩展为完整的圆形,并且背景填充了明亮的固定渐变。

自定义范围

给定以下 HTML 输入元素,我将向您展示如何自定义其外观

<input type="range">

我们需要自定义此元素的 3 个部分

  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 上的一篇精彩文章,作者是 Ana Tudor,它演示了轨道填充的仅限 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">

我们需要自定义此元素的 3 个部分

  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-styleposition 样式为我们稍后将引入的伪元素做好准备,以设置高亮样式。否则,它主要是我的一些次要的个人风格。我喜欢光标是指针,我喜欢轮廓偏移,默认复选框太小,如果 accent-color 得到支持,请将这些复选框引入品牌配色方案。

复选框标签

为复选框提供标签非常重要,原因有两个。第一个是表示复选框值用于什么,以回答“什么开启或关闭?”。第二个是用于 UX,Web 用户已经习惯于通过其关联的标签与复选框进行交互。

输入
<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 属性,该属性通过 ID 指向复选框:<label for="text-notifications">。在您的复选框上,同时加倍名称和 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://webdev.ac.cn/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)。父元素创建了一个 3D 空间,而此伪元素子元素通过将自身稍微向后放置在 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 管理轨道填充颜色,所以现在让我们看一下与 <form> 相关的 JavaScript

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

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

每次与表单交互并更改表单时,控制台都会将表单作为对象记录到表格中,以便在提交到服务器之前轻松查看。

A screenshot of the console.table() results, where the form data is shown in a table

结论

现在您知道我是如何做到的了,您会怎么做呢?!这构成了一些有趣的组件架构!谁将制作第一个在其喜欢的框架中带有插槽的版本?🙂

让我们使我们的方法多样化,并学习在 Web 上构建的所有方法。创建一个演示,在 Twitter 上向我发送链接,我将其添加到下面的 社区混音部分!

社区混音

  • @tomayac 的风格是关于复选框标签的悬停区域!此版本在元素之间没有悬停间隙:演示源代码