纯原生(html+css)实现自定义复选框

你知道组件库的checkbox如何实现的吗?

最近参加了一场笔试,题目要求使用 React 和 TypeScript 封装一个多选框组件。题目本身不难,但如果处理得太简单,就容易"出事" ------ 毕竟既然考你封装组件,那用现成组件库自然是不允许的,只能从零开始,用"原生方式"搞定。

不过,原生的 checkbox 又太单调 ,交互和样式都很局限。于是我去看了一下 Element Plus 的设计,想找找灵感。

没想到一看就被惊到了:

"哦,原来 checkbox 还可以这么写!"

从结构上看,Element Plus 并没有直接使用原生 checkbox 来展示勾选框,而是:

  • 将原生 <input type="checkbox"> 隐藏

  • 自定义了一个具有视觉样式的 span.checkbox__inner

  • 再通过 label 的点击联动原生 input,实现状态同步

看似复杂,其实本质是:

原生 input 被"屏蔽"了视觉,但保留了交互响应能力。

那么问题来了:

如何隐藏一个元素,但仍然保留交互?

我查阅了一些资料,并做了几个测试,发现了下面几种常见的"隐藏方式"及其副作用对比如下:

  • display: none 无文档流 无法交互
  • opacity: 0 有文档流 可以交互
  • height: 0 width: 0 无文档流 无法交互
  • visibility:hidden 无文档流 无法交互 但占据空间
  • clip-path:polygon(..) 裁剪为不可见 有文档流 但不可见 无法绑定
  • transsform.scale(0) 将元素缩放到不可见 有文档流 可以交互

所以最终选择哪种方式?

其实 opacity: 0 和 transform: scale(0) 都可以满足"视觉隐藏但保留交互"的需求。

但我最后还是选择了 opacity ------ 更直观,语义也更清晰,不容易引起奇怪的副作用或命中问题。

额外一点建议:关于 label 和 input 的绑定

在 Element Plus 的源码中,原生 是放在 标签内部的,这样可以自动形成绑定,不需要再写 for / id。确实没问题,也很简洁。

不过我个人的建议是:

出于习惯,如果结构允许,最好还是给 input 写上 id,label 使用 for 显式绑定。

  • 测试人员可以更方便地选中目标元素(特别是自动化测试)
  • 语义更清晰,可维护性更强
  • 即使结构发生变化,也不影响交互逻辑

那么还有一个问题,✅没了,如何实现选中效果呢?

选中时让原生input出现么?你有没有发现,勾勾的角好像是直角,我猜你肯定被问过如何用css画三角形吧?那么画个勾,问题也不大吧。

想的都是问题,做的才是答案,开始吧

先打好框架
html 复制代码
<label class="checkbox" for="checkbox">
    <span class="checkbox__input">
        <input class="checkbox__original" type="checkbox" id="checkbox">
        <span class="checkbox__inner"></span>
    </span>
    <span class="checkbox__label">点击选中</span>
</label>

完成常规的布局,第一件事儿,就是给原生input打上一套组合拳

css 复制代码
.checkbox__original {
  opacity: 0;
  width: 0;
  height: 0;
  z-index: -1;
  position: absolute;
}

似乎忘了勾选框,嗯,能不能勾勾用伪元素来做呢?span元素画正方形,然后伪元素绝对定位,微调一下,就可以了。

给span元素画边框

如果你希望一个 span 元素能设置宽高,一般做法是给它加上 display: inline-blockblock,或者让它处于一个特殊布局(如 flex/grid)下。
*

给当前元素加上伪元素,并画出选中标记✅

为什么伪元素默认是 inline

因为它本质上是"插入"到内容流中的一段内容,默认就是像文字一样在行内显示。但 inline不支持设置宽高的,所以我们一般都需要手动设置,如图,我们要做的,就是将它旋转一下,并取消左和上的边框,这里旋转中心,就是伪元素的中心。

移动选中标记

使用lefttop,所以它的position不能是默认的static,如果你想改为absolute,那就是相对于离它层级最近的一个不是static的容器,所以父容器也需要修改position,然后微调就可以了。

最后,再给它加上一些效果。
css 复制代码
.checkbox__inner{
    /* ...*/
    transition: background-color 0.3s ease,border-color 0.3s ease; /* 添加过渡动画 */
}
.checkbox__inner:hover{
    border-color: #409eff;
}
.checkbox__original:checked + .checkbox__inner{
    border-color: #409eff;
    background-color: #409eff;
}
.checkbox__original:checked + .checkbox__inner::after {
    transform: rotate(45deg) scale(1); /* 选中时显示 */
}

写在最后

我发现,亲手去写一些小样式、小交互,真的能极大地提升对前端的理解和掌控感。 知识的广度 ,决定了你解决问题时能想到多少种方案; 知识的深度 ,则决定了你的代码是否优雅而高效。 所以,如果你看到这里,不妨动动手,试试实现几种不同的点击动效?

相关推荐
星空寻流年4 小时前
css3伸缩盒模型第二章(侧轴相关)
javascript·css·css3
GalenWu5 小时前
对象转换为 JSON 字符串(或反向解析)
前端·javascript·微信小程序·json
GUIQU.6 小时前
【Vue】微前端架构与Vue(qiankun、Micro-App)
前端·vue.js·架构
数据潜水员6 小时前
插槽、生命周期
前端·javascript·vue.js
2501_907136826 小时前
CSS 学习与工程化实践指南
css
2401_837088506 小时前
CSS vertical-align
前端·html
优雅永不过时·6 小时前
实现一个漂亮的Three.js 扫光地面 圆形贴图扫光
前端·javascript·智慧城市·three.js·贴图·shader
CodeCraft Studio7 小时前
报表控件stimulsoft教程:使用 JoinType 关系参数创建仪表盘
前端·ui
春天姐姐8 小时前
vue知识点总结 依赖注入 动态组件 异步加载
前端·javascript·vue.js
互联网搬砖老肖9 小时前
Web 架构之数据读写分离
前端·架构·web