前言
面试官:(打着哈欠,眼皮打架)你说你写的组件库拓展性很好,怎么证明呢?
我:(自信满满)您直接去我的组件库官网看,这是传统组件库样式,大家都能做:

地址:上图页面地址
我 :(切换页面)然后这是我用自己写的 @t-headless-ui/react 中的同一个 Button 组件,只是稍微改了下样式。比如你想要像素风格,请看

地址:上图页面地址
(如果您觉得不错,请求 github 一个赞,我一定会拓展所有主流组件到其中的!)
面试官 :(原本昏昏欲睡,突然瞪大眼睛,从凳子上一跃而起)"卧槽!兄弟!你这...这怎么做到的?!"
我:(淡定一笑)无它,headless 组件库而已!
面试官:(扶了扶惊掉的下巴)"headless 组件库是什么意思?"
我:"简单说就是------我们只提供JavaScript逻辑和功能,样式你爱咋写咋写!甚至连DOM结构都随便你折腾!"
面试官 :(沉默三秒,突然起身握住我的手)"好!我服!明天就来上班!工资你随便开!"
当然,上面只是一个小玩笑,哈哈,今天我们就来介绍一下,如何写一个像素风格的 button 以及为啥我的 headless button 稍微改一下样式,就可以造一个生产级别能用的 button 呢?
一个生产环境要用的 button 需要具备什么样的属性呢?
button 的状态
一个 button 至少需要:
- 定义
disabled状态,在disabled状态,click事件是无法生效的 - 定义
loading状态,在loading状态,click事件同样是是无法生效的
在 headless 也就是无样式组件的层面上,做到拦截这两个属性的 click 事件就足够了.
是不是很简单,只是因为 button 的复杂度更多在于定制样式,所以 headless 组件库的 button 很容易实现。
核心代码如下:
javascript
const handleClick: MouseEventHandler = (event): void => {
if (loading || disabled) {
// 阻止按钮默认行为,防止在 loading 或 disabled 状态下意外提交表单或触发其他默认事件
event?.preventDefault?.();
return;
}
onClick?.(event);
};
好了,接下来我们说样式,样式的难度主要在于状态很多,包括:
- 各种主题色
- primary:主色
- success: 成功色
- error:失败颜色
- 。。。 然后每个状态都分为,default 状态,disabled 状态,loading 状态
- 各种风格
- 有背景色的风格
- 只有边框的风格
- 。。。
然后每个状态都分为,default 状态,disabled 状态,loading 状态
还有需要考虑 focus 上的颜色状态,hover 的颜色状态。
这才是一个 button 真正复杂的点!
如果你想学造组件库,欢迎加入,我们超级可拓展的 headless 组件库交流群(还有包装简历服务哦!)主页
我们现在为了说明这个像素 button 实现的思路,我们化繁为简,假设只有 1 种颜色,1 种风格,如下图:

说明实现思路是如何,大家可以触类旁通!
如何制作出像素效果
从上面图可以看出,最明显的像素想过就是我们死角都是 transparent(透明色),这个明显是 border 实现不了的。那如何做呢?
秘密就是 box-shadow !
我们先举一个小例子,你看看 box-shadow 如何使用,假设我们需要一个下边框,box-shadow 设置为:
css
box-shadow: 0 5px black;
其中 0 代表横向的偏移量,我们是 0 ,也就是横轴上什么也不做,5px 代表在竖直方向上向下制造一个 5px 的 shadow, 然后颜色设置为 black 也就是黑色,请看这个按钮下方多了一条黑色

接着,我们依葫芦画瓢,加上 上面的 shadow:
css
box-shadow: 0 5px black, 0 -5px black;
效果如下

接着右边我们也加一下:
css
box-shadow: 0 5px black, 0 -5px black, 5px 0 black;

是不是有感觉了,shadow 跟 border 最大的不同就是边角不会填充颜色,然后接着,我们加上又边框:
css
box-shadow: 0 5px black, 0 -5px black, 5px 0 black, -5px 0 black;
最后我们再润色一下,加一个上阴影,增加立体感,如下图

css
box-shadow: 0 5px black, 0 -5px black, 5px 0 black, -5px 0 black, inset 0px 4px rgba(255,255,255,0.21);
其中 inset 的意思是增加内阴影,之前我们都是外阴影,rgba(255,255,255,0.21) 是指白色,然后 0.21 的透明度。
初始化 button
在我们设置 box-shadow 之前,这个 button 背景色,字体这些还没初始化,我们先来补上这个知识
css
{
position: relative;
display: inline-flex;
justify-content: center;
align-items: center;
user-select: none;
padding-top: .25rem;
padding-bottom: .25rem;
padding-left: 1.25rem;
padding-right: 1.25rem;
background-color: rgb(88,199,192);
border-width: 0;
border-radius: 0;
}
好了初始化完毕,我们开始处理各种状态:
hover 状态
hover 状态我们用了一个 filter 函数,增加亮度。
css
filter:brightness(1.05)
但是同时注意,在 disabled 和 loading 状态 hover 无效。在 react 一般都要使用 classnames 库,tailwindcss 还要将 classnames 和 tailwindcss-merge 库结合, vue 好像天然对于 css 的条件判断就支持的不错。
类似代码如下(不同的框架和使用的 css 框架不同,代码肯定有不同,这个很简单,大家看得懂意思即可):
javascript
{
'hover:brightness-105': !disabled && !loading
}
'hover:brightness-105' 是 tailwindcss 中的类名, 对于普通的 css 你只要换成你希望的类名,然后引入对应的 css 文件即可(在 css 文件里实现这个 hover 的 css).
active 状态
active 状态,我们只是把内阴影变黑了,给人一种凹凸下去的感觉。 active 下的 box-shadow 变为
css
box-shadow: 0px 5px black,0px -5px black,5px 0px black,-5px 0px black,inset 0px 5px #00000038
可以看到只是最后 inset 中的颜色变深了,其它都没有变。
处理 disabled 状态
这个状态其实需要判断,比如 disabled 的时候 cursor ,也就是鼠标显示一个禁止点击的状态,这样有利于告诉用户此时点击无效。
类似的增加对 disabled 状态的处理:
javascript
{
'opacity-50 cursor-not-allowed': disabled, // disabled 时透明度降低,cursor 变为 not-allowed
}
字体更想像素的样子
这个最好是搜索有像素风格的字体样式,我是为了方便,使用系统自带中,类似有像素风格的字体,大家可以参考,兼容mac和 windows
css
{
font-family: 'ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,'Lucida_Console','Courier_New',monospace'
}
dark 模式
我使用的是 tailwindcss 天然对 dark 模式可以自定义颜色,国内很多人不使用 taiwindcss 这里就不赘述了
小总结
这里就结束了,这一种状态看起来代码不多,其实状态和颜色多了之后,css 代码会非常多。最后,大家可以看到 headless 组件的好处就是跟样式没有任何耦合,随便用各种 css 框架自己处理,它的好处我会在后面的文章继续介绍,例如 radio 组件,本质上就是单选,我们看到很多组件库都是一个圆形按钮的样式,其实谁规定了必须是这个样式?我可以是任何只要满足单选的样式即可。
所以后续会有很多有趣的主题加入进来,包括button,也会持续更新更重好玩的样式和动画!
最后你到看到这里了,欢迎各位大侠来组件库交流群交流,也希望各位大侠给个github的赞,嘿嘿