CSS Handbook · 现代进阶特性

第十一章 / 11.5 表单控件样式

第十一章 · 11.5 节

表单控件样式


2.4 节已经学过 :required、:invalid、:checked 这些表单相关的伪类,用来"根据状态选中元素"。这一节补上另一半:如何真正改变复选框、单选框、下拉框这些原生控件的外观。

appearance:接管原生外观

复选框、单选框、下拉框默认由操作系统绘制,样式无法直接用普通属性覆盖。appearance: none 可以去掉这层系统默认外观,把控件变成一个"空白画布",之后就能像 div 一样自由设置 border、background 等属性:

input[type="checkbox"] {
  appearance: none;
  width: 18px;
  height: 18px;
  border: 2px solid #999;
  border-radius: 4px;
  display: inline-grid;
  place-content: center;
}

input[type="checkbox"]::before {
  content: "";
  width: 10px;
  height: 10px;
  background: teal;
  clip-path: polygon(...); /* 用裁剪路径画一个对勾 */
  transform: scale(0);
  transition: transform .1s;
}

input[type="checkbox"]:checked::before {
  transform: scale(1); /* 8.2 节学过的过渡,配合 :checked 状态切换 */
}
提醒appearance: none 之后,浏览器不再提供任何默认的选中状态视觉反馈,务必自己补上 :checked、:focus-visible 样式,否则会造成可访问性倒退。

accent-color:低成本改配色

如果只是想让复选框、单选框、进度条、range 滑块跟随品牌色,不需要动用 appearance: none 这种"推倒重来"的方式——accent-color 一行属性就能让浏览器用你指定的颜色重新绘制原生控件,同时保留系统的可访问性行为:

:root {
  accent-color: teal;
}
DEMO · accent-color 覆盖前后对比

::placeholder 伪元素

输入框的占位文字可以通过 ::placeholder 伪元素单独设置颜色、字重,但要注意它能设置的属性有限(主要是文字相关属性,不支持 background 等布局属性):

input::placeholder {
  color: #aaa;
  font-style: italic;
}

结合表单伪类使用

控件外观应该和 2.4 节的表单伪类联动,让用户能一眼看出当前状态,这比单纯"好看"更重要:

input:focus-visible {
  outline: 2px solid teal;
  outline-offset: 2px;
}
input:invalid:not(:placeholder-shown) {
  border-color: crimson; /* 只在用户已经输入内容后才提示错误,而非一进页面就标红 */
}
input:disabled {
  background: #f2f2f2;
  cursor: not-allowed;
}

Chapter 11 · Section 11.5

Styling Form Controls


Section 2.4 already covered the form-related pseudo-classes such as :required, :invalid, and :checked, which select elements based on their state. This section fills in the other half: how to actually change the appearance of native controls such as checkboxes, radio buttons, and dropdowns.

appearance: Taking Over the Native Look

Checkboxes, radio buttons, and dropdowns are drawn by the operating system by default, so ordinary properties cannot override their styles directly. appearance: none strips away this system default look and turns the control into a "blank canvas"; after that you can freely set border, background, and other properties just like a div:

input[type="checkbox"] {
  appearance: none;
  width: 18px;
  height: 18px;
  border: 2px solid #999;
  border-radius: 4px;
  display: inline-grid;
  place-content: center;
}

input[type="checkbox"]::before {
  content: "";
  width: 10px;
  height: 10px;
  background: teal;
  clip-path: polygon(...); /* draw a checkmark with a clip path */
  transform: scale(0);
  transition: transform .1s;
}

input[type="checkbox"]:checked::before {
  transform: scale(1); /* the transition from Section 8.2, toggled by the :checked state */
}
ReminderAfter appearance: none, the browser no longer provides any default visual feedback for the checked state, so be sure to add your own :checked and :focus-visible styles, or accessibility will regress.

accent-color: Low-Cost Color Changes

If you only want checkboxes, radio buttons, progress bars, and range sliders to follow your brand color, you do not need the "tear it down and start over" approach of appearance: none — a single accent-color property lets the browser redraw the native controls with the color you specify while keeping the system's accessibility behavior:

:root {
  accent-color: teal;
}
DEMO · accent-color before and after

The ::placeholder Pseudo-element

The placeholder text of an input can be given its own color and font weight through the ::placeholder pseudo-element, but note that only a limited set of properties can be set (mainly text-related properties; layout properties such as background are not supported):

input::placeholder {
  color: #aaa;
  font-style: italic;
}

Combining with Form Pseudo-classes

Control appearance should work together with the form pseudo-classes from Section 2.4 so users can tell the current state at a glance — that matters more than merely "looking nice":

input:focus-visible {
  outline: 2px solid teal;
  outline-offset: 2px;
}
input:invalid:not(:placeholder-shown) {
  border-color: crimson; /* only flag errors after the user has typed something, instead of turning red as soon as the page loads */
}
input:disabled {
  background: #f2f2f2;
  cursor: not-allowed;
}