如何构建自定义表单控件 - 学习 Web 开发

如何构建自定义表单控件
===========

在许多情况下，可用的原生 HTML 表单控件是不够的。如果要在某些控件（例如 `<select>` 元素）上设置高级样式，或者如果要提供自定义行为，你就需要考虑构建自己的控件。

在本文中，我们会看到如何构建自定义控件。为此，我们将使用这样一个示例：重建 `<select>` 元素。我们还将讨论如何构建、何时构建自定义控件、构建是否存在意义，以及构建控件的相关注意事项。

**备注：** 我们将专注于构建控件，而不是怎样让代码更通用或可复用；那会涉及一些非基础的 JavaScript 代码和未知上下文下的 DOM 操作，这超过了这篇文章的范畴。

设计、结构和语义
--------

在构建一个自定义控件之前，首先你要确切的知道你要什么。这将为你节省宝贵的时间。特别地，清楚地定义控件的所有状态非常重要。要做到这一点，从状态和行为表现都为人所熟知的现有控件开始是很好的选择，这样你可以充分地模仿这些控件。

在我们的示例中，我们将重建 `<select>` 元素，这是我们希望实现的结果：

![选择框的三种状态](/zh-CN/docs/Learn/Forms/How_to_build_custom_form_controls/custom-select.png)

上面图片显示了我们控件的三个主要状态：正常状态（左）、活动状态（中）和展开状态（右）。

在行为方面，我们正在重建原始 HTML 元素，因此它应该具有与原生 HTML 元素相同的行为和语义。我们要求我们的控件可以通过鼠标和键盘进行使用，并且可以被屏幕阅读器所识别，就像任何原生控件一样。首先让我们定义控件如何进入每种状态：

**在以下情况下，控件处于正常状态：**

* 页面加载。
* 控件处于活动状态，但用户点击了控件以外的任何位置。
* 控件处于活动状态，但用户使用键盘（例如 `Tab` 键）将焦点移动到另一个控件。

**在以下情况下，控件处于活动状态：**

* 用户点击或在触摸屏上触摸控件。
* 用户按下 tab 键使控件获得了焦点。
* 控件处于展开状态然后用户点击控件。

**在以下情况下，控件处于展开状态：**

* 控件处于非展开状态时被用户点击。

我们知道如何改变状态后，定义如何改变控件的值同样重要：

**在以下情况下，其值将会被改变：**

* 控件在展开状态下用户点击一个选项。
* 控件在活动状态下用户按下键盘的上/下方向键。

**在以下情况下，其值不会被改变：**

* 在选择第一个选项时，用户按下键盘的上方向键。
* 在选择最后一个选项时，用户按下键盘的下方向键。

最后，让我们定义控件的选项将要怎么表现：

* 当控件处于展开状态时，被选中的选项将被突出显示
* 当鼠标悬停在某个选项上时，该选项将被突出显示，并且之前突出显示的选项将返回其正常的状态

对于我们的示例的目的，我们将就此结束；但是，如果你是一个认真的读者，你会注意到我们省略了一些东西，例如，你认为用户在控件处于展开状态时点击 tab 键会发生什么？答案是：*什么也不会发生*。好吧，似乎很明显这就是正确的行为，但事实是，因为在我们的规范中没有定义这种情况，我们很容易忽略这种行为。在团队环境中尤其是这样，因为设计控件行为的人与实现的人通常是不同的。

另外一个有趣的例子是：当控件处于展开状态时，用户按下键盘上方向键和下方向键将会发生什么？这个问题有些棘手，如果你认为活动状态和展开状态是完全不同的，那么答案又是“什么都不会发生”，因为我们没有定义任何在展开状态下键盘的交互行为。从另一个方面看，如果你认为活动状态和展开状态是有重叠的部分，那么控件的值可能会改变，但是被选中的选项肯定不会相应的进行突出显示，同样是因为我们没有定义在控件展开状态下的任何键盘交互事件（我们仅仅定义了控件打开会发生什么，而没有定义在其打开后会发生什么）。

我们必须进一步思考：按退出键会发生什么？按下 `Esc` 键会关闭一个打开的选择框。记住，如果你想要提供与现有的原生 `<select>` 相同的功能，那么它应该对所有用户都有相同的行为，不论是键盘、鼠标、触摸、屏幕阅读器，还是其他任何输入设备。

在我们的示例中，规范的缺失是显而易见的，所以我们将着手处理它们，但是对于一些没有人想到去定义正确行为的控件而言，这的确是一个问题。所以在元素（例如 `<select>`）标准化阶段，规范作者花费了大量的时间来定义每个输入设备每个用例的所有涉及的交互。创建新的控件并不容易，特别是你正在创建以前从未做过的东西，没有人知道其预期的行为和相关的交互是什么。至少 select 已经完成了这些设计，所以我们知道它应该如何表现！

设计新的交互方式只是行业中重要参与者的一种选择，他们有足够的影响力来推动他们创建的交互方式成为标准。例如，Apple 于 2001 年在 iPod 中推出了滚轮。他们拥有足够的市场份额，而成功推出了一种全新的设备交互方式，这是大多数设备公司无法做到的。

最好不要发明新的用户交互方式。对于你添加的任何交互方式，在设计阶段花费时间至关重要；如果你对一种行为的定义不够合适，或者忘记定义了某种行为，那么在用户习惯之后，将很难去重新定义它们。如果你在定义时有疑问，请征询他人的意见，如果你有预算，请不要犹豫去进行用户可用性测试，这个过程被称为用户体验设计（UX Design），如果你想要深入的学习相关的内容，请查阅下面这些有用资源：

* UXMatters.com
* UXDesign.com
* SmashingMagazine 用户体验设计部分

**备注：** 此外，在绝大多数系统中，还有一种方法能够打开 `<select>` 元素来观察其所有的选项（这和用鼠标点击 `<select>` 元素是一样的）。这可以通过 Windows 下的 `Alt` + `Down` 实现。这没有在我们的示例中实现，但是这样做会很方便，因为鼠标点击（`click`）事件就是由该原理实现的。

### 定义语义化的 HTML 结构

现在控件的基本功能已经决定，可以开始构建自定义控件了。第一步就是去确定它的 HTML 结构并给予一些基本的语义规则。重构 `<select>` 元素需要这样做：

```html
<!-- 这是我们控件的主要容器。
 tabindex 属性是用来让用户聚焦在控件上的。
 稍后我们会发现最好通过 JavaScript 来设定它的值。-->
<div class="select" tabindex="0">
  <!-- 这个容器用来显示控件当前的值 -->
  <span class="value">Cherry</span>

  <!-- 这个容器包含我们的控件的所有可用选项。
 因为它是一个列表，用 ul 元素是有意义的。-->
  <ul class="optList">
    <!-- 每个选项只包含用来显示的值，
 稍后我们会知道如何处理和表单一起发送的真实值 -->
    <li class="option">Cherry</li>
    <li class="option">Lemon</li>
    <li class="option">Banana</li>
    <li class="option">Strawberry</li>
    <li class="option">Apple</li>
  </ul>
</div>
```

注意类名的使用：不管实际使用了哪种底层 HTML 元素，它们都标识每个相关的部分。这很重要，因为这样做能确保我们的 CSS 和 JavaScript 不会和 HTML 结构强绑定，这样我们就可以在不破坏使用控件的代码的情况下进行实现更改。比如，如果你希望增加一个等价的 `<optgroup>` 元素。

然而，类名并不提供语义值。到现在为止，屏幕阅读器的用户只能“看到”无序列表。我们后面会为其添加 ARIA 语义。

使用 CSS 创建外观
-----------

现在我们有了结构，我们可以开始设计我们的控件了。构建自定义控件的重点是能够完全按照我们的期望设置它的样式。为了达到这个目的，我们将 CSS 部分的工作分为两部分：第一部分是让我们的控件表现得像一个 `<select>` 元素所必需的的 CSS 规则，第二部分包含了让控件看起来像我们所希望那样的精妙样式。

### 所需的样式

所需的样式是那些用以处理我们控件的三种状态的必须样式。

```css
.select {
  /\* 这将为选项列表创建一个上下文定位；如果完全支持 focus-within，
 则将其添加到“.select:focus-within”是个更好的选择
 \*/
  position: relative;

  /\* 这将使我们的控件成为文本流的一部分，同时又可以调整大小 \*/
  display: inline-block;
}
```

我们需要一个额外的 `active` 类来定义我们的控件处于其激活状态时的的界面外观。因为我们的控件是可以聚焦的，我们通过 `:focus` 伪类复用自定义样式来确保它们表现得一样。

```css
.select .active,
.select:focus {
  outline: none;

  /\* 这里的 box-shadow 属性并非必须，但确保活动状态能看出来非常重要——我们
 将其作为一个默认值，你可以随意地覆盖掉它。\*/
  box-shadow: 0 0 3px 1px #227755;
}
```

现在，让我们处理选项列表：

```css
/\* 这里的 .select 选择器帮助我们确保定义的类是
 在我们的控件里的那个。 \*/
.select .optList {
  /\* 这可以确保我们的选项列表将会显示在值的下面，并且会处在
 HTML 流之外 \*/
  position: absolute;
  top: 100%;
  left: 0;
}
```

我们需要一个额外的类来处理选项列表隐藏时的情况。为了管理没有完全匹配的活动状态和展开状态之间的差异，这是有必要的。

```css
.select .optList.hidden {
  /\* 这是一个以无障碍的形式隐藏列表的简单方法，
 对无障碍我们将在最后进一步拓展 \*/
  max-height: 0;
  visibility: hidden;
}
```

**备注：** 我们也可以使用 `transform: scale(1, 0)` 来指定选项列表的高度为零，但宽度不变。

### 美化

所以现在我们的基本功能已经就位，有趣的事情就可以开始了。下面是一个可行的简单示例，这和本文开头的截图是相对应的。不管怎样，你可以随意尝试看看能想出什么。

```css
.select {
  /\* 假设的单位换算是 1em == 16px，这是大多数浏览器的默认值。
 如果你对 em 和 px 的转换感到疑惑，请参考 https://nekocalc.com/px-to-em-converter \*/
  font-size: 0.625em; /\* 这个（10px）是以 em 值表达这个上下文的字体大小 \*/
  font-family: Verdana, Arial, sans-serif;

  box-sizing: border-box;

  /\* 我们需要为将要添加的向下箭头准备一些额外的空间 \*/
  padding: 0.1em 2.5em 0.2em 0.5em;
  width: 10em; /\* 100px \*/

  border: 0.2em solid #000;
  border-radius: 0.4em;
  box-shadow: 0 0.1em 0.2em rgba(0, 0, 0, 0.45);

  /\* 第一段声明是为不支持线性渐变的浏览器准备的。 \*/
  background: #f0f0f0;
  background: linear-gradient(0deg, #e3e3e3, #fcfcfc 50%, #f0f0f0);
}

.select .value {
  /\* 因为值的宽度可能超过控件的宽度，我们需要确保它不会改变控件的宽度。如果内容溢出了，我们显示省略号 \*/
  display: inline-block;
  width: 100%;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  vertical-align: top;
}
```

我们不需要一个额外的元素来设计向下箭头，而使用 `::after` 伪类替代。这也可以通过使用一张加在 `select` 类上的简单的背景图像来实现。

```css
.select::after {
  content: "▼"; /\* 我们使用了 unicode 字符 U+25BC，请确保设置了 charset meta 标签 \*/
  position: absolute;
  z-index: 1; /\* 这对于防止箭头覆盖选项列表很重要 \*/
  top: 0;
  right: 0;

  box-sizing: border-box;

  height: 100%;
  width: 2em;
  padding-top: 0.1em;

  border-left: 0.2em solid #000;
  border-radius: 0 0.1em 0.1em 0;

  background-color: #000;
  color: #fff;
  text-align: center;
}
```

接下来，让我们定义选项列表的样式。

```css
.select .optList {
  z-index: 2; /\* 我们显式定义选项列表始终与向下箭头重叠 \*/

  /\* 这会重置 ul 元素的默认样式 \*/
  list-style: none;
  margin: 0;
  padding: 0;

  box-sizing: border-box;

  /\* 这会确保即使数值比控件小，选项列表仍能变得跟控件自身一样宽 \*/
  min-width: 100%;

  /\* 万一列表太长了，它的内容会从垂直方向溢出（会自动添加一个竖向滚动条）
 但是水平方向不会（因为我们没有设定宽度，列表会自适应宽度。如果不能的话，内容会被截断） \*/
  max-height: 10em; /\* 100px \*/
  overflow-y: auto;
  overflow-x: hidden;

  border: 0.2em solid #000;
  border-top-width: 0.1em;
  border-radius: 0 0 0.4em 0.4em;

  box-shadow: 0 0.2em 0.4em rgba(0, 0, 0, 0.4);
  background: #f0f0f0;
}
```

对于选项，我们需要添加一个 `highlight` 类以便能标明用户将要选择或者已经选择的值。

```css
.select .option {
  padding: 0.2em 0.3em; /\* 2px 3px \*/
}

.select .highlight {
  background: #000;
  color: #ffffff;
}
```

这是我们的三种状态的结果（在此处查看源代码）：

#### 正常状态
```

<div class="select">
  <span class="value">Cherry</span>
  <ul class="optList hidden">
    <li class="option">Cherry</li>
    <li class="option">Lemon</li>
    <li class="option">Banana</li>
    <li class="option">Strawberry</li>
    <li class="option">Apple</li>
  </ul>
</div>
```

```
.select {
  position: relative;
  display: inline-block;
}

.select.active,
.select:focus {
  box-shadow: 0 0 3px 1px #227755;
  outline: none;
}

.select .optList {
  position: absolute;
  top: 100%;
  left: 0;
}

.select .optList.hidden {
  max-height: 0;
  visibility: hidden;
}

.select {
  font-size: 0.625em; /\* 10px \*/
  font-family: Verdana, Arial, sans-serif;

  box-sizing: border-box;

  padding: 0.1em 2.5em 0.2em 0.5em; /\* 1px 25px 2px 5px \*/
  width: 10em; /\* 100px \*/

  border: 0.2em solid #000; /\* 2px \*/
  border-radius: 0.4em; /\* 4px \*/

  box-shadow: 0 0.1em 0.2em rgba(0, 0, 0, 0.45); /\* 0 1px 2px \*/

  background: #f0f0f0;
  background: linear-gradient(0deg, #e3e3e3, #fcfcfc 50%, #f0f0f0);
}

.select .value {
  display: inline-block;
  width: 100%;
  overflow: hidden;

  white-space: nowrap;
  text-overflow: ellipsis;
  vertical-align: top;
}

.select::after {
  content: "▼";
  position: absolute;
  z-index: 1;
  height: 100%;
  width: 2em; /\* 20px \*/
  top: 0;
  right: 0;

  padding-top: 0.1em;

  box-sizing: border-box;

  text-align: center;

  border-left: 0.2em solid #000;
  border-radius: 0 0.1em 0.1em 0;

  background-color: #000;
  color: #fff;
}

.select .optList {
  z-index: 2;

  list-style: none;
  margin: 0;
  padding: 0;

  background: #f0f0f0;
  border: 0.2em solid #000;
  border-top-width: 0.1em;
  border-radius: 0 0 0.4em 0.4em;

  box-shadow: 0 0.2em 0.4em rgba(0, 0, 0, 0.4);

  box-sizing: border-box;

  min-width: 100%;
  max-height: 10em; /\* 100px \*/
  overflow-y: auto;
  overflow-x: hidden;
}

.select .option {
  padding: 0.2em 0.3em;
}

.select .highlight {
  background: #000;
  color: #ffffff;
}
```

#### 活动状态
```

<div class="select active">
  <span class="value">Cherry</span>
  <ul class="optList hidden">
    <li class="option">Cherry</li>
    <li class="option">Lemon</li>
    <li class="option">Banana</li>
    <li class="option">Strawberry</li>
    <li class="option">Apple</li>
  </ul>
</div>
```

```
.select {
  position: relative;
  display: inline-block;
}

.select.active,
.select:focus {
  box-shadow: 0 0 3px 1px #227755;
  outline: none;
}

.select .optList {
  position: absolute;
  top: 100%;
  left: 0;
}

.select .optList.hidden {
  max-height: 0;
  visibility: hidden;
}

.select {
  font-size: 0.625em; /\* 10px \*/
  font-family: Verdana, Arial, sans-serif;

  box-sizing: border-box;

  padding: 0.1em 2.5em 0.2em 0.5em; /\* 1px 25px 2px 5px \*/
  width: 10em; /\* 100px \*/

  border: 0.2em solid #000; /\* 2px \*/
  border-radius: 0.4em; /\* 4px \*/

  box-shadow: 0 0.1em 0.2em rgba(0, 0, 0, 0.45); /\* 0 1px 2px \*/

  background: #f0f0f0;
  background: linear-gradient(0deg, #e3e3e3, #fcfcfc 50%, #f0f0f0);
}

.select .value {
  display: inline-block;
  width: 100%;
  overflow: hidden;

  white-space: nowrap;
  text-overflow: ellipsis;
  vertical-align: top;
}

.select::after {
  content: "▼";
  position: absolute;
  z-index: 1;
  height: 100%;
  width: 2em; /\* 20px \*/
  top: 0;
  right: 0;

  padding-top: 0.1em;

  box-sizing: border-box;

  text-align: center;

  border-left: 0.2em solid #000;
  border-radius: 0 0.1em 0.1em 0;

  background-color: #000;
  color: #fff;
}

.select .optList {
  z-index: 2;

  list-style: none;
  margin: 0;
  padding: 0;

  background: #f0f0f0;
  border: 0.2em solid #000;
  border-top-width: 0.1em;
  border-radius: 0 0 0.4em 0.4em;

  box-shadow: 0 0.2em 0.4em rgba(0, 0, 0, 0.4);

  box-sizing: border-box;

  min-width: 100%;
  max-height: 10em; /\* 100px \*/
  overflow-y: auto;
  overflow-x: hidden;
}

.select .option {
  padding: 0.2em 0.3em;
}

.select .highlight {
  background: #000;
  color: #ffffff;
}
```

#### 展开状态
```

<div class="select active">
  <span class="value">Cherry</span>
  <ul class="optList">
    <li class="option highlight">Cherry</li>
    <li class="option">Lemon</li>
    <li class="option">Banana</li>
    <li class="option">Strawberry</li>
    <li class="option">Apple</li>
  </ul>
</div>
```

