本教程教你用 100% 纯 CSS 在 Bricks 里实现内容切换器(content switcher)——在多个内容区块间切换显示,典型场景如价格表(pricing tables)切换、功能特性切换,不需要任何 JS。
如果你想要 Bricks 内置的集成方案,可以直接购买 BricksExtras 插件;但如果你愿意动手写自定义 CSS,这篇教程正合适。
DOM 结构
先在 Bricks 里搭出下面的 DOM 结构,给每个元素按如下规则分配 class:
-
第一个元素是代码块(放 HTML)
-
revealed-items可以是任意元素(就是被切换显示的内容块)
纯 CSS 方法
HTML
把下面这段 HTML 粘贴到代码块里:
<fieldset class="radio-switch">
<!-- First Item -->
<input type="radio" name="switch" id="item1">
<label for="item1">Label 1</label>
<!-- Second Item -->
<input type="radio" name="switch" id="item2" checked>
<label for="item2">Label 2</label>
<!-- Third Item -->
<input type="radio" name="switch" id="item3">
<label for="item3">Label 3</label>
<div class="highlight"></div>
</fieldset>
标签文字(Label)可以按需修改,但不要改动属性,否则动画会失效。示例用了 3 个标签/输入项,CSS 原生支持到 4 个——想增减就按需增删条目。
第二个标签默认是选中态,因为它的 <input> 末尾带了 checked 属性;想默认选中哪个,就把 checked 移到哪个条目上。
CSS
把下面的 CSS 粘贴到页面:
/* CSS Variables */
fieldset.radio-switch {
--switch-item-number: 3;
--switch-padding: 5px;
--switch-bg: #fff;
--switch-border-width: 1px;
--switch-border-color: #ccc;
--switch-border-radius: 50px;
--switch-label-padding: 1.5rem 0.5rem;
--switch-inactive-text-color: #666;
--switch-active-text-color: #fff;
--switch-active-bg-color: #350792;
}
/* Fieldset*/
fieldset.radio-switch {
display: flex;
position: relative;
padding: 0;
outline: 0;
border: var(--switch-border-width) solid var(--switch-border-color);
background-color: var(--switch-bg);
border-radius: var(--switch-border-radius);
overflow: hidden;
width: 100%;
}
/* Hide Imputs*/
fieldset.radio-switch input {
visibility: hidden;
opacity: 0;
display: none !important;
}
/* Labels */
fieldset.radio-switch label {
z-index: 1;
margin: 0;
padding: var(--switch-label-padding);
border-radius: var(--switch-border-radius);
cursor: pointer;
color: var(--switch-inactive-text-color);
font-size: 14px;
line-height: 1.4em;
font-weight: 600;
}
/* Active Labels*/
fieldset.radio-switch input:checked+label {
color: var(--switch-active-text-color);
cursor: default;
}
/* Highlight */
fieldset.radio-switch label,
fieldset.radio-switch .highlight {
width: calc(100% / var(--switch-item-number));
display: flex;
align-items: center;
justify-content: center;
text-align: center;
transition: 300ms transform ease-in-out,
300ms color ease-in-out;
}
fieldset.radio-switch .highlight {
position: absolute;
background-color: var(--switch-active-bg-color);
top: calc(var(--switch-padding) / 2);
left: calc(var(--switch-padding) / 2);
bottom: 0;
right: 0;
border-radius: var(--switch-border-radius);
height: calc(100% - var(--switch-padding));
width: calc((100% / var(--switch-item-number)) - calc(var(--switch-padding) / var(--switch-item-number)));
}
/* Moving Highliht*/
fieldset.radio-switch input:nth-of-type(2):checked~.highlight {
transform: translateX(100%);
}
fieldset.radio-switch input:nth-of-type(3):checked~.highlight {
transform: translateX(200%);
}
fieldset.radio-switch input:nth-of-type(4):checked~.highlight {
transform: translateX(300%);
}
/* Revealed Items */
body.bricks-is-frontend .revealed-items {
display: none !important;
}
/* Active Reveald Item */
body.bricks-is-frontend .switcher-wrapper:has(input:nth-of-type(1):checked)~.revealed-items.item1,
body.bricks-is-frontend .switcher-wrapper:has(input:nth-of-type(2):checked)~.revealed-items.item2,
body.bricks-is-frontend .switcher-wrapper:has(input:nth-of-type(3):checked)~.revealed-items.item3,
body.bricks-is-frontend .switcher-wrapper:has(input:nth-of-type(4):checked)~.revealed-items.item4 {
display: flex !important;
animation: 300ms activeItems ease-in-out;
}
/* Custom Entrance Animation */
@keyframes activeItems {
0% {
transform: translateY(40px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
注意:第一段是 CSS 变量,方便你按需调整样式。其中 --switch-item-number 是必填变量,代表切换器里的标签数量——示例用了 3 个标签所以填 3;增删 HTML 条目时记得同步修改这个数字。
移动端竖排(可选)
想在手机上把切换器竖着排列,追加这段 CSS:
@media screen and (max-width: 767px) {
fieldset.radio-switch {
--switch-border-radius: 15px;
flex-direction: column;
}
/* Highlight */
fieldset.radio-switch .highlight,
fieldset.radio-switch label {
width: calc(100% - var(--switch-padding));
height: calc((100% / var(--switch-item-number)) - calc(var(--switch-padding) / var(--switch-item-number)));
}
/* Moving Highliht*/
fieldset.radio-switch input:nth-of-type(2):checked~.highlight {
transform: translateY(100%);
}
fieldset.radio-switch input:nth-of-type(3):checked~.highlight {
transform: translateY(200%);
}
fieldset.radio-switch input:nth-of-type(4):checked~.highlight {
transform: translateY(300%);
}
}
Firefox 兼容
早期 Firefox 不支持 :has() 伪类,需要做降级处理:先把默认显示的 revealed item 加上 .active 类,然后修改 CSS(把 :has() 那几行注释掉,改用 .active 类触发显示),最后加一段 JS 在点击时切换 .active 类:
/* Active Reveald Item */
/*
body.bricks-is-frontend .switcher-wrapper:has(input:nth-of-type(1):checked)~.revealed-items.item1,
body.bricks-is-frontend .switcher-wrapper:has(input:nth-of-type(2):checked)~.revealed-items.item2,
body.bricks-is-frontend .switcher-wrapper:has(input:nth-of-type(3):checked)~.revealed-items.item3,
body.bricks-is-frontend .switcher-wrapper:has(input:nth-of-type(4):checked)~.revealed-items.item4 {
display: flex !important;
animation: 300ms activeItems ease-in-out;
}*/
body.bricks-is-frontend .revealed-items.active{
display: flex !important;
animation: 300ms activeItems ease-in-out;
}
// Load the script when the DOM is loaded
window.addEventListener('DOMContentLoaded', () => {
// Query all the switchers on the page
const radioGroups = document.querySelectorAll('.switcher-wrapper');
// stop the script if there is no switcher
if (radioGroups.length < 1) return;
// Function to get all the siblings of an element
const getSiblings = (elem) => {
return Array.prototype.filter.call(elem.parentNode.children, (sibling) => {
return sibling !== elem;
});
};
// Loop in each switcher groups
radioGroups.forEach((radioGroup) => {
// Query the inputs
const radios = radioGroup.querySelectorAll('input');
// Query the revealed items
const revealedItems = getSiblings(radioGroup);
// Loop into each radio groups
radios.forEach((radio, index) => {
// Listener on click
radio.addEventListener('click', () => {
// Get the revealed item
const revealedItem = revealedItems[index];
// Remove the active class on all revealed items
revealedItems.forEach(el => el.classList.remove('active'));
// Add the active class to the active revealed item
revealedItem.classList.add('active');
})
})
})
})
💡 译者注:Firefox 从 121 版(2023 年 12 月)起已原生支持
:has(),现在的 Firefox 不再需要这段降级代码——保留它主要兼容旧版浏览器。
相关阅读:
适用场景与方案取舍
纯 CSS 切换器适合内容固定、样式统一的场景:价格表按月/年切换、功能特性分组、FAQ 标签页这类「切换的是内容块、不涉及数据请求」的需求,零 JS、零插件依赖,首屏性能最干净。
反过来,如果切换内容来自后台数据、需要异步加载,或者要联动其它元素(比如切换时改变图表、联动筛选),那用 BricksExtras 的集成方案或 JS 更合适。取舍标准就一条:切换的是静态内容还是动态逻辑——静态走 CSS,动态上插件/JS。
结论
设置正确的话,前端应该能看到:点击标签 → 高亮滑块平滑滑动 → 对应内容块淡入显示。
需要提醒:这个方案开箱即用不满足无障碍要求(accessibility)。如果你的项目强制要求无障碍,请换用其他方案,或自行补充额外工作。
常见问题(FAQ)
Q:最多支持几个标签?
A:这套 CSS 原生支持到 4 个标签项,示例用的是 3 个。想增减就按需增删 HTML 条目,同时记得把 CSS 变量 --switch-item-number 改成实际数量。
Q:怎么改默认选中的标签?
A:默认选中哪个,就把 checked 属性加到哪个 <input> 上(示例默认选中第二个)。
Q::has() 兼容性怎么样?
A:Firefox 121 起(2023 年 12 月)已原生支持 :has(),现代浏览器都没问题;旧版浏览器需要文中的降级方案(.active 类 + 一段 JS)。
Q:移动端怎么竖排?
A:文中有现成的媒体查询 CSS,max-width: 767px 下自动变为竖排布局,直接追加即可。
Q:这个方案有坑吗? A:开箱即用不满足无障碍(accessibility)要求,强制无障碍的项目请换方案或自行补足。
延伸阅读
- 想要现成外贸独立站模板与建站实战?看 铲子铺 chanzipu 的外贸建站教程(同用 Bricks,从零搭出能收询盘的站)
- 相关阅读:Bricks Setup Guide
延伸阅读
Bricks Accordion (Nestable) 图标旋转:展开/收起状态箭头切换
实战技巧:Bricks Accordion (Nestable) 图标旋转:展开/收起状态箭头切换——accordion 实操步骤+踩坑提醒,适合外贸独立站与 WordPress 开发者。
tips在 Bricks 中使用 IsotopeJS 创建筛选器(第四部分):AJAX 筛选与无限循环、排序、列表视图与禁用动画
实战技巧:在 Bricks 中使用 IsotopeJS 创建筛选器(第四部分):AJAX 筛选与无限循环、排序、列表视图与禁用动画——animation 实操步骤+踩坑提醒,适合外贸独立站与 WordPress 开发者。
tipsBricks 中同步两个可嵌套轮播(主轮播 + 缩略图联动)
实战技巧:Bricks 中同步两个可嵌套轮播(主轮播 + 缩略图联动)——slider 实操步骤+踩坑提醒,适合外贸独立站与 WordPress 开发者。
tipsBricks 手风琴(Nestable):第一项桌面端展开、移动端收起
实战技巧:Bricks 手风琴(Nestable):第一项桌面端展开、移动端收起——Bricks 实操步骤+踩坑提醒,适合外贸独立站与 WordPress 开发者。
