引言
在写 Markdown 转微信公众号, 又何必借助于其他工具?? 过程中, 参考了 antd 官方文档 中工具栏的样式, 发现了一个有意思的交互:

本文主要就是想着不用 JS 纯 CSS 复刻一个差不多的交互菜单, 最后完成的效果如下:

下面开始复刻....
补充: 本文案例展示点击这里(css/pop-up-menu), 源码点这儿(css/PopUpMenu)....
一、分析
不会分析, 就随便分析分析.....
功能分析:
- 点击按钮展开菜单, 按钮变为关闭按钮
- 点击关闭按钮, 菜单收起
- 点击页面空白区域, 菜单收起
- 菜单展开收起, 透明度、位置发生过渡动画
难点: 这里难点可能在于, 如何记录菜单展开、收起状态? 目前能想到的就是通过 表单 + 伪类来实现
二、布局
第一步, 先完成基本的布局: 基本代码、样式如下
jsx
// React 代码
import React from 'react';
import scss from './index.module.scss';
import {
ToolOutlined,
AppleOutlined,
CloseOutlined,
DeleteOutlined,
PieChartOutlined,
AliwangwangOutlined,
} from '@ant-design/icons';
export default () => (
<div className={scss.body}>
<div className={scss.mene}>
<div className={scss['mene-item-wrapper']}>
<div className={scss['mene-item']}>
<AliwangwangOutlined />
</div>
<div className={scss['mene-item']}>
<DeleteOutlined />
</div>
<div className={scss['mene-item']}>
<PieChartOutlined />
</div>
<div className={scss['mene-item']}>
<AppleOutlined />
</div>
</div>
<div className={scss['mene-toggle']}>
<ToolOutlined />
</div>
</div>
</div>
);
scss
// scss 样式
.body {
width: 100%;
height: 100%;
background-color: #fff;
position: relative;
}
.mene {
top: 100px;
left: 100px;
position: absolute;
overflow: hidden;
}
.mene-item,
.mene-toggle {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #fff;
box-shadow: 0 0 10px rgba($color: #000, $alpha: 10%);
margin: 10px;
display: flex;
align-items: center;
justify-content: center;
}
最终效果如下:

三、记录状态
在 React 中我们可能会通过 useState 声明一个状态, 来记录 UI 的交互状态, 但是如果不用任何 JS 我们有啥办法可以记录状态呢?
3.1 checkbox 配合 :checked
最开始想到的通过 checkbox 来记录状态, 并且通过 :checked 伪类来选中 勾选状态 的元素, 同时配合兄弟选择器 ~ 来选中同层级的元素, 下面我们来看个简单 DEMO
如下代码所示:
label关联到同层级的checkbox表单label本身展示为一个按钮, 按钮文案默认是展开, 当checkbox选中时按钮显示收起- 然后通过
#checkbox:checked ~ label选中checkbox勾选状态下的同层级兄弟元素label - 同理通过
#checkbox:checked ~ .content选中checkbox勾选状态下的同层级兄弟元素.content
html
<style>
.content {
width: 500px;
max-height: 46px;
overflow: hidden;
transition: all 0.4s;
margin-bottom: 20px;
}
label {
padding: 2px 10px;
border-radius: 4px;
background-color: #4096ff;
}
label::before {
content: '展开';
}
#checkbox:checked ~ label::before {
content: '收起';
}
#checkbox:checked ~ .content {
max-height: 400px;
}
</style>
<input type="checkbox" id="checkbox" />
<div class="content">
一个民族的复兴需要强大的物质力量,也需要强大的精神力量。党的十八大以来,以习近平同志为核心的党中央总揽全局,把宣传思想文化工作摆在重要位置,指引宣传思想文化事业在举旗定向、正本清源中取得历史性成就、发生历史性变革,在守正创新、开拓进取中展现新气象、迈向新征程。
</div>
<label for="checkbox"></label>
上面代码效果如下: 点击按钮将改变 checkbox 的状态, 长文本高度又受控于 checkbox 的状态

3.2 input 配合 :focus
下面介绍另一种方法, 也是本文案例最后选用的方案, 那就是通过 input 配合 :focus 来实现, 下面我们对上文的 DEMO 进行改造, 如下代码:
- 两个
label, 一个关联到同层级的input表单, 一个关联到不存在的表单(目的是为了让input失焦) - 两个
label展示为两个按钮, 点击展开按钮,input将获取到焦点, 点击收起或者其他空白位置,input将失去焦点 - 然后通过
#show:focus ~ .content选中input获取焦点状态下的同层级兄弟元素.content, 然后为其设置max-height - 同理通过
#show:focus ~ label[for=show]选中input获取焦点状态下的同层级兄弟元素label[for=show] - 通过
#show:not(:focus) ~ label[for=hide]选中input没有获取焦点状态下的同层级兄弟元素label[for=hide]
html
<style>
.content {
width: 500px;
max-height: 46px;
overflow: hidden;
transition: all 0.4s;
margin-bottom: 20px;
}
label {
padding: 2px 10px;
border-radius: 4px;
background-color: #4096ff;
}
#show:focus ~ .content {
max-height: 400px;
}
#show:focus ~ label[for=show] {
display: none;
}
#show:not(:focus) ~ label[for=hide] {
display: none;
}
</style>
<div class="container">
<input id="show" />
<div class="content">
一个民族的复兴需要强大的物质力量,也需要强大的精神力量。党的十八大以来,以习近平同志为核心的党中央总揽全局,把宣传思想文化工作摆在重要位置,指引宣传思想文化事业在举旗定向、正本清源中取得历史性成就、发生历史性变革,在守正创新、开拓进取中展现新气象、迈向新征程。
</div>
<label for="hide">收起</label>
<label for="show">展开</label>
</div>
上面代码效果如下: 点击按钮、页面空白区域, 将改变 input 的获取焦点的状态, 长文本高度又受控于 input 的状态