```
.select {
  position: relative;
  display: inline-block;
}

.select.active,
.select:focus {
  box-shadow: 0 0 3px 1px #227755;
  outline: none;
}

.select .optList {
  position: absolute;
  top: 100%;
  left: 0;
}

.select .optList.hidden {
  max-height: 0;
  visibility: hidden;
}

.select {
  font-size: 0.625em; /\* 10px \*/
  font-family: Verdana, Arial, sans-serif;

  box-sizing: border-box;

  padding: 0.1em 2.5em 0.2em 0.5em; /\* 1px 25px 2px 5px \*/
  width: 10em; /\* 100px \*/

  border: 0.2em solid #000; /\* 2px \*/
  border-radius: 0.4em; /\* 4px \*/

  box-shadow: 0 0.1em 0.2em rgba(0, 0, 0, 0.45); /\* 0 1px 2px \*/

  background: #f0f0f0;
  background: linear-gradient(0deg, #e3e3e3, #fcfcfc 50%, #f0f0f0);
}

.select .value {
  display: inline-block;
  width: 100%;
  overflow: hidden;

  white-space: nowrap;
  text-overflow: ellipsis;
  vertical-align: top;
}

.select::after {
  content: "▼";
  position: absolute;
  z-index: 1;
  height: 100%;
  width: 2em; /\* 20px \*/
  top: 0;
  right: 0;

  padding-top: 0.1em;

  box-sizing: border-box;

  text-align: center;

  border-left: 0.2em solid #000;
  border-radius: 0 0.1em 0.1em 0;

  background-color: #000;
  color: #fff;
}

.select .optList {
  z-index: 2;

  list-style: none;
  margin: 0;
  padding: 0;

  background: #f0f0f0;
  border: 0.2em solid #000;
  border-top-width: 0.1em;
  border-radius: 0 0 0.4em 0.4em;

  box-shadow: 0 0.2em 0.4em rgba(0, 0, 0, 0.4);

  box-sizing: border-box;

  min-width: 100%;
  max-height: 10em; /\* 100px \*/
  overflow-y: auto;
  overflow-x: hidden;
}

.select .option {
  padding: 0.2em 0.3em;
}

.select .highlight {
  background: #000;
  color: #fff;
}
```

通过 JavaScript 让你的控件动起来
----------------------

现在我们的设计和结构已经完成了。我们可以写些 JavaScript 代码来让这个控件真正生效。

**警告：** 下面的代码仅仅是教学性质的，不是生产环境的代码，并且不应该照搬使用。这种方案不具有前瞻性，而且可能在旧式浏览器上会不工作。这里面还有冗余的部分，在生产环境下，代码需要优化。

### 它为什么不生效？

在我们开始之前，要记住一件和 JavaScript 有关的非常重要的事情：**在浏览器中，这是一种不可靠的技术**。当你构建一个自定义控件时，你会不得不依赖于 JavaScript，因为这是将所有的东西联系在一起的线索。但是，很多情况下，JavaScript 不能在浏览器中运行。

* 用户禁用了 JavaScript：这是最不常见的情形。现在只有很少的人会禁用 JavaScript。
* 脚本没有加载：这是最常见的情形，特别是在移动端上，在那些网络非常不可靠的地方。
* 脚本是有问题的：你应该总是考虑这种可能性。
* 脚本和第三方脚本冲突：这可能会由用户使用的跟踪脚本和一些书签工具引发。
* 脚本与一个浏览器的扩展冲突，或者受其影响。（比如 Firefox 的 NoScript 扩展或者 Chrome 的 ScriptBlock 扩展）。
* 用户在使用旧版浏览器，而且你需要的一些特性没有被支持。当你使用一些最新的 API 时，这种情况会经常发生。
* 在 JavaScript 完全下载、解析和执行前，用户已经开始与内容进行交互。

因为这些风险，认真考虑 JavaScript 不生效时会发生什么是很重要的。处理这个问题的细节超出了这篇文章的范围，因为这与你有多么想使你的脚本具有通用性和可复用性更加相关，不过我们将在我们的示例中考虑与其相关的基本内容。

在我们的示例中，如果 JavaScript 代码没有运行，我们会回退到显示一个标准的 `<select>` 元素。包括我们的控件和 `<select>`；显示哪个取决于 body 元素的类，当加载成功时，脚本会更新 body 元素的类以使得控件生效。

为了实现这一点，我们需要两样东西。

首先，在每次使用我们的自定义控件前，我们需要添加一个标准的 `<select>` 元素。即使我们的 JavaScript 按预期工作，这个“额外”的 select 也是有好处的：我们可以使用这个 select 来将来自我们自定义的表单控件以及其他部分的表单数据发送出去。我们随后会详细的解释这一点。

```html
<body class="no-widget">
  <form>
    <select name="myFruit">
      <option>Cherry</option>
      <option>Lemon</option>
      <option>Banana</option>
      <option>Strawberry</option>
      <option>Apple</option>
    </select>

    <div class="select">
      <span class="value">Cherry</span>
      <ul class="optList hidden">
        <li class="option">Cherry</li>
        <li class="option">Lemon</li>
        <li class="option">Banana</li>
        <li class="option">Strawberry</li>
        <li class="option">Apple</li>
      </ul>
    </div>
  </form>
</body>
```

第二，我们需要两个新的类来隐藏不需要的元素：如果脚本未运行，我们会在视觉上隐藏自定义控件；如果脚本正常运行，则隐藏“真正”的 `<select>` 元素）。注意默认情况下，我们的 HTML 代码会隐藏我们的自定义控件。

```css
.widget select,
.no-widget .select {
  /\* 这个 CSS 选择器大体上说的是：
 - 要么我们将 body 的类设置为“widget”，隐藏真实的 <select> 元素
 - 或是我们没有改变 body 的类，这样 body 的类还是“no-widget”，
 因此类为“select”的元素需要被隐藏 \*/
  position: absolute;
  left: -5000em;
  height: 0;
  overflow: hidden;
}
```

上面的 CSS 在视觉上隐藏了其中的一个元素，但对屏幕阅读器仍然“可见”。

接下来我们需要一个 JavaScript 开关来决定脚本是否运行。这个开关非常简单：如果页面加载时，我们的脚本运行了，它将会移除 `no-widget` 类，并添加 `widget` 类，由此切换 `<select>` 元素和自定义控件的可视性。

```js
window.addEventListener("load", function () {
  document.body.classList.remove("no-widget");
  document.body.classList.add("widget");
});
```

#### 不使用 JS

查看完整源代码。
```

<form class="no-widget">
  <select name="myFruit">
    <option>Cherry</option>
    <option>Lemon</option>
    <option>Banana</option>
    <option>Strawberry</option>
    <option>Apple</option>
  </select>

  <div class="select">
    <span class="value">Cherry</span>
    <ul class="optList hidden">
      <li class="option">Cherry</li>
      <li class="option">Lemon</li>
      <li class="option">Banana</li>
      <li class="option">Strawberry</li>
      <li class="option">Apple</li>
    </ul>
  </div>
</form>
```

```
.widget select,
.no-widget .select {
  position: absolute;
  left: -5000em;
  height: 0;
  overflow: hidden;
}
```

#### 使用 JS

查看完整源代码。
```

<form class="no-widget">
  <select name="myFruit">
    <option>Cherry</option>
    <option>Lemon</option>
    <option>Banana</option>
    <option>Strawberry</option>
    <option>Apple</option>
  </select>

  <div class="select">
    <span class="value">Cherry</span>
    <ul class="optList hidden">
      <li class="option">Cherry</li>
      <li class="option">Lemon</li>
      <li class="option">Banana</li>
      <li class="option">Strawberry</li>
      <li class="option">Apple</li>
    </ul>
  </div>
</form>
```

```
.widget select,
.no-widget .select {
  position: absolute;
  left: -5000em;
  height: 0;
  overflow: hidden;
}

.select {
  position: relative;
  display: inline-block;
}

.select.active,
.select:focus {
  box-shadow: 0 0 3px 1px #227755;
  outline: none;
}

.select .optList {
  position: absolute;
  top: 100%;
  left: 0;
}

.select .optList.hidden {
  max-height: 0;
  visibility: hidden;
}

.select {
  font-size: 0.625em; /\* 10px \*/
  font-family: Verdana, Arial, sans-serif;

  box-sizing: border-box;

  padding: 0.1em 2.5em 0.2em 0.5em; /\* 1px 25px 2px 5px \*/
  width: 10em; /\* 100px \*/

  border: 0.2em solid #000; /\* 2px \*/
  border-radius: 0.4em; /\* 4px \*/

  box-shadow: 0 0.1em 0.2em rgba(0, 0, 0, 0.45); /\* 0 1px 2px \*/

  background: #f0f0f0;
  background: linear-gradient(0deg, #e3e3e3, #fcfcfc 50%, #f0f0f0);
}

.select .value {
  display: inline-block;
  width: 100%;
  overflow: hidden;

  white-space: nowrap;
  text-overflow: ellipsis;
  vertical-align: top;
}

.select::after {
  content: "▼";
  position: absolute;
  z-index: 1;
  height: 100%;
  width: 2em; /\* 20px \*/
  top: 0;
  right: 0;

  padding-top: 0.1em;

  box-sizing: border-box;

  text-align: center;

  border-left: 0.2em solid #000;
  border-radius: 0 0.1em 0.1em 0;

  background-color: #000;
  color: #fff;
}

.select .optList {
  z-index: 2;

  list-style: none;
  margin: 0;
  padding: 0;

  background: #f0f0f0;
  border: 0.2em solid #000;
  border-top-width: 0.1em;
  border-radius: 0 0 0.4em 0.4em;

  box-shadow: 0 0.2em 0.4em rgba(0, 0, 0, 0.4);

  box-sizing: border-box;

  min-width: 100%;
  max-height: 10em; /\* 100px \*/
  overflow-y: auto;
  overflow-x: hidden;
}

.select .option {
  padding: 0.2em 0.3em;
}

.select .highlight {
  background: #000;
  color: #ffffff;
}
```

```
window.addEventListener("load", () => {
  const form = document.querySelector("form");

  form.classList.remove("no-widget");
  form.classList.add("widget");
});
```

**备注：** 如果你真的想让你的代码变得通用和可复用，最好不要仅仅是添加一个类选择器开关，而是通过添加一个控件类的方式来隐藏 `<select>` 元素，并且动态地在每一个 `<select>` 元素后面添加代表页面中自定义控件的 DOM 树。

### 让工作变得更简单

在我们将要构建的代码之中，我们将会使用标准的 DOM API 和 JavaScript 来完成要做的所有工作。我们准备使用的特性如下所示：

1. `classList`
2. `addEventListener()`
3. `NodeList.forEach()`
4. `querySelector()` 和 `querySelectorAll()`
### 构造事件回调

基础已经准备好了。我们现在可以开始定义用户每次同我们的控件交互时会用到的所有函数了。

```js
// 这个函数会用在每当我们想要停用一个自定义控件的时候
// 它需要一个参数：
// select：要停用的带有 `select` 类的节点
function deactivateSelect(select) {
  // 如果控件没有运行，不用进行任何操作
  if (!select.classList.contains("active")) return;

  // 我们需要获取自定义控件的选项列表
  const optList = select.querySelector(".optList");

  // 关闭选项列表
  optList.classList.add("hidden");

  // 然后停用控件本身
  select.classList.remove("active");
}

// 每当用户想要激活（或停用）这个控件的时候，会调用这个函数
// 它需要 2 个参数：
// select：要激活的带有 `select` 类的 DOM 节点
// selectList：包含所有带 `select` 类的 DOM 节点的列表
function activeSelect(select, selectList) {
  // 如果控件已经激活了，不进行任何操作
  if (select.classList.contains("active")) return;

  // 我们需要关闭所有自定义控件的活动状态
  // 因为 deactiveselect 函数满足 forEach 回调函数的所有请求，
  // 我们直接使用它，不使用中间匿名函数
  selectList.forEach(deactivateSelect);

  // 然后我们激活指定的控件
  select.classList.add("active");
}

// 每当用户想要打开/关闭选项列表的时候，会调用这个函数
// 它需要一个参数：
// select：要反转状态的列表的 DOM 节点
function toggleOptList(select) {
  // 该列表不包含在控件中
  const optList = select.querySelector(".optList");

  // 我们改变列表的类去显示/隐藏它
  optList.classList.toggle("hidden");
}

// 每当我们要高亮一个选项的时候，会调用该函数
// 它需要两个参数：
// select：带有 `select` 类的 DOM 节点，包含了需要高亮强调的选项
// option：需要高亮强调的带有'option'类的 DOM 节点
function highlightOption(select, option) {
  // 为我们的自定义 select 元素获取所有有效选项的列表
  const optionList = select.querySelectorAll(".option");

  // 我们移除所有选项的高亮强调
  optionList.forEach(function (other) {
    other.classList.remove("highlight");
  });

  // 我们高亮强调正确的选项
  option.classList.add("highlight");
}
```

这是你需要用来处理控件不同状态的所有代码。

接下来，我们将这些函数绑定到合适的事件上：

```js
// 我们处理文档加载时的事件绑定。
window.addEventListener("load", function () {
  const selectList = document.querySelectorAll(".select");

  // 每个自定义控件都需要初始化
  selectList.forEach(function (select) {
    // 它的 `option` 元素也需要
    const optionList = select.querySelectorAll(".option");

    // 每当用户的鼠标悬停在一个选项上时，我们高亮这个指定的选项
    optionList.forEach((option) => {
      option.addEventListener("mouseover", () => {
        // 注意：`select` 和 `option` 变量是我们函数调用范围内有效的闭包。
        highlightOption(select, option);
      });
    });

    // 每当用户点击一个自定义的 select 元素时
    select.addEventListener("click", (event) => {
      // 注意：`select` 变量是我们函数调用范围内有效的闭包。

      // 我们改变选项列表的可见性
      toggleOptList(select);
    });

    // 如果控件获得了焦点
    // 每当用户点击它或是用 tab 键访问这个控件时，其获得焦点
    select.addEventListener("focus", (event) => {
      // 注意：`select` 和 `selectlist` 变量是我们函数调用范围内有效的闭包。

      // 我们激活这个控件
      activeSelect(select, selectList);
    });

    // 如果控件失去焦点
    select.addEventListener("blur", (event) => {
      // 注意：`select` 变量是我们函数调用范围内有效的闭包。

      // 我们关闭这个控件
      deactivateSelect(select);
    });

    // 如果用户按下 `esc` 键，失去焦点
    select.addEventListener("keyup", (event) => {
      // 在松开 `esc` 键时关闭控件
      if (event.key === "Escape") {
        deactivateSelect(select);
      }
    });
  });
});
```

现在，我们的控件会根据我们的设计改变状态，但是它的值仍然没有更新。我们接下来会处理这件事。

#### 实时示例

查看完整源代码。
```

<form class="no-widget">
  <select name="myFruit" tabindex="-1">
    <option>Cherry</option>
    <option>Lemon</option>
    <option>Banana</option>
    <option>Strawberry</option>
    <option>Apple</option>
  </select>

  <div class="select" tabindex="0">
    <span class="value">Cherry</span>
    <ul class="optList hidden">
      <li class="option">Cherry</li>
      <li class="option">Lemon</li>
      <li class="option">Banana</li>
      <li class="option">Strawberry</li>
      <li class="option">Apple</li>
    </ul>
  </div>
</form>
```

```
.widget select,
.no-widget .select {
  position: absolute;
  left: -5000em;
  height: 0;
  overflow: hidden;
}

.select {
  position: relative;
  display: inline-block;
}

.select.active,
.select:focus {
  box-shadow: 0 0 3px 1px #227755;
  outline: none;
}

.select .optList {
  position: absolute;
  top: 100%;
  left: 0;
}

.select .optList.hidden {
  max-height: 0;
  visibility: hidden;
}

.select {
  font-size: 0.625em; /\* 10px \*/
  font-family: Verdana, Arial, sans-serif;

  box-sizing: border-box;

  padding: 0.1em 2.5em 0.2em 0.5em; /\* 1px 25px 2px 5px \*/
  width: 10em; /\* 100px \*/

  border: 0.2em solid #000; /\* 2px \*/
  border-radius: 0.4em; /\* 4px \*/

  box-shadow: 0 0.1em 0.2em rgba(0, 0, 0, 0.45); /\* 0 1px 2px \*/

  background: #f0f0f0;
  background: linear-gradient(0deg, #e3e3e3, #fcfcfc 50%, #f0f0f0);
}

.select .value {
  display: inline-block;
  width: 100%;
  overflow: hidden;

  white-space: nowrap;
  text-overflow: ellipsis;
  vertical-align: top;
}

.select::after {
  content: "▼";
  position: absolute;
  z-index: 1;
  height: 100%;
  width: 2em; /\* 20px \*/
  top: 0;
  right: 0;

  padding-top: 0.1em;

  box-sizing: border-box;

  text-align: center;

  border-left: 0.2em solid #000;
  border-radius: 0 0.1em 0.1em 0;

  background-color: #000;
  color: #fff;
}

.select .optList {
  z-index: 2;

  list-style: none;
  margin: 0;
  padding: 0;

  background: #f0f0f0;
  border: 0.2em solid #000;
  border-top-width: 0.1em;
  border-radius: 0 0 0.4em 0.4em;

  box-shadow: 0 0.2em 0.4em rgba(0, 0, 0, 0.4);

  box-sizing: border-box;

  min-width: 100%;
  max-height: 10em; /\* 100px \*/
  overflow-y: auto;
  overflow-x: hidden;
}

.select .option {
  padding: 0.2em 0.3em;
}

.select .highlight {
  background: #000;
  color: #ffffff;
}
```

```
function deactivateSelect(select) {
  if (!select.classList.contains("active")) return;

  const optList = select.querySelector(".optList");

  optList.classList.add("hidden");
  select.classList.remove("active");
}

function activeSelect(select, selectList) {
  if (select.classList.contains("active")) return;

  selectList.forEach(deactivateSelect);
  select.classList.add("active");
}

function toggleOptList(select, show) {
  const optList = select.querySelector(".optList");

  optList.classList.toggle("hidden");
}

function highlightOption(select, option) {
  const optionList = select.querySelectorAll(".option");

  optionList.forEach((other) => {
    other.classList.remove("highlight");
  });

  option.classList.add("highlight");
}

window.addEventListener("load", () => {
  const form = document.querySelector("form");

  form.classList.remove("no-widget");
  form.classList.add("widget");
});

window.addEventListener("load", () => {
  const selectList = document.querySelectorAll(".select");

  selectList.forEach((select) => {
    const optionList = select.querySelectorAll(".option");

    optionList.forEach((option) => {
      option.addEventListener("mouseover", () => {
        highlightOption(select, option);
      });
    });

    select.addEventListener(
      "click",
      (event) => {
        toggleOptList(select);
      },
      false,
    );

    select.addEventListener("focus", (event) => {
      activeSelect(select, selectList);
    });

    select.addEventListener("blur", (event) => {
      deactivateSelect(select);
    });

    select.addEventListener("keyup", (event) => {
      if (event.keyCode === 27) {
        deactivateSelect(select);
      }
    });
  });
});
```

### 处理控件的值

既然我们的控件已经开始工作了，我们必须添加代码，使其能够根据用户的输入更新取值，并且能将取值随表单数据一同发送。

实现这一点最简单的方法是在后台使用原生控件。这样的控件会使用浏览器提供的所有内置控件来跟踪值，并且在表单提交时，取值也会像往常一样发送。当有现成的功能时，我们再做重复工作就毫无意义了。

像前面所看到的那样，出于无障碍的原因，我们已经使用了一个原生的选择（select）控件作为回退显示内容；我们可轻松的将它的值与我们的自定义控件之间的值同步。

```js
// 这个函数更新显示的值并将其通过原生控件同步
// 它需要 2 个参数：
// select：含有要更新的值的 `select` 类的 DOM 节点
// index：要被选择的值的索引
function updateValue(select, index) {
  // 我们需要为了给定的自定义控件获取原生控件
  // 在我们的示例中，原生控件是自定义控件的“兄弟”
  const nativeWidget = select.previousElementSibling;

  // 我们也需要得到自定义控件的值占位符
  const value = select.querySelector(".value");

  // 还有整个选项列表
  const optionList = select.querySelectorAll(".option");

  // 我们将被选择的索引设定为我们的选择的索引
  nativeWidget.selectedIndex = index;

  // 更新相应的值占位符
  value.innerHTML = optionList[index].innerHTML;

  // 然后高亮我们自定义控件里对应的选项
  highlightOption(select, optionList[index]);
}

// 这个函数返回原生控件里当前选定的索引
// 它需要 1 个参数：
// select：跟原生控件有关的 `select` 类 DOM 节点
function getIndex(select) {
  // 我们需要为了给定的自定义控件访问原生控件
  // 在我们的示例中，原生控件是自定义控件的“兄弟”
  const nativeWidget = select.previousElementSibling;

  return nativeWidget.selectedIndex;
}
```

通过这两个函数，我们可以将原生控件绑定到自定义的控件上。

```js
// 我们在文档加载时处理事件的绑定。
window.addEventListener("load", () => {
  const selectList = document.querySelectorAll(".select");

  // 每个自定义控件都需要初始化
  selectList.forEach((select) => {
    const optionList = select.querySelectorAll(".option");
    const selectedIndex = getIndex(select);

    // 使我们的自定义控件可以获得焦点
    select.tabIndex = 0;

    // 我们让原生控件无法获得焦点
    select.previousElementSibling.tabIndex = -1;

    // 确保默认选中的值正确显示
    updateValue(select, selectedIndex);

    // 每当用户点击一个选项的时候，更新相应的值
    optionList.forEach((option, index) => {
      option.addEventListener("click", (event) => {
        updateValue(select, index);
      });
    });

    // 每当用户在获得焦点的控件上用键盘操作时，更新相应的值
    select.addEventListener("keyup", (event) => {
      let index = getIndex(select);

      // 当用户点击 esc 键时，关闭自定义控件
      if (event.key === "Escape") {
        deactivateSelect(select);
      }

      // 当用户点击向下箭头时，跳转到下一个选项
      if (event.key === "ArrowDown" && index < optionList.length - 1) {
        index++;
      }

      // 当用户点击向上箭头时，跳转到上一个选项
      if (event.key === "ArrowUp" && index > 0) {
        index--;
      }

      updateValue(select, index);
    });
  });
});
```

在上面的代码里，值得注意的是 `tabIndex` (en-US) 属性的使用。使用这个属性是很有必要的，这可以确保原生控件将永远不会获得焦点，而且还可以确保当用户使用键盘和鼠标时，我们的自定义控件能够获得焦点。

做完上面这些后，我们就完成了！

#### 实时示例

查看完整源代码。
```

<form class="no-widget">
  <select name="myFruit">
    <option>Cherry</option>
    <option>Lemon</option>
    <option>Banana</option>
    <option>Strawberry</option>
    <option>Apple</option>
  </select>

  <div class="select">
    <span class="value">Cherry</span>
    <ul class="optList hidden">
      <li class="option">Cherry</li>
      <li class="option">Lemon</li>
      <li class="option">Banana</li>
      <li class="option">Strawberry</li>
      <li class="option">Apple</li>
    </ul>
  </div>
</form>
```

```
.widget select,
.no-widget .select {
  position: absolute;
  left: -5000em;
  height: 0;
  overflow: hidden;
}

.select {
  position: relative;
  display: inline-block;
}

.select.active,
.select:focus {
  box-shadow: 0 0 3px 1px #227755;
  outline: none;
}

.select .optList {
  position: absolute;
  top: 100%;
  left: 0;
}

.select .optList.hidden {
  max-height: 0;
  visibility: hidden;
}

.select {
  font-size: 0.625em; /\* 10px \*/
  font-family: Verdana, Arial, sans-serif;

  box-sizing: border-box;

  padding: 0.1em 2.5em 0.2em 0.5em; /\* 1px 25px 2px 5px \*/
  width: 10em; /\* 100px \*/

  border: 0.2em solid #000; /\* 2px \*/
  border-radius: 0.4em; /\* 4px \*/

  box-shadow: 0 0.1em 0.2em rgba(0, 0, 0, 0.45); /\* 0 1px 2px \*/

  background: #f0f0f0;
  background: linear-gradient(0deg, #e3e3e3, #fcfcfc 50%, #f0f0f0);
}

.select .value {
  display: inline-block;
  width: 100%;
  overflow: hidden;

  white-space: nowrap;
  text-overflow: ellipsis;
  vertical-align: top;
}

.select::after {
  content: "▼";
  position: absolute;
  z-index: 1;
  height: 100%;
  width: 2em; /\* 20px \*/
  top: 0;
  right: 0;

  padding-top: 0.1em;

  box-sizing: border-box;

  text-align: center;

  border-left: 0.2em solid #000;
  border-radius: 0 0.1em 0.1em 0;

  background-color: #000;
  color: #fff;
}

.select .optList {
  z-index: 2;

  list-style: none;
  margin: 0;
  padding: 0;

  background: #f0f0f0;
  border: 0.2em solid #000;
  border-top-width: 0.1em;
  border-radius: 0 0 0.4em 0.4em;

  box-shadow: 0 0.2em 0.4em rgba(0, 0, 0, 0.4);

  box-sizing: border-box;

  min-width: 100%;
  max-height: 10em; /\* 100px \*/
  overflow-y: auto;
  overflow-x: hidden;
}

.select .option {
  padding: 0.2em 0.3em;
}

.select .highlight {
  background: #000;
  color: #ffffff;
}
```

```
function deactivateSelect(select) {
  if (!select.classList.contains("active")) return;

  const optList = select.querySelector(".optList");

  optList.classList.add("hidden");
  select.classList.remove("active");
}

function activeSelect(select, selectList) {
  if (select.classList.contains("active")) return;

  selectList.forEach(deactivateSelect);
  select.classList.add("active");
}

function toggleOptList(select, show) {
  const optList = select.querySelector(".optList");

  optList.classList.toggle("hidden");
}

function highlightOption(select, option) {
  const optionList = select.querySelectorAll(".option");

  optionList.forEach((other) => {
    other.classList.remove("highlight");
  });

  option.classList.add("highlight");
}

function updateValue(select, index) {
  const nativeWidget = select.previousElementSibling;
  const value = select.querySelector(".value");
  const optionList = select.querySelectorAll(".option");

  nativeWidget.selectedIndex = index;
  value.innerHTML = optionList[index].innerHTML;
  highlightOption(select, optionList[index]);
}

function getIndex(select) {
  const nativeWidget = select.previousElementSibling;

  return nativeWidget.selectedIndex;
}

window.addEventListener("load", () => {
  const form = document.querySelector("form");

  form.classList.remove("no-widget");
  form.classList.add("widget");
});

window.addEventListener("load", () => {
  const selectList = document.querySelectorAll(".select");

  selectList.forEach((select) => {
    const optionList = select.querySelectorAll(".option");

    optionList.forEach((option) => {
      option.addEventListener("mouseover", () => {
        highlightOption(select, option);
      });
    });

    select.addEventListener("click", (event) => {
      toggleOptList(select);
    });

    select.addEventListener("focus", (event) => {
      activeSelect(select, selectList);
    });

    select.addEventListener("blur", (event) => {
      deactivateSelect(select);
    });
  });
});

window.addEventListener("load", () => {
  const selectList = document.querySelectorAll(".select");

  selectList.forEach((select) => {
    const optionList = select.querySelectorAll(".option");
    const selectedIndex = getIndex(select);

    select.tabIndex = 0;
    select.previousElementSibling.tabIndex = -1;

    updateValue(select, selectedIndex);

    optionList.forEach((option, index) => {
      option.addEventListener("click", (event) => {
        updateValue(select, index);
      });
    });

    select.addEventListener("keyup", (event) => {
      let index = getIndex(select);

      if (event.key === "Escape") {
        deactivateSelect(select);
      }
      if (event.key === "ArrowDown" && index < optionList.length - 1) {
        index++;
      }
      if (event.key === "ArrowUp" && index > 0) {
        index--;
      }

      updateValue(select, index);
    });
  });
});
```

但是等等，我们真的做完了嘛？

使其变得无障碍
-------

我们构建了一个能够生效的东西，尽管这离一个特性齐全的选择框还差得远，但是它效果不错。但是我们已经完成的事情只不过是摆弄 DOM。这个控件并没有真正的语义，即使它看起来像一个选择框，但是从浏览器的角度来看并不是，所以辅助技术并不能明白这是一个选择框。简单来说，这个全新的选择框并不具备无障碍！

幸运的是，有一种解决方案叫做 ARIA。ARIA 代表“无障碍富互联网应用”。这是一个专为我们现在做的事情设计的 W3C 规范：使 web 应用和自定义控件可以无障碍访问，它本质上是一组用来拓展 HTML 的属性集，以便我们能够更好的描述角色、状态和属性，就像我们刚才设计的元素是它试图传递的原生元素一样。只要编辑 HTML 标记就可以使用这些属性。我们也可以通过 JavaScript 在用户更新选择的值时更新 ARIA 属性。

### `role` 属性

ARIA 使用的关键属性是角色（`role`）属性。`role` 属性接受一个值，该值定义了一个元素的用途。每一个角色定义了它自己的需求和行为。在我们的示例中，我们会使用 `listbox` 这一角色。这是一个“复合角色（composite role）”，表示具有该角色的元素应该有子元素，每个子元素都有特定的角色。（在这个案例中，至少有一个具有`option` 角色的子元素）。

同样值得注意的是，ARIA 定义了默认应用于标准 HTML 标记的角色。例如，`<table>` 元素与角色 `grid` 相匹配，而 `<ul>` 元素与角色 `list` 相匹配。由于我们使用了一个 `<ul>` 元素，我们想要确保我们控件的 `listbox` 角色能替代 `<ul>` 元素的 `list` 角色。为此，我们会使用角色 `presentation`。这个角色被设计成让我们来表示一个元素没有特殊的含义，并且仅仅用于提供信息。我们会将其应用到 `<ul>` 元素上。

为了支持 `listbox` 角色，我们只需要将我们的 HTML 改成这样：

```html
<!-- 我们把 role="listbox" 属性添加到我们的顶层元素 -->
<div class="select" role="listbox">
  <span class="value">Cherry</span>
  <!-- 我们也把 role="presentation" 添加到 ul 元素中 -->
  <ul class="optList" role="presentation">
    <!-- 然后把 role="option" 属性添加到所有 li 元素中 -->
    <li role="option" class="option">Cherry</li>
    <li role="option" class="option">Lemon</li>
    <li role="option" class="option">Banana</li>
    <li role="option" class="option">Strawberry</li>
    <li role="option" class="option">Apple</li>
  </ul>
</div>
```

**备注：** 不需要同时包含 `role` 属性和 `class` 属性。你可以在 CSS 中使用 `[role="option"]` 属性选择器来代替 `.option` 类。

### `aria-selected` 属性

仅仅使用 `role` 属性是不够的。ARIA 还提供了许多状态和属性特征。你能更好更充分的利用它们，你的控件就会能够被辅助技术更好地理解。在我们的示例中，我们会把使用限制在一个属性上：`aria-selected`。

`aria-selected` 属性被用来标记当前被选中的选项；这可以让辅助技术告知用户当前的选项是什么。我们会通过 JavaScript 动态地使用该属性，每当用户选择一个选项时标记选中的选项。为了达到这一目的，我们需要修正我们的 `updateValue()` 函数：

```js
function updateValue(select, index) {
  const nativeWidget = select.previousElementSibling;
  const value = select.querySelector(".value");
  const optionList = select.querySelectorAll('[role="option"]');

  // 我们确保所有的选项都没有被选中
  optionList.forEach((other) => {
    other.setAttribute("aria-selected", "false");
  });

  // 我们确保选定的选项被选中了
  optionList[index].setAttribute("aria-selected", "true");

  nativeWidget.selectedIndex = index;
  value.innerHTML = optionList[index].innerHTML;
  highlightOption(select, optionList[index]);
}
```

让屏幕阅读器聚焦于不可见的 select 而忽略我们的添加样式后的 select 似乎更简单，但这不是一个无障碍的解决方案。屏幕阅读器的用户不仅限于盲人；弱视甚至视力没问题的人也使用它们。因此，你不能让屏幕阅读器聚焦于不可见的元素。

下面是经过所有的改变之后的最终结果（藉由 NVDA 或 VoiceOver 这样的辅助技术尝试它，你会对此有更好的体会）。

#### 实时示例

查看完整源代码。
```

<form class="no-widget">
  <select name="myFruit">
    <option>Cherry</option>
    <option>Lemon</option>
    <option>Banana</option>
    <option>Strawberry</option>
    <option>Apple</option>
  </select>

  <div class="select" role="listbox">
    <span class="value">Cherry</span>
    <ul class="optList hidden" role="presentation">
      <li class="option" role="option" aria-selected="true">Cherry</li>
      <li class="option" role="option">Lemon</li>
      <li class="option" role="option">Banana</li>
      <li class="option" role="option">Strawberry</li>
      <li class="option" role="option">Apple</li>
    </ul>
  </div>
</form>
```

```
.widget select,
.no-widget .select {
  position: absolute;
  left: -5000em;
  height: 0;
  overflow: hidden;
}

.select {
  position: relative;
  display: inline-block;
}

.select.active,
.select:focus {
  box-shadow: 0 0 3px 1px #227755;
  outline: none;
}

.select .optList {
  position: absolute;
  top: 100%;
  left: 0;
}

.select .optList.hidden {
  max-height: 0;
  visibility: hidden;
}

.select {
  font-size: 0.625em; /\* 10px \*/
  font-family: Verdana, Arial, sans-serif;

  box-sizing: border-box;

  padding: 0.1em 2.5em 0.2em 0.5em; /\* 1px 25px 2px 5px \*/
  width: 10em; /\* 100px \*/

  border: 0.2em solid #000; /\* 2px \*/
  border-radius: 0.4em; /\* 4px \*/

  box-shadow: 0 0.1em 0.2em rgba(0, 0, 0, 0.45); /\* 0 1px 2px \*/

  background: #f0f0f0;
  background: linear-gradient(0deg, #e3e3e3, #fcfcfc 50%, #f0f0f0);
}

.select .value {
  display: inline-block;
  width: 100%;
  overflow: hidden;

  white-space: nowrap;
  text-overflow: ellipsis;
  vertical-align: top;
}

.select::after {
  content: "▼";
  position: absolute;
  z-index: 1;
  height: 100%;
  width: 2em; /\* 20px \*/
  top: 0;
  right: 0;

  padding-top: 0.1em;

  box-sizing: border-box;

  text-align: center;

  border-left: 0.2em solid #000;
  border-radius: 0 0.1em 0.1em 0;

  background-color: #000;
  color: #fff;
}

.select .optList {
  z-index: 2;

  list-style: none;
  margin: 0;
  padding: 0;

  background: #f0f0f0;
  border: 0.2em solid #000;
  border-top-width: 0.1em;
  border-radius: 0 0 0.4em 0.4em;

  box-shadow: 0 0.2em 0.4em rgba(0, 0, 0, 0.4);

  box-sizing: border-box;

  min-width: 100%;
  max-height: 10em; /\* 100px \*/
  overflow-y: auto;
  overflow-x: hidden;
}

.select .option {
  padding: 0.2em 0.3em;
}

.select .highlight {
  background: #000;
  color: #ffffff;
}
```

```
function deactivateSelect(select) {
  if (!select.classList.contains("active")) return;

  const optList = select.querySelector(".optList");

  optList.classList.add("hidden");
  select.classList.remove("active");
}

function activeSelect(select, selectList) {
  if (select.classList.contains("active")) return;

  selectList.forEach(deactivateSelect);
  select.classList.add("active");
}

function toggleOptList(select, show) {
  const optList = select.querySelector(".optList");

  optList.classList.toggle("hidden");
}

function highlightOption(select, option) {
  const optionList = select.querySelectorAll(".option");

  optionList.forEach((other) => {
    other.classList.remove("highlight");
  });

  option.classList.add("highlight");
}

function updateValue(select, index) {
  const nativeWidget = select.previousElementSibling;
  const value = select.querySelector(".value");
  const optionList = select.querySelectorAll(".option");

  optionList.forEach((other) => {
    other.setAttribute("aria-selected", "false");
  });

  optionList[index].setAttribute("aria-selected", "true");

  nativeWidget.selectedIndex = index;
  value.innerHTML = optionList[index].innerHTML;
  highlightOption(select, optionList[index]);
}

function getIndex(select) {
  const nativeWidget = select.previousElementSibling;

  return nativeWidget.selectedIndex;
}

window.addEventListener("load", () => {
  const form = document.querySelector("form");

  form.classList.remove("no-widget");
  form.classList.add("widget");
});

window.addEventListener("load", () => {
  const selectList = document.querySelectorAll(".select");

  selectList.forEach((select) => {
    const optionList = select.querySelectorAll(".option");
    const selectedIndex = getIndex(select);

    select.tabIndex = 0;
    select.previousElementSibling.tabIndex = -1;

    updateValue(select, selectedIndex);

    optionList.forEach((option, index) => {
      option.addEventListener("mouseover", () => {
        highlightOption(select, option);
      });

      option.addEventListener("click", (event) => {
        updateValue(select, index);
      });
    });

    select.addEventListener("click", (event) => {
      toggleOptList(select);
    });

    select.addEventListener("focus", (event) => {
      activeSelect(select, selectList);
    });

    select.addEventListener("blur", (event) => {
      deactivateSelect(select);
    });

    select.addEventListener("keyup", (event) => {
      let index = getIndex(select);

      if (event.keyCode === 27) {
        deactivateSelect(select);
      }
      if (event.keyCode === 40 && index < optionList.length - 1) {
        index++;
      }
      if (event.keyCode === 38 && index > 0) {
        index--;
      }

      updateValue(select, index);
    });
  });
});
```

如果你想继续前进，此示例中的代码需要进行一些改进才能变得通用和可复用。你可以尝试进行这方面的练习。有两个提示可以帮助你：我们所有函数的第一个参数都是相同的，这意味着这些函数需要相同的上下文。构建一个对象来共享该上下文是明智的。