3.3 回到正题
上面两小节主要就是介绍了两种 CSS 记录 UI 状态的方法, 下面回到正题: 使用 input 配合 :focus 来记录菜单展开收起的一个状态
下面直接看代码:
- 新增一个
input表单, 用于记录UI状态:input获取焦点则打开菜单栏 - 新增两个
label标签做切换按钮- 一个绑定上方
input表单, 点击将使input获取到焦点(展示菜单) - 另一个则绑定不存在的一个表单, 点击将使得
input失去焦点(隐藏菜单) - 点击空白区域(非
input标签), 将使得input失去焦点(隐藏菜单)
- 一个绑定上方
- 新增
CSS样式- 设置
label样式, 比较常规, 就不展开了 - 通过
input:focus选中获取焦点的input表单, 此时菜单应该是打开状态, 需要隐藏显示菜单的按钮(这里是通过兄弟选择器~来实现) - 通过
input:not(:focus)选中失去焦点的input表单, 此时菜单应该是关闭状态, 需要隐藏关闭菜单的按钮(这里是通过兄弟选择器~来实现)
- 设置
diff
// React 代码
export default () => (
<div className={scss.body}>
<div className={scss.mene}>
+ <input id="open" />
<div className={scss['mene-item-wrapper']}>
.....
</div>
<div className={scss['mene-toggle']}>
+ <label htmlFor="open">
+ <ToolOutlined />
+ </label>
+ <label htmlFor="close">
+ <CloseOutlined />
+ </label>
</div>
</div>
</div>
);
diff
// scss 样式
+ // 设置 label 样式
+ .mene-toggle {
+ label {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+ }
+ // 获取焦点(开启状态)
+ input:focus {
+ ~ .mene-toggle label[for="open"] {
+ display: none;
+ }
+ }
+ // 失去焦点(关闭状态)
+ input:not(:focus) {
+ ~ .mene-toggle label[for="close"] {
+ display: none;
+ }
+ }
到此, 代码效果如下:
- 点击
🔧按钮,input获取焦点, 此时会隐藏🔧按钮, 展示关闭按钮 - 点击关闭按钮,
input失去焦点, 此时会隐藏关闭按钮, 展示🔧按钮 - 点击页面空白位置(非
input),input失去焦点, 此时会隐藏关闭按钮, 展示🔧按钮

四、动画
上面我们成功记录了 UI 的一个状态, 剩下就差菜单列表隐藏、显示动画了
直接看代码:
.mene-item-wrapper主要就是设置复用的样式: 设置transform缩放的基点、设置过渡属性transitioninput:not(:focus)状态(关闭菜单)下, 通过~ .mene-item-wrapper将菜单列表通过opacity以及transform将其隐藏起来input:focus状态(关闭菜单)下, 通过~ .mene-item-wrapper将菜单显示出来
diff
// scss 样式
+ .mene-item-wrapper {
+ transition: all 0.4s;
+ transform-origin: 30px bottom;
+ }
// 获取焦点(开启状态)
input:focus {
~ .mene-toggle label[for="open"] {
display: none;
}
+ // 显示菜单
+ ~ .mene-item-wrapper {
+ opacity: 1;
+ transform: scale(1) translateY(0);
+ }
}
// 失去焦点(关闭状态)
input:not(:focus) {
~ .mene-toggle label[for="close"] {
display: none;
}
+ // 隐藏菜单
+ ~ .mene-item-wrapper {
+ opacity: 0;
+ transform: scale(0) translateY(200px);
+ }
}
最后看下实际效果:
- 点击
🔧按钮, 显示菜单 - 点击关闭按钮, 隐藏菜单
- 点击空白区域(非
input区域), 隐藏菜单

五、收尾
认真的同学, 应该会发现在菜单列表隐藏、显示过程中, 菜单列表和切换按钮层级是有问题的:

关于层级问题, 脑海里第一反应就是设置一个 z-index: 9999, 但如果真去设置了会发现并没有什么用, 甚至 z-index 属性都没生效:

如上图, 其实 z-index 属性没有生效的原因, 浏览器已经给我们提示了!!! 这里只需要为 切换按钮 设置个 position: relative; 即可, 目的是为了开启层叠上下文, 至于 z-index 其实这里并不需要再设置, 因为默认情况下, 在同层级的 DOM 列表中, 默认情况下后一个元素的层级是高于前面的元素的; 所以这里只需要做如下代码调整即可:
diff
// scss 样式
.mene-toggle {
+ position: relative;
label {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
}
这样层级问题就解决了, 来看效果:

最后, 我们还需要将 input 输入框隐藏掉, 这里方法就很多咯!! 这边我直接将其宽度、高度、边框、边距设置为 0:
diff
+ input {
+ width: 0;
+ height: 0;
+ border: 0;
+ padding: 0;
+ }
好了, 本文到处结束🔚 喜欢的点个赞、收藏呗