代替方法：使用单选按钮
-----------

在上面的示例中，我们使用非语义化的 HTML、CSS 和 JavaScript 重新构建了一个 `<select>` 元素。这个 select 从有限的选项中选择一个选项，这和一组同名的 radio 按钮的功能是一样的。

我们可以使用单选按钮重新实现这个功能，让我们看看这个方法。

我们可以从一个完全语义化、无障碍且无序的单选按钮列表开始，使用一对语义化的 `<fieldset>` 和 `<legend>` 对来标记整个组。

```html
<fieldset>
  <legend>选择一种水果</legend>
  <ul class="styledSelect">
    <li>
      <input
 type="radio"
 name="fruit"
 value="Cherry"
 id="fruitCherry"
 checked />
      <label for="fruitCherry">樱桃</label>
    </li>
    <li>
      <input type="radio" name="fruit" value="Lemon" id="fruitLemon" />
      <label for="fruitLemon">柠檬</label>
    </li>
    <li>
      <input type="radio" name="fruit" value="Banana" id="fruitBanana" />
      <label for="fruitBanana">香蕉</label>
    </li>
    <li>
      <input
 type="radio"
 name="fruit"
 value="Strawberry"
 id="fruitStrawberry" />
      <label for="fruitStrawberry">草莓</label>
    </li>
    <li>
      <input type="radio" name="fruit" value="Apple" id="fruitApple" />
      <label for="fruitApple">苹果</label>
    </li>
  </ul>
</fieldset>
```

我们将对单选按钮列表（不是 legend/fieldset）设置一些样式，使其看起来有点像前面的示例，这里只是为了表明它可以实现：

```css
.styledSelect {
  display: inline-block;
  padding: 0;
}
.styledSelect li {
  list-style-type: none;
  padding: 0;
  display: flex;
}
.styledSelect [type="radio"] {
  position: absolute;
  left: -100vw;
  top: -100vh;
}
.styledSelect label {
  margin: 0;
  line-height: 2;
  padding: 0 0 0 4px;
}
.styledSelect:not(:focus-within) input:not(:checked) + label {
  height: 0;
  outline: none;
  overflow: hidden;
}
.styledSelect:not(:focus-within) input:checked + label {
  border: 0.2em solid #000;
  border-radius: 0.4em;
  box-shadow: 0 0.1em 0.2em rgba(0, 0, 0, 0.45);
}
.styledSelect:not(:focus-within) input:checked + label::after {
  content: "▼";
  background: black;
  float: right;
  color: white;
  padding: 0 4px;
  margin: 0 -4px 0 4px;
}
.styledSelect:focus-within {
  border: 0.2em solid #000;
  border-radius: 0.4em;
  box-shadow: 0 0.1em 0.2em rgba(0, 0, 0, 0.45);
}
.styledSelect:focus-within input:checked + label {
  background-color: #333;
  color: #fff;
  width: 100%;
}
```

无需 JavaScript，只需一点点 CSS，我们就可以为单选按钮列表添加样式，使其只显示选中的项目。当焦点在 `<fieldset>` 中的 `<ul>` 内时，列表会打开，上下（和左右）箭头可以选择上一个和下一个项目。尝试一下：

在某种程度上，这不需要 JavaScript 就可以工作。我们创建了一个与自定义控件类似的控件，即使 JavaScript 运行失败也能正常工作。看起来是一个很好的解决方案，对吧？嗯，但它不是完美的。它确实可以与键盘配合使用，但不能按预期使用鼠标单击。使用 Web 标准作为自定义控件的基础而不是依赖框架来创建没有原生语义的元素，可能更有意义。然而，我们的控件不具有与 `<select>` 原生具有的相同的功能。

从正面来说，屏幕阅读器完全可以访问这个控件，而且其完全可以使用键盘导航。但是，该控件并不是 `<select>` 的替代。有一些功能是不同或者缺失的。例如，所有的四个箭头都可以导航到选项，但是当用户在最后一个按钮上点击向下箭头时，它选到第一个按钮；它不会像 `<select>` 那样停在选项列表的顶部和底部。

我们将把添加这个缺失功能作为读者的一个练习。

总结
--

我们已经了解了所有和构建一个自定义表单控件相关的基础知识，但是如你所见，做这件事非常繁琐。在创建你自己的自定义控件之前，请考虑 HTML 是否提供可用于充分满足你的要求的替代元素。如果你的确需要创建自定义控件，那么通过第三方库，而不是自己从头构建会更容易，也更好。但是，如果你确实需要自己创建，修改现有元素或使用框架来实现预制控件，请记住创建可用且无障碍的表单控件比看起来更复杂。

这儿有一些库，在你编写自己的之前应该了解一下：

* jQuery UI
* AXE 无障碍的自定义选择下拉菜单
* msDropDown

如果你确实通过单选按钮、你自己的 JavaScript 或第三方库创建了替代控件，请确保其无障碍且功能可靠；也就是说，它需要能够更好地与各种浏览器配合使用，而这些浏览器与所使用的 Web 标准的兼容性各不相同。祝愉快！

参见
--

### 学习路径

* 你的第一个 HTML 表单
* 如何构造 HTML 表单
* 原生表单控件
* HTML5 的 input 类型
* 其他表单控件
* UI 伪类
* 为 HTML 表单添加样式
* 表单数据校验
* 发送表单数据
### 高级主题

* 使用 JavaScript 发送表单
* **如何构建自定义表单控件**
* 旧式浏览器中的 HTML 表单
* 高级设计 HTML 表单
* 表单控件兼容性列表


HTML5 的输入（input）类型 - 学习 Web 开发

HTML5 的输入（input）类型
==================

* 上一页
* Overview: Forms
* 下一页

在前一篇文章中我们学习了 `<input>` 元素，涵盖了自 HTML 早期以来可用的 `type` 属性的原始值。现在我们来具体看一看较新的表单控件，包含一些新添加至 HTML 5 的 input 类型，以允许收集更多特定类型的数据。

|  |  |
| --- | --- |
| 前提： |
 计算机基础知识和对于
 HTML 的基本理解。
  |
| 目标： | 了解可用于创建本地表单控件的较新的 input 类型值，以及如何使用 HTML 实现它们。 |

**备注：** 本篇文章中讨论的大多数特性都受到了广泛支持，如果有任何例外将会在文章中说明，如果你需要更多浏览器支持的细节，你应该查看我们的 HTML 表单元素参考，特别是深入的 <input> 类型 参考。

由于 HTML 表单控件的外观可能与设计者的规格有很大的不同，web 开发者有时会建立自己的自定义表单控件。我们在一个高级教程中介绍了这一点：如何构建自定义表单控件。

E-mail 地址字段
-----------

将 `type` 属性设置为 `email` 就可以使用这种控件：

```html
<input type="email" id="email" name="email" />
```

当使用了这种 `type` 类型时，用户需要输入一个合法的电子邮件地址，任何其他输入都会使得浏览器在表单提交时显示错误信息。你可以在下面的截图中看到这个行为：

![An invalid email input showing the message "Please enter an email address."](/zh-CN/docs/Learn/Forms/HTML5_input_types/email_address_invalid.png)

你也可以搭配使用 `multiple` (en-US) 属性，以允许在同一个 `email` 输入框中输入多个电子邮件地址，以英文逗号分隔：

```html
<input type="email" id="email" name="email" multiple />
```

在某些设备上——特别是像智能手机这样具有动态键盘的触摸设备——可能会出现一个包括 `@` 键的更适合于输入电子邮件地址的虚拟键盘。请看下面的 Firefox Android 版键盘截图，以示说明：

![Firefox for Android email keyboard, with the at sign displayed by default.](/zh-CN/docs/Learn/Forms/HTML5_input_types/fx-android-email-type-keyboard.jpg)

**备注：** 你可以在基本 input 例子中找到基本文本 input 类型的例子（也请看看源代码）。

这是使用这些较新的 input 类型的另一个很好的理由——可以为这些设备的用户改善用户体验。

### 客户端验证

正如你在上面看到的，`email` 与其他较新的 `input` 类型一样，提供了内置的*客户端*错误验证，在数据被发送到服务器之前由浏览器执行。它*是*引导用户准确填写表格的一个有用的辅助工具，可以节省时间：可以立即知道你的数据是否正确，而不需要等待服务器返回结果。

但它*不应该*被认为是一种详尽的安全措施！你的应用程序始终应该在*服务器端*和客户端对任何表单提交的数据进行安全检查，因为客户端验证太容易被关闭了，所以恶意用户仍然可以很容易地将坏数据发送到你的服务器。请参阅网站安全来了解*可能*会发生什么。实现服务端认证超出了本章的范围，但你应该熟稔于心。

注意，在默认限制条件下，`a@b` 也是一个合法的电子邮件地址，因为 `email` input 类型默认也允许内部网络的电子邮件地址。为了实现不同的验证行为，你可以使用 `pattern` 属性，而且可以自定义错误信息；我们将在客户端表单认证文章中进一步说明如何使用这些功能。

**备注：** 如果输入的数据不是一个电子邮件地址，会匹配 `:invalid` 伪类，且 `validityState.typeMismatch` (en-US) 属性总会返回 `true`。

查询字段
----

查询字段（Search fields）旨在用于在页面和应用程序上创建搜索框。将 `type` 属性设置为 `search` 就可以使用这种控件：

```html
<input type="search" id="search" name="search" />
```

`text` 字段和 `search` 字段的主要区别是浏览器赋予它们的外观显示。通常情况下，`search` 字段拥有圆角边框，有时会显示“Ⓧ”标志，当点击它时会清除输入框。另外，在动态键盘设备上，键盘的回车键会显示“**search**”，或显示为放大镜图标。

下面的截图显示了 macOS 上的 Firefox 71、Safari 13 和 Chrome 79，以及 Windows 10 上的 Edge 18 和 Chrome 79 的非空查询字段，请注意，清除图标仅当在输入框中存在值得时候才会显示，且与 Safari 不同的是，仅当聚焦状态时才会显示此图标。

![Screenshots of search fields on several platforms.](/zh-CN/docs/Learn/Forms/HTML5_input_types/search_focus.png)

另外一个值得提示的特性是，`search` 字段的值可以自动地保存下来，在同一网站的自动完成框中复用输入，这样的特性倾向于在大多数现代浏览器中自动进行。

电话号码字段
------

在 `type` 属性中使用 `tel` 值，即可创建一个专门用于输入电话号码的文本域：

```html
<input type="tel" id="tel" name="tel" />
```

当使用带有动态键盘的移动设备访问带有 `type="tel"` 的表单时，大多数设备会显示数字键盘。这意味着只要数字键盘有用，这种类型就很有用，而且不只是用于电话号码。

下方截图为在 Android 系统的 Firefox 浏览器上访问该类型表单的键盘：

![Firefox for Android email keyboard, with ampersand displayed by default.](/zh-CN/docs/Learn/Forms/HTML5_input_types/fx-android-tel-type-keyboard.jpg)

由于世界各地的电话号码格式多种多样，这种类型的字段对用户输入的值没有任何限制（这意味着它可能包括字母等非数字值）。

像之前提及的那样，`pattern` 属性也可以应用在这里来附加一些限制，你会在客户端表单验证文章中学到更多内容。

URL 字段
------

在 `type` 属性值中使用 `url`，即可创建一个用于输入网址的文本字段：

```html
<input type="url" id="url" name="url" />
```

它为字段添加了特殊的验证约束。浏览器会在没有协议（例如 `http:`）输入或网址格式不对的情况下报告错误。在具有动态键盘的设备上，键盘通常会显示部分或全部冒号、句号和正斜杠作为默认键。

看看下面这个例子（取自于 Android 上的 Firefox 浏览器)：

![Firefox for Android email keyboard, with ampersand displayed by default.](/zh-CN/docs/Learn/Forms/HTML5_input_types/fx-android-url-type-keyboard.jpg)

**备注：** URL 格式正确可不意味着所指向的地址一定实际存在！

数字字段
----

用于输入数字的控件可以由 `type` 为 `number` 的 `<input>` 元素创建。这个控件外观与文本域类似，但只允许浮点数输入，并通常以旋转器（spinner）的形式提供按钮来增加和减少控件的值。在有动态键盘的设备上，一般会显示数字键盘。

看看下面这个例子（取自于 Android 上的 Firefox 浏览器)：

![Firefox for Android email keyboard, with ampersand displayed by default.](/zh-CN/docs/Learn/Forms/HTML5_input_types/fx-android-number-type-keyboard.jpg)

使用 `number` input 类型，你可以使用 `min` 和 `max` 属性控制允许输入的最小值和最大值。

你也可以使用 `step` 属性来设定每次按下 spinner 按钮增加或减少的值。默认情况下，number input 类型只允许整数值输入，为了允许浮点数输入，要指定 `step="any"` (en-US)。如果省略了此值，`step` 会默认为 `1`，意味着只有自然数是有效的输入。

我们来看一些例子。第一个例子创建了可从 `1` 到 `10` 之间选择值的数字选择器，且单击一次按钮所增长或减少的值为 `2`。

```html
<input type="number" name="age" id="age" min="1" max="10" step="2" />
```

第二个例子创建了可从 `0` 到 `1` 之间选择值得数字选择器，且单击一次按钮所增长或减少的值为 `0.01`。

```html
<input type="number" name="change" id="pennies" min="0" max="1" step="0.01" />
```

当有效值的范围有限时，`number` 输入类型才有意义，例如一个人的年龄或身高。如果范围太大，渐进式增加没有意义（如范围为 `00001` 到 `99999` 的美国 ZIP 码）的话，使用 `tel` 类型可能会更好；它提供了数字键盘，而放弃了数字输入器的 spinner UI 功能。

滑块控件
----

另外一种选择数字的方式是使用**滑块（slider）**。你在买房网站等网站上经常看到这种情况，你想设置一个最高的房产价格来进行过滤。让我们看一个现场示例来说明这一点：

从使用上来说，滑块的准确性不如文本字段。因此，它们被用来挑选*精确值*不一定那么重要的数字。

在 `<input>` 元素中使用 `range` 作为属性 `type` 的值，就可以创建一个滑块，滑块可以通过鼠标、触摸，或用键盘的方向键移动。

正确配置滑块组件非常重要。推荐分别配置 `min` (en-US)、`max` (en-US) 和 `step` (en-US) 属性来设置滑块的最小值、最大值和增量值。

我们来看看示例背后的代码，这样你就可以看到它是如何完成的。首先是基本的 HTML 代码：

```html
<label for="price">Choose a maximum house price: </label>
<input
 type="range"
 name="price"
 id="price"
 min="50000"
 max="500000"
 step="100"
 value="250000" />
<output class="price-output" for="price"></output>
```

本示例创建了一个其取值为 `50000` 到 `500000` 之间的滑块，每次的增量值是 100。我们使用 `value` 属性设定了此滑块的默认值为 `250000`。

使用滑块的一个问题是，它们不提供任何种类的视觉反馈来说明当前的值是什么。这是我们附加了一个包含当前值输出的 `<output>` 元素的原因。你可以在任何元素内显示一个输入值或一个计算的输出值，但是 `<output>` 是特殊的，就像 `<label>` 那样，它可以指定 `for` 属性，允许你将它与输出值来自的一个或多个元素联系起来。

要真正显示当前值，并在其变化时更新，你必须使用 JavaScript，这相对容易做到：

```js
const price = document.querySelector("#price");
const output = document.querySelector(".price-output");

output.textContent = price.value;

price.addEventListener("input", () => {
  output.textContent = price.value;
});
```

这里我们将 `range` 输入元素和 `output` 元素存为了两个变量。然后我们马上将 `output` 的 `textContent` 属性设置为 input 的 `value` 。最终，我们设置了一个事件监听器，确保每次范围滑块移动时，`output` 的 `textContent` 总是可以及时更新为新值。

**备注：** 在 CSS Tricks 网站上有一篇关于该主题的很好的教程：The Output Element。

日期和时间选择器
--------

收集日期和时间值历来是 web 开发者的噩梦。为了获得良好的用户体验，提供一个日历选择用户界面是很重要的，使用户能够选择日期，而不需要切换到本地日历应用程序的上下文，或者可能以难以解析的不同格式输入。上个千年（1000~1999 年）的最后一分钟可以用以下不同的方式表示，例如：1999/12/31，23:59 或 12/31/99T11:59PM。

HTML 日期控件可用于处理这种特定的数据，提供日历控件并使数据统一。

日期和时间控件可由 `<input>` 元素和一个合适的 `type` 属性值来创建，该值取决于要收集的类型（日期、时间、还是以上全部）。这里有一个示例，在浏览器不支持的情况下会自动回退为 `<select>` 元素：

让我们简单地看看不同的可用类型。请注意，这些类型的用法相当复杂，特别是考虑到浏览器的支持（见下文）；要想了解全部细节，请跟随下面的链接进入到每种类型的参考页面，包括详细的例子。

### `datetime-local`

`<input type="datetime-local">` 创建了显示和选择一个没有特定时区信息的日期和时间的控件。

```html
<input type="datetime-local" name="datetime" id="datetime" />
```

### `month`

`<input type="month">` 创建了显示和选择带有年份信息的某个月的控件。

```html
<input type="month" name="month" id="month" />
```

### `time`

`<input type="time">` 创建了显示和选择时间的控件。时间可能会以 *12 小时制*显示，但一定会以 *24 小时制*形式返回。

```html
<input type="time" name="time" id="time" />
```

### `week`

`<input type="week">` 创建了显示和选择一年中特定编号周的控件。

一周以周一开始，一直运行到周日结束。另外，每年的第一周总会包含那一年首个星期四，其中可能不包括当年的第一天，也可能包括前一年的最后几天。

```html
<input type="week" name="week" id="week" />
```

### 限制日期/时间值

所有的日期和时间控件总可以由 `min` (en-US) 和 `max` (en-US) 属性控制，可由 `step` (en-US) 属性进一步做控制，具体值随着 input 类型的不同而产生变化。

```html
<label for="myDate">When are you available this summer?</label>
<input
 type="date"
 name="myDate"
 min="2013-06-01"
 max="2013-08-31"
 step="7"
 id="myDate" />
```

颜色选择器控件
-------

颜色总是有点难处理。有许多方法来表达它们，如 RGB 值（十进制或十六进制）、HSL 值、关键词等。

用于输入颜色的控件可以由 `type` 为 `color` 的 `<input>` 元素创建：

```html
<input type="color" name="color" id="color" />
```

在支持的情况下，点击一个颜色控件将倾向于显示操作系统的默认颜色选择功能，以便你真正做出选择。以下是在 macOS 上 Firefox 浏览器的屏幕截图，提供了一个例子：

![Firefox for Android email keyboard, with ampersand displayed by default.](/zh-CN/docs/Learn/Forms/HTML5_input_types/fx-macos-color.jpg)

这里有一个在线示例供你尝试：

返回值总是颜色的小写的 6 位十六进制数表示。

技能测试！
-----

你已经看完了这篇文章的主要内容，但你还记得所有重要的信息吗？在继续学习其他内容之前，你可以在这里找到进一步的测试来确定你是否掌握了这些知识点——请参见技能测试：HTML5 控件 (en-US)。

总结
--

至此，我们结束了对 HTML5 表单输入类型的考察。还有一些其他的控件类型，由于其行为非常特殊，所以不能轻易地归为一类，但仍有必要了解一下。我们将在下一篇文章中介绍这些类型。

* 上一页
* Overview: Forms
* 下一页
### 进阶内容

* 如何构建表单控件
* 使用 JavaScript 发送表单
* 表单控件兼容性列表


原生表单部件 - 学习 Web 开发

原生表单部件
======

* 上一页
* Overview: Forms
* 下一页

在上一篇文章中，我们标记了一个功能性的 web 表单示例，介绍了一些表单部件和常见的结构元素，并重点介绍了无障碍的最佳实践。现在，我们将详细研究不同表单部件的功能，查看了哪些选项可用于收集不同类型的数据。这个指南有些详尽，涵盖了所有可用的原生表单小部件。

|  |  |
| --- | --- |
| 前提： | 计算机基础知识和对于 HTML 的基本理解。 |
| 目标： | 要了解在浏览器中可以使用什么类型的原生表单小部件来收集数据，以及如何使用 HTML 实现它们。 |

**备注：** widget 在本页面中被统一翻译为部件，但在其他地方可能也被译为组件。

你可能已经遇见过了一些表单元素，包括：`<form>`、`<fieldset>`、`<legend>`、`<textarea>`、`<label>`、`<button>` 和 `<input>`。这篇文章提到了：

* 常见的输入（input）类型元素：button、checkbox、file、hidden、image、password、radio、reset、submit 和 text。
* 所有表单部件共有的一些属性。

**备注：** 本文中讨论的大多数特性都在浏览器中得到了广泛的支持，但并非所有表单部件都受浏览器支持。我们在接下来的两篇文章中提到了 HTML5 带来的新的表单部件。如果你想要更准确的细节，你应该参考我们的 HTML 表单元素参考，特别是我们的种类繁多的 <input> 类型参考。

文本输入框
-----

文本输入框是最基本的表单小部件。这是一种非常方便的方式，可以让用户输入任何类型的数据。我们已经看到过几个简单的例子。

**备注：** HTML 表单文本字段是简单的纯文本输入控件。这意味着你不能将它们用作富文本编辑（粗体、斜体等）。你遇到的所有富文本编辑器都是使用 HTML、CSS 和 JavaScript 所创建的自定义小部件。

所有文本框都有一些通用规范：

* 它们可以被标记为 `readonly`（用户不能修改输入值）甚至是 `disabled`（输入值永远不会与表单数据的其余部分一起发送）。
* 它们可以有一个 `placeholder`；这是文本输入框中出现的文本，用来简略描述输入框的目的。
* 它们可以使用 `size` (en-US)（框的物理尺寸）和 `maxlength` (en-US)（可以输入的最大字符数）进行限制。
* 如果浏览器支持的话，它们可以从拼写检查（使用 `spellcheck` 属性）中获益。

**备注：** `<input>` 元素是如此特别因为它可以通过简单设置 `type` 属性，来接收多种类型的数据。它被用于创建大多数类型的表单小部件，包括单行文本字段、没有文本输入的控件、时间和日期控件和按钮。

### 单行文本框

使用 `type` 属性值被设置为 `text` 的 `<input>` 元素创建一个单行文本框（同样的，如果你不提供 `type` 属性，该属性将被默认设置为 `text`）。在你指定的 `type` 属性的值在浏览器中是未知的情况下（比如你指定 `type="date"`，但是浏览器不支持原生日期选择器），属性值也将会回落到 `text`。

**备注：** 你可以在 Github 上的 single-line-text-fields.html 找到所有单行文本框类型（你也可以直接看预览版）。

这是一个基本的单行文本框示例：

```html
<input type="text" id="comment" name="comment" value="I'm a text field" />
```

单行文本框只有一个真正的约束：如果你输入带有换行符的文本，浏览器会在发送数据之前删除这些换行符。

*以下截图现实了 macOS 上的 Firefox 71 和 Safair 以及 Windows 10 上的 Chrome 79 和 Edge 18 中的默认、聚焦和禁用的文本输入框。*

![Screenshot of the disabled attribute and default :focus styles on a text input in Firefox, Safari, Chrome and Edge.](/zh-CN/docs/Learn/Forms/Basic_native_form_controls/disabled.png)

**备注：** HTML5 通过为 `type` 属性增加特殊值增强了基本单行文本框。这些值仍然将 `<input>` 元素转换为单行文本框，但它们为字段添加了一些额外的约束和特性。例如，URL 和 数值类型的约束，我们将会在下一篇文章中介绍：HTML5 input 类型。

#### 密码框

通过设置 type 属性值为 `password` 来设置该类型框：

```html
<input type="password" id="pwd" name="pwd" />
```

它不会为输入的文本添加任何特殊的约束，但是它会模糊输入到字段中的值（例如，用点或星号），这样它就不能被其他人读取。

请记住，这只是一个用户界面特性；除非你安全地提交你的表单，否则它会以明文发送，这不利于安全——恶意的一方可能会截获你的数据，窃取你的密码、信用卡信息，或者你提交的其他任何东西。保护用户不受此影响的最佳方式是在安全连接上托管任何涉及表单的页面（例如：`https://`……地址），使得数据在发送之前就已加密。

现代浏览器认识到在不安全的连接上发送表单数据所带来的安全影响，并且已经实现了警告，以阻止用户使用不安全的表单。有关 Firefox 实现的更多信息，请参见不安全的密码 (en-US)。

### 隐藏内容

另一个原生的文本框控件是 `hidden` input 类型。它被用于创建对用户不可见的表单部件，但在发送表单时，会与其他的表单数据一起被发送到服务器——例如，你可能希望向服务器提交一个时间戳，说明订单是何时产生的。因为它是隐藏的，所以用户看不到也不能简单地修改该值，它将永远不会获得焦点，屏幕阅读器也不会注意到它。

```html
<input type="hidden" id="timestamp" name="timestamp" value="1286705410" />
```

如果你创建了这样一个元素，就需要设置它的 `name` 和 `value` 属性。元素的值可以通过 JavaScript 动态设置。`hidden` input 类型不应有关联的标签（`label` 元素）。

HTML5 中添加了其他的文本输入类型，如：search、url 和 tel。这些内容将在下一篇文章 HTML5 input 类型中介绍。

可选中项：复选框和单选按钮
-------------

可选中项是可以通过单击它们来更改状态的小部件。有两种可选中项：复选框和单选按钮。两者都使用 `checked` 属性，以指示该部件的默认状态：“选中”或“未选中”。

值得注意的是，这些小部件与其他表单小部件不一样。对于大多数表单部件，一旦表单提交，所有具有 `name` 属性的小部件都会被发送，即使没有任何值被填。对于可选中项，只有在勾选时才发送它们的值。如果他们没有被勾选，就不会发送任何东西，甚至连他们的名字也没有。而在它们被勾选且没有提供值（value）时，将会发送名字和缺省值 `on`。

**备注：** 你可以在 Github 上看到 checkable-items.html（你也可以直接看预览版）。

为了获得最大的可用性和无障碍（无障碍），建议你在 `<fieldset>` 中包围每个相关项目的列表，并使用 `<legend>` 提供对列表的全面描述。每个单独的 `<label>`/`<input>` 元素都应该包含在它自己的列表项中（或者类似的）。正如在下方示例中显示的。

### 复选框

使用 `type` 属性值为 `checkbox` 的 `<input>` 元素来创建一个复选框。

```html
<input type="checkbox" id="questionOne" name="subscribe" value="yes" checked />
```

相关的复选框元素应该使用具有相同值的 `name` 属性。包含 `checked` 属性使复选框在页面加载时自动被选中。点击复选框或其相关联的标签也将自动翻转复选框的状态（选中、取消选中）

```html
<fieldset>
  <legend>Choose all the vegetables you like to eat</legend>
  <ul>
    <li>
      <label for="carrots">Carrots</label>
      <input
 type="checkbox"
 id="carrots"
 name="vegetable"
 value="carrots"
 checked />
    </li>
    <li>
      <label for="peas">Peas</label>
      <input type="checkbox" id="peas" name="vegetable" value="peas" />
    </li>
    <li>
      <label for="cabbage">Cabbage</label>
      <input type="checkbox" id="cabbage" name="vegetable" value="cabbage" />
    </li>
  </ul>
</fieldset>
```

*以下截图显示了 macOS 上的 Firefox 71 和 Safair 13 以及 Windows 10 上的 Chrome 79 和 Edge 18 中的默认、聚焦和禁用的复选框。*

![Default, focused and disabled Checkboxes in Firefox 71 and Safari 13 on Mac and Chrome 79 and Edge 18 on Windows 10](/zh-CN/docs/Learn/Forms/Basic_native_form_controls/checkboxes.png)

**备注：** 任何带有 `checked` 属性的复选框和单选按钮在加载时都会匹配 `:default` 伪类，即使它们后面不再被选中。任何当前被选中的元素，都会匹配 `:checked` 伪类。

由于复选框的开关特性，其被认为是一种切换按钮，许多开发者和设计人员覆盖了默认的复选框样式，以使其看起来像是一个切换开关的按钮。你可以在这里看到一个示例（参见源码）。

### 单选按钮

使用 `type` 属性值为 `radio` 的 `<input>` 元素来创建一个单选按钮。

```html
<input type="radio" id="soup" name="meal" checked />
```

几个单选按钮可以连接在一起。如果它们的 `name` 属性共享相同的值，那么它们将被认为属于同一组的按钮。同一组中只有一个按钮可以同时被选；这意味着当其中一个被选中时，所有其他的都将自动未选中。如果没有选中任何一个，那么整个单选按钮池就被认为处于未知状态，并且没有以表单的形式发送任何值。

```html
<fieldset>
  <legend>What is your favorite meal?</legend>
  <ul>
    <li>
      <label for="soup">Soup</label>
      <input type="radio" id="soup" name="meal" value="soup" checked />
    </li>
    <li>
      <label for="curry">Curry</label>
      <input type="radio" id="curry" name="meal" value="curry" />
    </li>
    <li>
      <label for="pizza">Pizza</label>
      <input type="radio" id="pizza" name="meal" value="pizza" />
    </li>
  </ul>
</fieldset>
```

以下的截图展示了 macOS 上的 Firefox 71 和 Safari 13 以及 Windows 10 上的 Chrome 79 和 Edge 18 中选中和未选中状态、聚焦、禁用以及被选中的单选按钮。

![Radio buttons on Firefox 71 and Safari 13 on Mac and Chrome 79 and Edge 18 on Windows 10](/zh-CN/docs/Learn/Forms/Basic_native_form_controls/radios.png)

按钮
--

在 HTML 表单中，有三种按钮：

`Submit`

将表单数据发送到服务器。对于 `<button>` 元素，省略 `type` 属性（或是一个无效的 `type` 值）的结果就是一个提交按钮。

`Reset`

将所有表单小部件重新设置为它们的默认值。

`Anonymous`

没有自动生效的按钮，但是可以使用 JavaScript 代码进行定制。

我们也可以使用 `<button>` 元素。它也带有 `submit`、`reset` 和 `button` 这几个 `type` 属性，与 `<input>` 的三种按钮行为一致。它们之间的主要区别在于 `<button>` 元素更易于设置样式。

**备注：** `image` input 类型同样被渲染为一个按钮。我们将在后面提到。

**备注：** 你可以在 Github 上看到 button-examples.html（你也可以直接看预览版）。

下面，你可以看到每一种与 `<button>` 对应类型等价的 `<input>` 类型的示例。

### submit

```html
<button type="submit">This is a <strong>submit button</strong></button>

<input type="submit" value="This is a submit button" />
```

### reset

```html
<button type="reset">This is a <strong>reset button</strong></button>

<input type="reset" value="This is a reset button" />
```

### anonymous

```html
<button type="button">This is an <strong>anonymous button</strong></button>

<input type="button" value="This is an anonymous button" />
```

不管你使用的是 `<button>` 元素还是 `<input>` 元素，按钮的行为都是一样的。然而，有一些显著的不同之处：从示例中可以看到，`<button>` 元素允许你在它们的标签中使用 HTML 内容，这些内容被插入到 `<button>` 开始和结束的标签之间。另一方面，`<input>` 元素是空元素；它显示的内容需要插入到 `value` 属性中，因此只接受纯文本内容。

以下的截图展示了 macOS 上的 Firefox 71 和 Safari 13 以及 Windows 10 上的 Chrome 79 和 Edge 18 中默认、聚焦、禁用的 input 按钮。

![Default, focused and disabled button input types in Firefox 71 and Safari 13 on Mac and Chrome 79 and Edge 18 on Windows 10](/zh-CN/docs/Learn/Forms/Basic_native_form_controls/buttons.png)

从技术上讲，使用`<button>`元素或`<input>`元素定义的按钮几乎没有区别。唯一值得注意的区别是按钮本身的标签。在`<input>`元素中，标签只能是字符数据，而在`<button>`元素中，标签可以是 HTML，因此可以相应地进行样式化。

### 图像按钮

**图像按钮**（image button）控件渲染的方式与 `<img>` 几乎完全相同。只是在用户点击它时，图像按钮的行为与提交（submit）按钮相同。

图像按钮是使用 `type` 属性值设置为 `image` 的 `<input>` 元素创建的。这个元素支持与 `<img>` 元素相同的属性，和其他表单按钮支持的所有属性。

```html
<input type="image" alt="Click me!" src="my-img.png" width="80" height="30" />
```

如果使用图像按钮来提交表单，这个小部件不会提交它的值；而是提交在图像上单击处的 X 和 Y 坐标（坐标是相对于图像的，这意味着图像的左上角表示坐标 (0, 0)），坐标被发送为两个键/值对：

* X 值键是 `name` 属性的值，后面是字符串“.x”。
* Y 值键是 `name` 属性的值，后面是字符串“.y”。

例如，当你点击这个小部件图像坐标为 (123,456) 的位置时，它将会通过 `get` 方法提交。你可以看到类似的 URL：
```

http://foo.com?pos.x=123&pos.y=456
```

这是构建“热图”的一种非常方便的方式。如何发送和检索这些值在发送和检索表单数据文章中详细说明。

文件选择器
-----

HTML 表单能够将文件发送到服务器；在发送和检索表单数据的文章中详细描述了这个特定的操作。文件选择器小部件是用户如何选择一个或多个文件来发送的。

要创建一个文件选择器小部件，你可以使用 `<input>` 元素，将它的 `type` 属性设置为 `file`。被接受的文件类型可以使用 `accept` 属性来约束。此外，如果你想让用户选择多个文件，那么可以通过添加 `multiple` 属性来实现。

#### 示例

在本例中，创建一个文件选择器，请求图形图像文件。在本例中，允许用户选择多个文件。

```html
<input type="file" name="file" id="file" accept="image/\*" multiple />
```

在一些移动终端上，文件选择器可以访问由设备相机和麦克风直接获取的图片、视频、音频。我们只需要这样设置 `accept` 属性即可（分别对应相机捕获的图片、视频和麦克风获取的音频）：

```html
<input type="file" accept="image/\*;capture=camera" />
<input type="file" accept="video/\*;capture=camcorder" />
<input type="file" accept="audio/\*;capture=microphone" />
```

通用属性
----

大部分用来定义表单小部件的元素都有一些他们自己的属性。然而，在所有表单元素中都有一组通用属性，它们可以对这些小部件进行控制。下面是这些通用属性的列表：

| 属性名称 | 默认值 | 描述 |
| --- | --- | --- |
| `autofocus` | false | 这个布尔属性允许你指定当页面加载时元素应该自动具有输入焦点，除非用户覆盖它，例如通过键入不同的控件。文档中只有一个与表单相关的元素可以指定这个属性。 |
| `disabled` | false | 这个布尔属性表示用户不能与元素交互。如果没有指定这个属性，元素将从包含它的元素继承设置，例如 `<fieldset>`；如果没有包含在设定了 `disabled` 属性的元素里，那么这个元素就是可用的。 |
| `form` |  | 小部件与之相关联的表单元素。属性值必需是同个文档中的 `<form>` 元素的 `id` 属性。理论上，它允许你在 `<form>` 元素之外设置一个表单小部件。 |
| `name` |  | 元素的名称；这是跟表单数据一起提交的。 |
| `value` |  | 元素的初始值。 |

技能测试！
-----

你已经看完了这篇文章的主要内容，但你还记得所有重要的信息吗？在继续学习其他内容之前，你可以在这里找到进一步的测试来确定你是否掌握了这些知识点——请参见技能测试：基础控件 (en-US)。

总结
--

这篇文章介绍了较老的输入类型——在早期的 HTML 中引入的原始输入类型，它们在所有的浏览器中都得到了很好的支持。在下一节中，我们将介绍 HTML5 中新添加的 `type` 属性。

* 上一页
* Overview: Forms
* 下一页
### 高级技巧

* 如何构建表单控件
* 使用 JavaScript 发送表单
* 表单部件兼容性列表


创建我的第一个表单 - 学习 Web 开发

创建我的第一个表单
=========

* Overview: Forms
* 下一页

本系列的一篇文章为你提供了第一次创建 web 表单的经验，包括设计一个简单表单、使用正确的 HTML 表单控件和其他 HTML 元素实现它、通过 CSS 添加一些非常简单的样式，以及如何将数据发送到服务器。稍后我们将在这一章节中更详细地扩展这些主题。

|  |  |
| --- | --- |
| 前提： | 基本计算机素养和对 HTML 的基本理解。 |
| 目标： |
 熟悉 web
 表单是什么、它们的用途、如何设计它们，以及简单情况下需要的基本
 HTML 元素。
  |

web 表单是什么？
----------

web 表单是用户和 web 站点或应用程序之间交互的主要内容之一。它们允许用户输入数据，大多数情况下会将数据发送到 web 服务器进行处理和存储（见后面的章节发送表单数据），或者在客户端使用某种方式立刻更新界面（例如，添加列表中的另一个项目，或者显示或隐藏 UI 功能）。

web 表单是由一个或多个**表单控件**（有时称为**小部件**），以及一些有助于构建整个表单的附加元素组成——通常被称为 **HTML 表单**。这些控件可以是文本字段（单行或多行）、选择框、按钮、复选框或单选按钮，大部分是使用 `<input>` 元素创建的，尽管还有一些其他元素需要学习。

还可对表单控件进行编程，以强制指定输入的格式和值（**表单验证**），并与文本标签配对，向有视力障碍的用户描述其用途。

设计表单
----

在开始编写代码之前，最好先退一步，花点时间考虑一下你的表单。设计一个快速的模型将帮助你定义你想要询问用户的正确的数据集。从用户体验（UX）的角度来看，要记住：表单越大，失去用户的风险就越大。保持简单，保持专注：只要求必要的数据。

在构建站点或应用程序时，设计表单是非常重要的一步。这超出了本文的范围，涵盖了表单的用户体验，但是如果你想深入了解这个主题，你应该阅读下面的文章：

* 杂志《Smashing Magazine》中有很好的关于表单用户体验的文章，或许其中最重要的应该是它们的 Extensive Guide To Web Form Usability。
* UXMatters 也是一个非常周到的资源，从基本的最佳实践到复杂的问题如多页表单，都有很好的建议。

在本文中，我们将构建一个简单的联系人表单。让我们做一个粗略的草图。

![The form to build, roughly sketch](/zh-CN/docs/Learn/Forms/Your_first_form/form-sketch-low.jpg)

我们的表单将包含三个文本字段和一个按钮。我们向用户询问他们的姓名、电子邮件和他们想要发送的信息。点击这个按钮将把他们的数据发送到一个 web 服务器。

主动学习：使用 HTML 实现我们的表单
--------------------

好了，现在我们准备进入 HTML 代码并对表单进行编码。为了构建我们的联系人表单，我们将使用以下 HTML 元素：`<form>`、`<label>`、`<input>`、`<textarea>` 和 `<button>`。

在进一步讨论之前，先创建一个简单 HTML 模板的本地副本——你将在这里输入表单的 HTML。

### `<form>` 元素

所有表单都以一个 `<form>` 元素开始：

```html
<form action="/my-handling-form-page" method="post"></form>
```

这个元素正式定义了一个表单。就像 `<section>` 元素或 `<footer>` 元素，它是一个容器元素，但它也支持一些特定的属性来配置表单的行为方式。它的所有属性都是可选的，但实践中最好至少要设置 `action` 属性和 `method` 属性。

* `action` 属性定义了在提交表单时，应该把所收集的数据送给谁（URL）去处理。
* `method` 属性定义了发送数据的 HTTP 方法（通常是 `get` 或 `post`）。

**备注：** 如果你想深入了解这些属性是如何工作的，那么将在发送表单数据文章中详细说明。

现在，将上面的 `<form>` 元素添加到你的 HTML `<body>` 中。

### `<label>`、`<input>` 和 `<textarea>` 元素

我们的联系人表单非常简单，包含三个文本字段，每个字段都有一个标签。

* 名称的输入字段将是一个基本的单行文本字段。
* 电子邮件的输入字段将是一个只接受电子邮件地址的单行文本字段
* 消息的输入字段将是一个 `<textarea>`——基本的多行文本字段。

就 HTML 代码而言，我们需要如下的东西来实现这些表单小部件：

```html
<form action="/my-handling-form-page" method="post">
  <ul>
    <li>
      <label for="name">Name:</label>
      <input type="text" id="name" name="user\_name" />
    </li>
    <li>
      <label for="mail">E-mail:</label>
      <input type="email" id="mail" name="user\_email" />
    </li>
    <li>
      <label for="msg">Message:</label>
      <textarea id="msg" name="user\_message"></textarea>
    </li>
  </ul>
</form>
```

更新你的表单代码，使其看起来像上面的代码。

使用 `<li>` 元素可以使我们更加方便地构造我们自己的代码，并且更容易样式化（参见本文后面的内容）。为了可用性和无障碍，我们为每个表单控件包含一个明确的标签。注意在所有 `<label>` 元素上使用 `for` (en-US) 属性；它是将标签链接到表单控件的一种正规方式。这个属性引用对应的表单控件的 `id`。

这样做有一些好处。最明显的一个好处是允许用户单击标签以激活相应的表单控件，它还有助于使用屏幕阅读器的用户读取名称。如果你想更好地理解这个属性的其他好处，你可以找到如何构造 web 表单的详细信息。

在 `<input>` 元素中，最重要的属性是 `type` 属性。这个属性非常重要，因为它定义了 `<input>` 属性的行为方式。稍后你将在原生表单控件文章中找到更多关于此的内容。

* 在我们的简单示例中，我们使用值 text 作为第一个输入——这个属性的默认值。它表示一个基本的单行文本字段，接受任何类型的文本输入。
* 对于第二个输入，我们使用值 email，它定义了一个只接受格式正确的电子邮件地址的单行文本字段。这会将一个基本的文本字段转换为一种“智能”字段，该字段将对用户输入的数据进行一些检查。在稍后的表单数据验证文章中，你将了解到更多关于客户端表单验证的信息。

最后但同样重要的是，要注意 `<input>` 和 `<textarea></textarea>` 的语法。这是 HTML 的一个奇怪之处。`<input>` 标签是一个空元素，这意味着它不需要关闭标签。相反，`<textarea>` 不是一个空元素，因此必须使用适当的结束标记来关闭它。这对 HTML 表单的特定特性有影响：定义默认值的方式。要定义 `<input>` 的默认值，你必须使用 `value` 属性，如下所示：

```html
<input type="text" value="by default this element is filled with this text" />
```

相反，如果你想定义 `<textarea>` 的默认值，你只需在 `<textarea>` 元素的开始和结束标记之间放置默认值，就像这样：

```html
<textarea>
by default this element is filled with this text
</textarea>
```

### `<button>` 元素

我们的表格已经快准备好了，我们只需要再添加一个按钮，让用户在填写完表单后发送他们的数据。这是通过使用 `<button>` 元素完成的。在 `</ul>` 这个结束标签上方添加以下内容：

```html
<li class="button">
  <button type="submit">Send your message</button>
</li>
```

你会看到 `<button>` 元素也接受一个 `type`属性，它接受 `submit`、`reset` 或者 `button` 三个值中的任一个。

* 单击 `type` 属性定义为 `submit` 值（也是默认值）的按钮会发送表单的数据到 `<form>` 元素的 `action` 属性所定义的网页。
* 单击 `type` 属性定义为 `reset` 值的按钮 将所有表单小部件重新设置为它们的默认值。从用户体验的角度来看，这被认为是一种糟糕的做法。
* 单击 `type` 属性定义为 `button` 值的按钮，不会发生任何事！这听起来很傻，但是用 JavaScript 构建定制按钮非常有用。

**备注：** 你还可以使用相应类型的 `<input>` 元素来生成一个按钮，如 `<input type="submit">`。`<button>` 元素的主要优点是，`<input>` 元素只允许纯文本作为其标签，而 `<button>` 元素允许完整的 HTML 内容，允许更复杂、更有创意的按钮内容。

基本表单样式
------

现在你已经完成了表单的 HTML 代码，尝试保存它并在浏览器中查看它。现在，你会看到它看起来很丑。

**备注：** 如果你怀疑你的 HTML 代码不对，试着把它和我们完成的例子进行比较——first-form.html（你也可以查看预览版）。

如何排布好表单是公认的难点。这超出了本文的讨论范围，所以现在我们只需要让你添加一些 CSS 来让它看起来很好。

首先，在你的 HTML 头部中添加一个 `<style>`元素。应该是这样的：

```html
<style></style>
```

在 `style` 标签中，添加如下的 CSS：

```css
form {
  /\* 居中表单 \*/
  margin: 0 auto;
  width: 400px;
  /\* 显示表单的轮廓 \*/
  padding: 1em;
  border: 1px solid #ccc;
  border-radius: 1em;
}

ul {
  list-style: none;
  padding: 0;
  margin: 0;
}

form li + li {
  margin-top: 1em;
}

label {
  /\* 确保所有 label 大小相同并正确对齐 \*/
  display: inline-block;
  width: 90px;
  text-align: right;
}

input,
textarea {
  /\* 确保所有文本输入框字体相同
 textarea 默认是等宽字体 \*/
  font: 1em sans-serif;

  /\* 使所有文本输入框大小相同 \*/
  width: 300px;
  box-sizing: border-box;

  /\* 调整文本输入框的边框样式 \*/
  border: 1px solid #999;
}

input:focus,
textarea:focus {
  /\* 给激活的元素一点高亮效果 \*/
  border-color: #000;
}

textarea {
  /\* 使多行文本输入框和它们的 label 正确对齐 \*/
  vertical-align: top;

  /\* 给文本留下足够的空间 \*/
  height: 5em;
}

.button {
  /\* 把按钮放到和文本输入框一样的位置 \*/
  padding-left: 90px; /\* 和 label 的大小一样 \*/
}

button {
  /\* 这个外边距的大小与 label 和文本输入框之间的间距差不多 \*/
  margin-left: 0.5em;
}
```

保存并重新加载，现在它看起来没那么丑了。

**备注：** 你可以在 GitHub 上的这里找到这个版本 first-form-styled.html（也可以在这儿看在线演示）。

向你的 web 服务器发送表单数据
-----------------

最后一部分，也许是最棘手的部分，是在服务器端处理表单数据。`<form>` 元素通过 `action` 属性和 `method` 属性来定义发送数据的位置和方式。

但这还不够。我们需要为每个表单控件提供 `name` 属性。这些名称对客户端和服务端都很重要：在浏览器端，它告诉浏览器给每个数据的名称，在服务器端，它允许服务器按名称处理每个数据块。表单数据以**名称/值对**的形式发送到服务器。

要命名表单中的数据，你需要在每个表单小部件上使用 `name` 属性来收集特定的数据块。让我们再来看看我们的表单代码：

```html
<form action="/my-handling-form-page" method="post">
  <ul>
    <li>
      <label for="name">Name:</label>
      <input type="text" id="name" name="user\_name" />
    </li>
    <li>
      <label for="mail">E-mail:</label>
      <input type="email" id="mail" name="user\_email" />
    </li>
    <li>
      <label for="msg">Message:</label>
      <textarea id="msg" name="user\_message"></textarea>
    </li>

    …
  </ul>
</form>
```

在我们的示例中，表单会发送三个已命名的数据块“`user_name`”、“`user_email`”和“`user_message`”。这些数据将用使用 HTTP `POST` 方法，把信息发送到 URL 为“`/my-handling-form-page`”的目录下。

在服务器端，位于 URL“`/my-handling-form-page`”上的脚本将接收的数据作为 HTTP 请求中包含的 3 个键/值项的列表。这个脚本处理这些数据的方式取决于你。每个服务器端语言（PHP、Python、Ruby、Java、c 等等）都有自己的表单数据处理机制。深入到这个主题已经超出了本指南的范围，但是如果你想了解更多，我们已经在发送表单数据文章中提供了一些示例。

总结
--

祝贺你，你已经构建了你的第一个 HTML 表单。它看起来就像这样：
```

<form action="/my-handling-form-page" method="post">
  <div>
    <label for="name">Name:</label>
    <input type="text" id="name" name="user\_name" />
  </div>

  <div>
    <label for="mail">E-mail:</label>
    <input type="email" id="mail" name="user\_email" />
  </div>

  <div>
    <label for="msg">Message:</label>
    <textarea id="msg" name="user\_message"></textarea>
  </div>

  <div class="button">
    <button type="submit">Send your message</button>
  </div>
</form>
```

```
form {
  /\* Just to center the form on the page \*/
  margin: 0 auto;
  width: 400px;

  /\* To see the limits of the form \*/
  padding: 1em;
  border: 1px solid #ccc;
  border-radius: 1em;
}

div + div {
  margin-top: 1em;
}

label {
  /\* To make sure that all label have the same size and are properly align \*/
  display: inline-block;
  width: 90px;
  text-align: right;
}

input,
textarea {
  /\* To make sure that all text field have the same font settings
 By default, textarea are set with a monospace font \*/
  font: 1em sans-serif;

  /\* To give the same size to all text field \*/
  width: 300px;

  -moz-box-sizing: border-box;
  box-sizing: border-box;

  /\* To harmonize the look & feel of text field border \*/
  border: 1px solid #999;
}

input:focus,
textarea:focus {
  /\* To give a little highlight on active elements \*/
  border-color: #000;
}

textarea {
  /\* To properly align multiline text field with their label \*/
  vertical-align: top;

  /\* To give enough room to type some text \*/
  height: 5em;

  /\* To allow users to resize any textarea vertically
 It works only on Chrome, Firefox and Safari \*/
  resize: vertical;
}

.button {
  /\* To position the buttons to the same position of the text fields \*/
  padding-left: 90px; /\* same size as the label elements \*/
}

button {
  /\* This extra margin represent the same space as the space between
 the labels and their text fields \*/
  margin-left: 0.5em;
}
```

然而，这仅仅是开始，现在是时候深入研究了。表单比我们在这里看到的要强大得多，本指南的其他文章将帮助你掌握其余部分。

* Overview: Forms
* 下一页
### 高级主题

* 如何构建自定义表单控件
* 通过 JavaScript 发送表单
* 表单控件的属性兼容性列表


表单控件兼容性列表 - 学习 Web 开发

表单控件兼容性列表
=========

下面的兼容性表格尝试总结 HTML 表单的 CSS 支持状况。由于 CSS 和 HTML 表单的复杂性，不能把这些表格当作完善的参考。但是，它们可以让你很好地洞察什么能做什么不能做，这将会对你学习使用有很好地帮助。

如何阅读表格
------

### 值

对于每个属性，有四种可能地取值：

✅ 支持

此属性具有相当一致的跨浏览器支持。在某些极端情况下，你可能仍然会面临奇怪的副作用。

⚠ 部分支持

尽管这个属性会生效，你还是会经常面对奇怪的副作用和不一致性。你应该尽力避免这些属性，除非你已经深知那些副作用。

❌ 不支持

此属性就是不工作或者表现得非常不一致，所以并不可靠。

不适用

此属性对这种类型的组件没有意义。

### 渲染

对于每个属性有两种可能的渲染方式：

N (Normal)

表示这个属性会像设置的那样应用。

T (Tweaked)

表示这个属性需要通过下列的额外规则来使用：

```css
\* {
  /\* Turn off the native look and feel \*/
  -webkit-appearance: none;
  appearance: none;

  /\* for Internet Explorer \*/
  background: none;
}
```

兼容性表格
-----

### 全局行为

对许多浏览器来说，许多行为在全局范围内都是通用的：

`border`、`background`、`border-radius`、`height`

在某些浏览器中，这里的任意一个属性可能影响组件的部分或全部的原生外观。小心使用。

`line-height`

不同浏览器支持情况不同，避免使用。

`text-decoration`

Opera 的表单组件不支持这个属性。

`text-overflow`

Opera、Safari 和 IE9 的表单组件不支持这个属性。

`text-shadow`

Opera 的表单组件不支持 `text-shadow`，IE9 则是整个表单都不支持这个属性。

### 文本字段

参见 `text`、`search` 和 `password` input 类型。

| 属性 | N | T | 注释 |
| --- | --- | --- | --- |
| *CSS 盒子模型* |
| --- |
| `width` | ✅ 支持 | ✅ 支持 |  |
| `height` | ⚠ 部分支持[1][2] | ✅ 支持 | 1. WebKit 浏览器（主要在 Mac OSX 和 iOS 上）的搜索字段使用原生的样式和行为。因此，需要使用
 `-webkit-appearance:none` 才能将这个属性应用到搜索字段上。
2. Windows 7 的 Internet Explorer 9 不会应用到边框上，除非应用了
 `background:none`。
 |
| `border` | ⚠ 部分支持[1][2] | ✅ 支持 | 1. WebKit 浏览器（主要在 Mac OSX 和 iOS 上）的搜索字段使用原生的样式和行为。因此，需要使用
 `-webkit-appearance:none` 才能将这个属性应用到搜索字段上。
2. Windows 7 的 Internet Explorer 9 不会应用到边框上，除非应用了
 `background:none`。
 |
| `margin` | ✅ 支持 | ✅ 支持 |  |
| `padding` | ⚠ 部分支持[1][2] | ✅ 支持 | 1. WebKit 浏览器（主要在 Mac OSX 和 iOS 上）的搜索字段使用原生的样式和行为。因此，需要使用
 `-webkit-appearance:none` 才能将这个属性应用到搜索字段上。
2. Windows 7 的 Internet Explorer 9 不会应用到边框上，除非应用了
 `background:none`。
 |
| *文本和字体* |
| --- |
| `color`[1] | ✅ 支持 | ✅ 支持 | 1. 如果没有设置 `border-color` 属性，一些基于 WebKit 的浏览器会将
 `color` 属性应用到边框上，颜色和
 ``<textarea>`` 的字体颜色一样。
 |
| `font` | ✅ 支持 | ✅ 支持 | 查看有关 `line-height` 的注释 |
| `letter-spacing` | ✅ 支持 | ✅ 支持 |  |
| `text-align` | ✅ 支持 | ✅ 支持 |  |
| `text-decoration` | ⚠ 部分支持 | ⚠ 部分支持 | 查看有关 Opera 的注释 |
| `text-indent` | ⚠ 部分支持[1] | ⚠ 部分支持[1] | 1. IE9 只在 ``<textarea>``
 上支持这个属性，而 Opera 只在单行文本字段中支持。
 |
| `text-overflow` | ⚠ 部分支持 | ⚠ 部分支持 |  |
| `text-shadow` | ⚠ 部分支持 | ⚠ 部分支持 |  |
| `text-transform` | ✅ 支持 | ✅ 支持 |  |
| *边框和背景* |
| --- |
| `background` | ⚠ 部分支持[1] | ✅ 支持 | 1. WebKit 浏览器（主要在 Mac OSX 和 iOS 上）的搜索字段使用原生的样式和行为。因此，需要使用
 `-webkit-appearance:none` 才能将这个属性应用到搜索字段上。
2. Windows 7 的 Internet Explorer 9 不会应用到边框上，除非应用了
 `background:none`。
 |
| `border-radius` | ⚠ 部分支持[1][2] | ✅ 支持 | 1. WebKit 浏览器（主要在 Mac OSX 和 iOS 上）的搜索字段使用原生的样式和行为。因此，需要使用
 `-webkit-appearance:none` 才能将这个属性应用到搜索字段上。
2. Windows 7 的 Internet Explorer 9 不会应用到边框上，除非应用了
 `background:none`。
3. 在 Opera 上，只有当边框被明确设定时，`border-radius` 属性才会被应用。
 |
| `box-shadow` | ❌ 不支持 | ⚠ 部分支持[1] | 1. IE9 不支持这个属性。
 |

### 按钮

参见 `button`、`submit` 和 `reset` input 类型，以及 ``<button>`` 元素。

| 属性 | N | T | 注释 |
| --- | --- | --- | --- |
| *CSS 盒子模型* |
| --- |
| `width` | ✅ 支持 | ✅ 支持 |  |
| `height` | ⚠ 部分支持[1] | ✅ 支持 | 1. 这个属性不能应用于 Mac OSX 或 iOS 上基于 WebKit 的浏览器。
 |
| `border` | ⚠ 部分支持 | ✅ 支持 |  |
| `margin` | ✅ 支持 | ✅ 支持 |  |
| `padding` | ⚠ 部分支持[1] | ✅ 支持 | 1. 这个属性不能应用于 Mac OSX 或 iOS 上基于 WebKit 的浏览器。
 |
| *文本和字体* |
| --- |
| `color` | ✅ 支持 | ✅ 支持 |  |
| `font` | ✅ 支持 | ✅ 支持 | 查看 `line-height` 的注释。 |
| `letter-spacing` | ✅ 支持 | ✅ 支持 |  |
| `text-align` | ❌ 不支持 | ❌ 不支持 |  |
| `text-decoration` | ⚠ 部分支持 | ✅ 支持 |  |
| `text-indent` | ✅ 支持 | ✅ 支持 |  |
| `text-overflow` | ❌ 不支持 | ❌ 不支持 |  |
| `text-shadow` | ⚠ 部分支持 | ⚠ 部分支持 |  |
| `text-transform` | ✅ 支持 | ✅ 支持 |  |
| *边框和背景* |
| --- |
| `background` | ✅ 支持 | ✅ 支持 |  |
| `border-radius` | ✅ 支持[1] | ✅ 支持[1] | 1. 在 Opera 上，只有当边框被明确设定时，`border-radius` 属性才会被应用。
 |
| `box-shadow` | ❌ 不支持 | ⚠ 部分支持[1] | 1. IE9 不支持这个属性。
 |

### 数字

参见 `number` input 类型。没有一种标准的方式改变数字字段的样式，值得注意的是 Safari 上的数字字段不在这个范围内。

| 属性 | N | T | 注释 |
| --- | --- | --- | --- |
| *CSS 盒子模型* |
| --- |
| `width` | ✅ 支持 | ✅ 支持 |  |
| `height` | ⚠ 部分支持[1] | ⚠ 部分支持[1] | 1. 在 Opera 上，数字选择器缩小时，可能会隐藏字段中的内容。
 |
| `border` | ✅ 支持 | ✅ 支持 |  |
| `margin` | ✅ 支持 | ✅ 支持 |  |
| `padding` | ⚠ 部分支持[1] | ⚠ 部分支持[1] | 1. 在 Opera 上，数字选择器缩小时，可能会隐藏字段中的内容。
 |
| *文本和字体* |
| --- |
| `color` | ✅ 支持 | ✅ 支持 |  |
| `font` | ✅ 支持 | ✅ 支持 | 查看有关 `line-height` 的注释。 |
| `letter-spacing` | ✅ 支持 | ✅ 支持 |  |
| `text-align` | ✅ 支持 | ✅ 支持 |  |
| `text-decoration` | ⚠ 部分支持 | ⚠ 部分支持 |  |
| `text-indent` | ✅ 支持 | ✅ 支持 |  |
| `text-overflow` | ❌ 不支持 | ❌ 不支持 |  |
| `text-shadow` | ⚠ 部分支持 | ⚠ 部分支持 |  |
| `text-transform` | 不适用 | 不适用 |  |
| *边框和背景* |
| --- |
| `background` | ❌ 不支持 | ❌ 不支持 | 支持，但浏览器之间的不一致性太多，所以并不可靠。 |
| `border-radius` | ❌ 不支持 | ❌ 不支持 |
| `box-shadow` | ❌ 不支持 | ❌ 不支持 |

### 复选框和单选按钮

参见 `checkbox` 和 `radio` input 类型。

| 属性 | N | T | 注释 |
| --- | --- | --- | --- |
| *CSS 盒子模型* |
| --- |
| `width` | ❌ 不支持[1] | ❌ 不支持[1] | 1. 一些浏览器会添加额外的边缘，另一些会拉伸组件。
 |
| `height` | ❌ 不支持[1] | ❌ 不支持[1] | 1. 一些浏览器会添加额外的边缘，另一些会拉伸组件。
 |
| `border` | ❌ 不支持 | ❌ 不支持 |  |
| `margin` | ✅ 支持 | ✅ 支持 |  |
| `padding` | ❌ 不支持 | ❌ 不支持 |  |
| *文本和字体* |
| --- |
| `color` | 不适用 | 不适用 |  |
| `font` | 不适用 | 不适用 |  |
| `letter-spacing` | 不适用 | 不适用 |  |
| `text-align` | 不适用 | 不适用 |  |
| `text-decoration` | 不适用 | 不适用 |  |
| `text-indent` | 不适用 | 不适用 |  |
| `text-overflow` | 不适用 | 不适用 |  |
| `text-shadow` | 不适用 | 不适用 |  |
| `text-transform` | 不适用 | 不适用 |  |
| *边框和背景* |
| --- |
| `background` | ❌ 不支持 | ❌ 不支持 |  |
| `border-radius` | ❌ 不支持 | ❌ 不支持 |  |
| `box-shadow` | ❌ 不支持 | ❌ 不支持 |  |

### 选择框（单行）

参见 ``<select>``、``<optgroup>`` 和 ``<option>`` 元素。

| 属性 | N | T | 注释 |
| --- | --- | --- | --- |
| *CSS 盒子模型* |
| --- |
| `width` | ⚠ 部分支持[1] | ⚠ 部分支持[1] | 1. 这个属性在 ``<select>``
 元素上一切正常，但不能用于 ``<option>``
 或 ``<optgroup>`` 元素。
 |
| `height` | ❌ 不支持 | ✅ 支持 |  |
| `border` | ⚠ 部分支持 | ✅ 支持 |  |
| `margin` | ✅ 支持 | ✅ 支持 |  |
| `padding` | ❌ 不支持[1] | ⚠ 部分支持[2] | 1. 这个属性可以应用，但在 Mac OSX 上，但 Mac OSX 上浏览器之间的以不一致的方向显示。
2. 这个属性在 ``<select>``
 元素上一切正常，但不能用于
 ``<option>`` 和
 ``<optgroup>`` 元素。
 |
| *文本和字体* |
| --- |
| `color` | ⚠ 部分支持[1] | ⚠ 部分支持[1] | 1. 在 Mac OSX 上，基于 WebKit 的浏览器不支持将这个属性用于原生组件，它们和 Opera
 在 ``<option>`` 和 ``<optgroup>``
 元素上完全不支持这个属性。
 |
| `font` | ⚠ 部分支持[1] | ⚠ 部分支持[1] | 1. 在 Mac OSX 上，基于 WebKit 的浏览器不支持将这个属性用于原生组件，它们和 Opera
 在 ``<option>`` 和 ``<optgroup>``
 元素上完全不支持这个属性。
 |
| `letter-spacing` | ⚠ 部分支持[1] | ⚠ 部分支持[1] | 1. IE9 不支持将这个属性用于
 ``<select>``、``<option>``
 和 ``<optgroup>`` 元素；Mac OSX
 上基于 WebKit 的浏览器不支持将这个属性应用于
 ``<option>`` 和
 ``<optgroup>`` 元素。
 |
| `text-align` | ❌ 不支持[1] | ❌ 不支持[1] | 1. Windows 7 上的 IE9 和 Mac OSX 上基于 WebKit 的浏览器不支持这个组件上的属性。
 |
| `text-decoration` | ⚠ 部分支持[1] | ⚠ 部分支持[1] | 1. 只有 Firefox 提供了对这个属性的完全支持。Opera
 完全不支持这个属性，而其他浏览器只提供了对
 ``<select>`` 元素的支持。
 |
| `text-indent` | ⚠ 部分支持[1][2] | ⚠ 部分支持[1][2] | 1. 大部分浏览器仅支持将这个属性用于
 ``<select>`` 元素。
2. IE9 不支持这个属性。
 |
| `text-overflow` | ❌ 不支持 | ❌ 不支持 |  |
| `text-shadow` | ⚠ 部分支持[1][2] | ⚠ 部分支持[1][2] | 1. 大部分浏览器仅支持将这个属性用于
 ``<select>`` 元素。
2. IE9 不支持这个属性。
 |
| `text-transform` | ⚠ 部分支持[1] | ⚠ 部分支持[1] | 1. 大部分浏览器仅支持将这个属性用于
 ``<select>`` 元素。
 |
| *边框和背景* |
| --- |
| `background` | ⚠ 部分支持[1] | ⚠ 部分支持[1] | 1. 大部分浏览器仅支持将这个属性用于
 ``<select>`` 元素。
 |
| `border-radius` | ⚠ 部分支持[1] | ⚠ 部分支持[1] |
| `box-shadow` | ❌ 不支持 | ⚠ 部分支持[1] |

注意，Firefox 没有提供用于修改 ``<select>`` 元素的向下箭头的方法。

### 选择框（多行）

参见 ``<select>``、``<optgroup>`` 和 ``<option>`` 元素，以及 `size` 属性 (en-US)。

| 属性 | N | T | 注释 |
| --- | --- | --- | --- |
| *CSS 盒子模型* |
| --- |
| `width` | ✅ 支持 | ✅ 支持 |  |
| `height` | ✅ 支持 | ✅ 支持 |  |
| `border` | ✅ 支持 | ✅ 支持 |  |
| `margin` | ✅ 支持 | ✅ 支持 |  |
| `padding` | ⚠ 部分支持[1] | ⚠ 部分支持[1] | 1. Opera 在 ``<select>``
 元素上不支持 `padding-top` 和
 `padding-bottom`。
 |
| *文本和字体* |
| --- |
| `color` | ✅ 支持 | ✅ 支持 |  |
| `font` | ✅ 支持 | ✅ 支持 | 参见有关 `line-height` 的注释。 |
| `letter-spacing` | ⚠ 部分支持[1] | ⚠ 部分支持[1] | 1. IE9 在 ``<select>``、
 ``<option>`` 和
 ``<optgroup>`` 元素上不支持这个属性；Mac OSX
 上基于 WebKit 的浏览器在 ``<option>``
 和 ``<optgroup>`` 元素上不支持这个属性。
 |
| `text-align` | ❌ 不支持[1] | ❌ 不支持[1] | 1. Windows 7 上的 IE9 和 Mac OSX 上基于 WebKit 的浏览器，不支持这个组件上的这个属性。
 |
| `text-decoration` | ❌ 不支持[1] | ❌ 不支持[1] | 1. 只被 Firefox and IE9+ 支持。
 |
| `text-indent` | ❌ 不支持 | ❌ 不支持 |  |
| `text-overflow` | ❌ 不支持 | ❌ 不支持 |  |
| `text-shadow` | ❌ 不支持 | ❌ 不支持 |  |
| `text-transform` | ⚠ 部分支持[1] | ⚠ 部分支持[1] | 1. 大部分浏览器仅支持将这个属性用于
 ``<select>`` 元素。
 |
| *边框和背景* |
| --- |
| `background` | ✅ 支持 | ✅ 支持 |  |
| `border-radius` | ✅ 支持[1] | ✅ 支持[1] | 1. 在 Opera 上，只有当边框被明确设定时，`border-radius` 属性才会被应用。
 |
| `box-shadow` | ❌ 不支持 | ⚠ 部分支持[1] | 1. IE9 不支持这个属性。
 |

### 数据列表

参见 ``<datalist>`` 和 ``<input>`` 元素，以及 `list` 属性。

| 属性 | N | T | 注释 |
| --- | --- | --- | --- |
| *CSS 盒子模型* |
| --- |
| `width` | ❌ 不支持 | ❌ 不支持 |  |
| `height` | ❌ 不支持 | ❌ 不支持 |  |
| `border` | ❌ 不支持 | ❌ 不支持 |  |
| `margin` | ❌ 不支持 | ❌ 不支持 |  |
| `padding` | ❌ 不支持 | ❌ 不支持 |  |
| *文本和字体* |
| --- |
| `color` | ❌ 不支持 | ❌ 不支持 |  |
| `font` | ❌ 不支持 | ❌ 不支持 |  |
| `letter-spacing` | ❌ 不支持 | ❌ 不支持 |  |
| `text-align` | ❌ 不支持 | ❌ 不支持 |  |
| `text-decoration` | ❌ 不支持 | ❌ 不支持 |  |
| `text-indent` | ❌ 不支持 | ❌ 不支持 |  |
| `text-overflow` | ❌ 不支持 | ❌ 不支持 |  |
| `text-shadow` | ❌ 不支持 | ❌ 不支持 |  |
| `text-transform` | ❌ 不支持 | ❌ 不支持 |  |
| *边框和背景* |
| --- |
| `background` | ❌ 不支持 | ❌ 不支持 |  |
| `border-radius` | ❌ 不支持 | ❌ 不支持 |  |
| `box-shadow` | ❌ 不支持 | ❌ 不支持 |  |

### 文件选择器

参见 `file` input 类型。

| 属性 | N | T | 注释 |
| --- | --- | --- | --- |
| *CSS 盒子模型* |
| --- |
| `width` | ❌ 不支持 | ❌ 不支持 |  |
| `height` | ❌ 不支持 | ❌ 不支持 |  |
| `border` | ❌ 不支持 | ❌ 不支持 |  |
| `margin` | ✅ 支持 | ✅ 支持 |  |
| `padding` | ❌ 不支持 | ❌ 不支持 |  |
| *文本和字体* |
| --- |
| `color` | ✅ 支持 | ✅ 支持 |  |
| `font` | ❌ 不支持[1] | ❌ 不支持[1] | 1. 支持，但浏览器之间的不一致性太多，所以并不可靠。
 |
| `letter-spacing` | ⚠ 部分支持[1] | ⚠ 部分支持[1] | 1. 许多浏览器将这个属性应用到选择按钮上。
 |
| `text-align` | ❌ 不支持 | ❌ 不支持 |  |
| `text-decoration` | ❌ 不支持 | ❌ 不支持 |  |
| `text-indent` | ⚠ 部分支持[1] | ⚠ 部分支持[1] | 1. 它表现的或多或少的像一个组件额外的左边距。
 |
| `text-overflow` | ❌ 不支持 | ❌ 不支持 |  |
| `text-shadow` | ❌ 不支持 | ❌ 不支持 |  |
| `text-transform` | ❌ 不支持 | ❌ 不支持 |  |
| *边框和背景* |
| --- |
| `background` | ❌ 不支持[1] | ❌ 不支持[1] | 1. 支持，但浏览器之间的不一致性太多，所以并不可靠。
 |
| `border-radius` | ❌ 不支持 | ❌ 不支持 |  |
| `box-shadow` | ❌ 不支持 | ⚠ 部分支持[1] | 1. IE9 不支持这个属性。
 |

### 日期选择器

参见 `date` 和 `time` input 类型。很多属性都被浏览器支持，但浏览器之间的不一致性太多，所以并不可靠。

| 属性 | N | T | 注释 |
| --- | --- | --- | --- |
| *CSS 盒子模型* |
| --- |
| `width` | ❌ 不支持 | ❌ 不支持 |  |
| `height` | ❌ 不支持 | ❌ 不支持 |  |
| `border` | ❌ 不支持 | ❌ 不支持 |  |
| `margin` | ✅ 支持 | ✅ 支持 |  |
| `padding` | ❌ 不支持 | ❌ 不支持 |  |
| *文本和字体* |
| --- |
| `color` | ❌ 不支持 | ❌ 不支持 |  |
| `font` | ❌ 不支持 | ❌ 不支持 |  |
| `letter-spacing` | ❌ 不支持 | ❌ 不支持 |  |
| `text-align` | ❌ 不支持 | ❌ 不支持 |  |
| `text-decoration` | ❌ 不支持 | ❌ 不支持 |  |
| `text-indent` | ❌ 不支持 | ❌ 不支持 |  |
| `text-overflow` | ❌ 不支持 | ❌ 不支持 |  |
| `text-shadow` | ❌ 不支持 | ❌ 不支持 |  |
| `text-transform` | ❌ 不支持 | ❌ 不支持 |  |
| *边框和背景* |
| --- |
| `background` | ❌ 不支持 | ❌ 不支持 |  |
| `border-radius` | ❌ 不支持 | ❌ 不支持 |  |
| `box-shadow` | ❌ 不支持 | ❌ 不支持 |  |

### 颜色选择器

参见 `color` input 类型：

| 属性 | N | T | 注释 |
| --- | --- | --- | --- |
| *CSS 盒子模型* |
| --- |
| `width` | ✅ 支持 | ✅ 支持 |  |
| `height` | ❌ 不支持[1] | ✅ 支持 | 1. Opera 以与选择组件一样的限制来处理它。
 |
| `border` | ✅ 支持 | ✅ 支持 |  |
| `margin` | ✅ 支持 | ✅ 支持 |  |
| `padding` | ❌ 不支持[1] | ✅ 支持 | 1. Opera 以与选择组件一样的限制来处理它。
 |
| *文本和字体* |
| --- |
| `color` | 不适用 | 不适用 |  |
| `font` | 不适用 | 不适用 |  |
| `letter-spacing` | 不适用 | 不适用 |  |
| `text-align` | 不适用 | 不适用 |  |
| `text-decoration` | 不适用 | 不适用 |  |
| `text-indent` | 不适用 | 不适用 |  |
| `text-overflow` | 不适用 | 不适用 |  |
| `text-shadow` | 不适用 | 不适用 |  |
| `text-transform` | 不适用 | 不适用 |  |
| *边框和背景* |
| --- |
| `background` | ❌ 不支持[1] | ❌ 不支持[1] | 1. 支持，但浏览器之间的不一致性太多，所以并不可靠。
 |
| `border-radius` | ❌ 不支持[1] | ❌ 不支持[1] |
| `box-shadow` | ❌ 不支持[1] | ❌ 不支持[1] |

### 标量值和进度

参见 ``<meter>`` 和 ``<progress>`` 元素：

| 属性 | N | T | 注释 |
| --- | --- | --- | --- |
| *CSS 盒子模型* |
| --- |
| `width` | ✅ 支持 | ✅ 支持 |  |
| `height` | ✅ 支持 | ✅ 支持 |  |
| `border` | ⚠ 部分支持 | ✅ 支持 |  |
| `margin` | ✅ 支持 | ✅ 支持 |  |
| `padding` | ✅ 支持 | ⚠ 部分支持[1] | 1. 当 `padding` 属性被引用于一个 tweaked
 元素时，Chrome 会隐藏 ``<progress>``
 和 ``<meter>`` 元素。
 |
| *文本和字体* |
| --- |
| `color` | 不适用 | 不适用 |  |
| `font` | 不适用 | 不适用 |  |
| `letter-spacing` | 不适用 | 不适用 |  |
| `text-align` | 不适用 | 不适用 |  |
| `text-decoration` | 不适用 | 不适用 |  |
| `text-indent` | 不适用 | 不适用 |  |
| `text-overflow` | 不适用 | 不适用 |  |
| `text-shadow` | 不适用 | 不适用 |  |
| `text-transform` | 不适用 | 不适用 |  |
| *边框和背景* |
| --- |
| `background` | ❌ 不支持[1] | ❌ 不支持[1] | 1. 支持，但浏览器之间的不一致性太多，所以并不可靠。
 |
| `border-radius` | ❌ 不支持[1] | ❌ 不支持[1] |
| `box-shadow` | ❌ 不支持[1] | ❌ 不支持[1] |

### 范围

参见 `range` input 类型。没有一种标准的方式来改变范围小组件的样式，Opera 也没有调整范围小组件默认渲染的方法。

| 属性 | N | T | 注释 |
| --- | --- | --- | --- |
| *CSS 盒子模型* |
| --- |
| `width` | ✅ 支持 | ✅ 支持 |  |
| `height` | ⚠ 部分支持[1] | ⚠ 部分支持[1] | 1. Chrome 和 Opera 在组件周围添加了额外的空白，而
 Windows 7 上的 Opera 则拉伸范围选择器的滑块。
 |
| `border` | ❌ 不支持 | ✅ 支持 |  |
| `margin` | ✅ 支持 | ✅ 支持 |  |
| `padding` | ⚠ 部分支持[1] | ✅ 支持 | 1. `padding` 属性被应用时，不会有任何视觉效果。
 |
| *文本和字体* |
| --- |
| `color` | 不适用 | 不适用 |  |
| `font` | 不适用 | 不适用 |  |
| `letter-spacing` | 不适用 | 不适用 |  |
| `text-align` | 不适用 | 不适用 |  |
| `text-decoration` | 不适用 | 不适用 |  |
| `text-indent` | 不适用 | 不适用 |  |
| `text-overflow` | 不适用 | 不适用 |  |
| `text-shadow` | 不适用 | 不适用 |  |
| `text-transform` | 不适用 | 不适用 |  |
| *边框和背景* |
| --- |
| `background` | ❌ 不支持[1] | ❌ 不支持[1] | 1. 支持，但浏览器之间的不一致性太多，所以并不可靠。
 |
| `border-radius` | ❌ 不支持[1] | ❌ 不支持[1] |
| `box-shadow` | ❌ 不支持[1] | ❌ 不支持[1] |

### 图像按钮

参见 `image` input 类型：

| 属性 | N | T | 注释 |
| --- | --- | --- | --- |
| *CSS 盒子模型* |
| --- |
| `width` | ✅ 支持 | ✅ 支持 |  |
| `height` | ✅ 支持 | ✅ 支持 |  |
| `border` | ✅ 支持 | ✅ 支持 |  |
| `margin` | ✅ 支持 | ✅ 支持 |  |
| `padding` | ✅ 支持 | ✅ 支持 |  |
| *文本和字体* |
| --- |
| `color` | 不适用 | 不适用 |  |
| `font` | 不适用 | 不适用 |  |
| `letter-spacing` | 不适用 | 不适用 |  |
| `text-align` | 不适用 | 不适用 |  |
| `text-decoration` | 不适用 | 不适用 |  |
| `text-indent` | 不适用 | 不适用 |  |
| `text-overflow` | 不适用 | 不适用 |  |
| `text-shadow` | 不适用 | 不适用 |  |
| `text-transform` | 不适用 | 不适用 |  |
| *边框和背景* |
| --- |
| `background` | ✅ 支持 | ✅ 支持 |  |
| `border-radius` | ⚠ 部分支持[1] | ⚠ 部分支持[1] | 1. IE9 不支持这个属性。
 |
| `box-shadow` | ⚠ 部分支持[1] | ⚠ 部分支持[1] | 1. IE9 不支持这个属性。
 |

参见
--

### 学习路径

* 创建我的第一个表单
* 如何构造 HTML 表单
* 原生表单控件
* HTML5 input 类型
* 其他表单控件
* UI 伪类
* 样式化 HTML 表单
* 表单数据校验
* 发送表单数据
### 进阶内容

* 使用 JavaScript 发送表单
* 如何构建自定义表单控件
* 旧式浏览器中的 HTML 表单
* 高级 HTML 表单样式
* 表单组件兼容性列表


样式化 HTML 表单 - 学习 Web 开发

样式化 HTML 表单
===========

* 上一页
* Overview: Forms
* 下一页

在这篇文章中，用户将学习如何使用 HTML 表单和 CSS 以使页面更加美观。令人惊讶的是，这可能有点棘手。由于历史和技术的原因，表单部件不能很好地与 CSS 配合工作。由于这些困难，许多开发人员选择构建自己的 HTML 小部件 (en-US)以获得更好的控制和视觉观感。然而，在现代浏览器中，web 设计者越来越多地控制表单元素的设计。让我们深入研究。

为什么使用 CSS 美化表单组件这么困难？
---------------------

在 1995 年左右的 Web 早期，表单组件 (或控件) 在 HTML 2 规范中被添加到 HTML。由于表单组件的复杂性，实现者选择依靠底层操作系统来管理和渲染它们。

若干年后，CSS 被创建出来了，那么技术上的必要性，就是使用原生组件来实现表单控制，这是因为风格的要求。在 CSS 的早期，表单样式控制不是优先事项。

由于用户习惯于各自平台的视觉外观，浏览器厂商不愿意对表单控件样式进行调整;到目前为止，要重建所有控件以使它们可美化仍然是非常困难的。

即使在今天，仍然没有一个浏览器完全实现了 CSS 2.1。然而，随着时间的推移，浏览器厂商已经改进了对表单元素的 CSS 支持，尽管可用性的声誉不好，但现在已经可以使用 CSS 来设计HTML 表单。

### 涉及到 CSS，并非所有组件都是平等的

目前，在使用表单时使用 CSS 仍然有一些困难。这些问题可以分为三类：

#### 好的

有些元素在跨平台上时很少出现问题。包括以下结构元素：

1. `<form>`
2. `<fieldset>`
3. `<label>`
4. `<output>`

这还包括所有文本字段小部件（单行和多行）和按钮。

#### 不好的

一些元素难以被美化，并且可能需要一些复杂的技巧，偶尔需要高级的 CSS3 知识。

这些包括`<legend>`元素，但不能在所有平台上正确定位。Checkbox 和 radio 按钮也不能直接应用样式，但是，感谢 CSS3，你可以解决这个问题。`placeholder` 的内容不能以任何标准方式应用样式，但是实现它的所有浏览器也都实现了私有的 CSS 伪元素或伪类，让你可以对其定义样式。

我们会在如何构建自定义表单挂件一文中讲述如何处理更多特定的问题。

#### 丑陋的

有些元素根本不能用应用 CSS 样式。这些包括：所有高级用户界面小部件，如范围，颜色或日期控件; 和所有下拉小部件，包括`<select>`, `<option>`, `<optgroup>`和`<datalist>` 元素。文件选择器小部件也被称为不可样式化。新的`<progress>`和`<meter>` 元素也属于这个类别。

所有这些小部件的主要问题来自于它们具有非常复杂的结构，而 CSS 目前还不足以表达这些小部件的所有细微部分。如果你想定制这些小部件，你必须依靠 JavaScript 来构建一个你能够应用样式的 DOM 树。我们会在 How to build custom form widgets (en-US)一文中探索如何实现这一点。

基本样式美化
------

为了使用 CSS 美化容易被美化的元素，你并不会碰到任何困难，因为它们的大部分行为同其他 HTML 元素差不多。但是，每个浏览器的用户代理样式表可能会有点不一致，所以有一些技巧可以帮助你更轻松地设计它们。

### Search 字段

搜索框是唯一一种应用 CSS 样式有点棘手的文本字段。在基于 WebKit 的浏览器（Chrome，Safari 等）上，你必须使用`-webkit-appearance`专有属性来调整它。我们在文章中进一步讨论这个属性：HTML 表单的高级样式 (en-US)。

#### Example

```html
<form>
  <input type="search" />
</form>
```

```css
input[type="search"] {
  border: 1px dotted #999;
  border-radius: 0;

  -webkit-appearance: none;
}
```

![This is a screenshot of a search filed on Chrome, with and without the use of -webkit-appearance](/files/4153/search-chrome-macos.png)

截图中是 Chrome 浏览器中的两个搜索框，在我们的例子中，两个搜索框均被设置为有边框。第一个没有使用`-webkit-appearance`渲染，而第二个使用了 `-webkit-appearance:none`. 两者的不同显而易见。

### 字体和文本

CSS font 和 text 功能能被很容易的应用到任何组件上（当然你可以在 form 组件上使用`@font-face` ）。然而，浏览器的行为经常不一致。默认情况下，一些组件不会从它们的父元素继承 `font-family`和 `font-size` 。相反，许多浏览器使用系统默认的字体和文本。为了让 form 表单的外观和其他内容保持一致，你可以在你的样式表中增加以下内容：

```css
button,
input,
select,
textarea {
  font-family: inherit;
  font-size: 100%;
}
```

下面的截图显示了不同之处; 左边是 Mac OS X 上 Firefox 中元素的默认渲染，其中使用了平台的默认字体样式。在右边是相同的元素，应用了我们的字体统一样式规则。

![This is a screenshot of the main form widgets on Firefox on Mac OSX, with and without font harmonization](/files/4157/font-firefox-macos.png)

关于使用系统默认样式的表单还是使用设计用于匹配内容的自定义样式表单，有很多争议。作为网站或 Web 应用程序的设计者，你可以自己做出决定。

### 盒子模型

所有文本字段都完全支持与 CSS 盒模型相关的每个属性 (`width`, `height`, `padding`, `margin`, 和 `border`)。但是，像以前一样，浏览器在显示这些小部件时依赖于系统默认的样式。你需要定义如何将其融入到你的内容中。如果你既想保持小部件的原生外观和感觉，又想给他们一个一致的尺寸，那么你会遇到一些困难 (如果你想保持组件的原生观感，又想给它们一致的大小，你会面临一些困难)。

**这是因为每个小部件都有自己的边框，填充和边距的规则。** 所以如果你想给几个不同的小部件相同的大小，你必须使用`box-sizing` 属性：

```css
input,
textarea,
select,
button {
  width: 150px;
  margin: 0;

  -webkit-box-sizing: border-box; /\* For legacy WebKit based browsers \*/
  -moz-box-sizing: border-box; /\* For legacy (Firefox <29) Gecko based browsers \*/
  box-sizing: border-box;
}
```

![This is a screenshot of the main form widgets on Chrome on Windows 7, with and without the use of box-sizing.](/files/4161/size-chrome-win7.png)

在上面的屏幕截图中，左侧的列没有`box-sizing`，而右侧的列使用了这个属性和`border-box`。请注意我们是怎样确保所有元素都占用相同的空间量，尽管平台对每种窗口小部件都有默认规则。

### 定位（Positioning）

HTML 表单部件的定位通常不是问题; 但是，你应该特别注意两点：

#### legend

`<legend>`元素易于应用 CSS，除了定位。在所有浏览器中， `<legend>` 元素定位是其 `<fieldset>` 父元素的上边框的最顶端。在 HTML 流中无法改变它的绝对位置，无法让其远离顶部边框。然而，你可以使用 `position` 属性将其位置设置为绝对或相对。除此之外，它近几年是 fieldset 边框的一部分。

由于`<legend>`元素对无障碍非常重要，因为它能被无障碍技术作为每个 fieldset 中的表单元素的标签读出来，它通常与标题配对，并且在无障碍中被隐藏。例如：

##### HTML

```html
<fieldset>
  <legend>Hi!</legend>
  <h1>Hello</h1>
</fieldset>
```

##### CSS

```css
legend {
  width: 1px;
  height: 1px;
  overflow: hidden;
}
```

#### textarea

默认情况下，所有浏览器都认为`<textarea>` 元素是 inline block，与文本底线对齐。这很少是我们真正想看到的。要将内联 (`inline-block`) 块更改为块 (`block`)，使用`display`属性非常简单。但是如果你想以 inline 方式使用它，通常改变垂直对齐方式：

```css
textarea {
  vertical-align: top;
}
```

示例
--

让我们来看一个样式化 HTML 表单的实际的案例。这有助于理清这里面的许多概念。我们将构建下面的"明信片" 联系人表单：

![This is what we want to achieve with HTML and CSS](/files/4149/screenshot.png)

如果你想继续关注这个例子，复制我们的 postcard-start.html 文件，并遵循接下来的指导操作。

### The HTML

HTML 只比我们在 the first article of this guide (en-US) 中涉及到的多一些；它只有一些额外的 id 和 title。

```html
<form>
  <h1>to: Mozilla</h1>

  <div id="from">
    <label for="name">from:</label>
    <input type="text" id="name" name="user\_name" />
  </div>

  <div id="reply">
    <label for="mail">reply:</label>
    <input type="email" id="mail" name="user\_email" />
  </div>

  <div id="message">
    <label for="msg">Your message:</label>
    <textarea id="msg" name="user\_message"></textarea>
  </div>

  <div class="button">
    <button type="submit">Send your message</button>
  </div>
</form>
```

将上面的代码添加到你 HTML 的 body 中。

### 组织你的静态文件

好戏要开始了！在开始写代码之前，我们需要三个额外的静态文件：

1. 明信片的背景——下载这幅图片，把它和你的 HTML 文件保存在相同目录下。
2. 打字机字体：源自 fontsquirrel.com 的 "Secret Typewriter“字体——将 TTF 文件下载到和上面相同的文件夹里。
3. 手绘字体：源自 fontsquirrel.com 的 The "Journal" 字体——将 TTF 文件下载到和上面相同的文件夹里。

在你开始之前需要对字体做一些处理：

1. 打开 fontsquirrel 网络字体生成器.
2. 使用表单，上传你的字体文件并生成一个网络字体包，将这个包下载到你的电脑上。
3. 解压提供的 zip 文件。
4. 再解压后的文件内容里你会找到两个 `.woff` 文件和两个`.woff2` 文件。将这四个文件拷贝到一个叫 fonts 的文件夹里，而 fonts 文件夹位于和上面相同的文件夹里。我们为每种字体使用两个不同的文件以最大限度地保证浏览器兼容性。查看我们的 Web 字体 一文获取更多信息。
### CSS

现在我们可以深入探究本例的 CSS 了。将下面所有的代码块一个接一个地加到`<style>` 元素里。

首先，我们要准备一些基础。这需要定义 `@font-face` 规则，以及所有的 `<body>` 元素和 `<form>` 元素基本规则：

```css
@font-face {
  font-family: "handwriting";
  src:
    url("fonts/journal-webfont.woff2") format("woff2"),
    url("fonts/journal-webfont.woff") format("woff");
  font-weight: normal;
  font-style: normal;
}

@font-face {
  font-family: "typewriter";
  src:
    url("fonts/veteran\_typewriter-webfont.woff2") format("woff2"),
    url("fonts/veteran\_typewriter-webfont.woff") format("woff");
  font-weight: normal;
  font-style: normal;
}

body {
  font: 21px sans-serif;

  padding: 2em;
  margin: 0;

  background: #222;
}

form {
  position: relative;

  width: 740px;
  height: 498px;
  margin: 0 auto;

  background: #fff url(background.jpg);
}
```

现在我们可以定位我们的元素，包括标题和其他表单元素：

```css
h1 {
  position: absolute;
  left: 415px;
  top: 185px;

  font:
    1em "typewriter",
    sans-serif;
}

#from {
  position: absolute;
  left: 398px;
  top: 235px;
}

#reply {
  position: absolute;
  left: 390px;
  top: 285px;
}

#message {
  position: absolute;
  left: 20px;
  top: 70px;
}
```

现在我们开始处理表单元素本身。首先，让我们确保 `<label>` 被赋予了正确的字体：

```css
label {
  font:
    0.8em "typewriter",
    sans-serif;
}
```

文本域需要一些通用的规则，我们只需简单的移除 `borders` 和 `backgrounds`, 并重新定义其`padding` 和 `margin`：

```css
input,
textarea {
  font:
    0.9em/1.5em "handwriting",
    sans-serif;

  border: none;
  padding: 0 10px;
  margin: 0;
  width: 240px;

  background: none;
}
```

当其中的一个域获得焦点后，我们用浅灰色、半透明的背景高亮它们，注意添加`outline` 属性非常重要，这样可以移除由某些浏览器添加的默认高亮效果：

```css
input:focus,
textarea:focus {
  background: rgba(0, 0, 0, 0.1);
  border-radius: 5px;
  outline: none;
}
```

现在我们的文本域已经完成了，我们需要调整单行和多行文本域的显示，使其能够匹配，因为通常情况下它们不会以默认的设置而具有一样的外观。

单行文本需要一些调整才能在 Internet Explorer 中渲染良好。Internet Explorer 没有基于字体的自然高度来定义文本域的高度（而这是所有其他浏览器都有的行为）。为了修正这个问题，我们需要给域添加一个确定的高度，像下面这样：

```css
input {
  height: 2.5em; /\* for IE \*/
  vertical-align: middle; /\* This is optional but it makes legacy IEs look better \*/
}
```

`<textarea>` 元素默认地被渲染成一个块级元素。这里有重要地两点是 `resize` 和 `overflow` 属性。因为我们的设计是一个固定大小的设计，所以我们会使用 `resize` 属性来防止用户调整我们的多行文本域的大小。`overflow` 属性是用来让域在不同的浏览器上渲染得更一致。一些浏览器默认值为 `auto`，而一些将默认值设为 `scroll`。在我们得例子中，最好确定每个浏览器都使用 `auto`：

```css
textarea {
  display: block;

  padding: 10px;
  margin: 10px 0 0 -10px;
  width: 340px;
  height: 360px;

  resize: none;
  overflow: auto;
}
```

`<button>` 元素上使用 CSS 非常方便；你可以做你任何想做得事情，甚至包括使用 伪元素：

```css
button {
  position: absolute;
  left: 440px;
  top: 360px;

  padding: 5px;

  font: bold 0.6em sans-serif;
  border: 2px solid #333;
  border-radius: 5px;
  background: none;

  cursor: pointer;

  -webkit-transform: rotate(-1.5deg);
  -moz-transform: rotate(-1.5deg);
  -ms-transform: rotate(-1.5deg);
  -o-transform: rotate(-1.5deg);
  transform: rotate(-1.5deg);
}

button:after {
  content: " >>>";
}

button:hover,
button:focus {
  outline: none;
  background: #000;
  color: #fff;
}
```

瞧！

**备注：** 如果你的例子没有像你预期的那样工作，你想将它同我们的版本检查对比，你可以在 Github 上找到它——查看在线演示（也可以查看源代码）。

总结
--

如你所见，若我们想构建只包含文本域和按钮的表单，用 CSS 美化它们非常容易。如果你想要知道更多能够让你的处理表单组件时更轻松的 CSS 小技巧，看一看 normalize.css 项目的表单部分。

下一篇文章中，我们将会看到如何处理落入“不好的”和“丑陋的”分类的表单组件。

* 上一页
* Overview: Forms
* 下一页
### 进阶内容

* 如何构建自定义表单控件
* 通过 JavaScript 发送表单
* 表单控件的属性兼容性列表