#Sibling Index Playground
交互式体验 CSS sibling-index() 和 sibling-count() 函数,学习如何使用兄弟元素索引创建动态样式效果。
- Chrome 138+ · 完全支持
- Edge 138+ · 完全支持
- Safari 26.2+ · 完全支持
- Firefox · 暂不支持
此组件使用 CSS sibling-index() 和 sibling-count() 函数。如使用不支持此功能的浏览器,部分效果可能无法正常显示。
#什么是 sibling-index() 和 sibling-count()?
这两个 CSS 函数让你能够基于元素在兄弟元素中的位置和总数来动态计算样式值:
sibling-index(): 返回元素在兄弟元素中的索引(从 1 开始)sibling-count(): 返回兄弟元素的总数(包括自身)
通过这两个函数,你可以实现很多以前需要 JavaScript 或手动编写 :nth-child() 选择器才能完成的效果。
#示例演示
#1. 交错动画 (Stagger Animation)
使用 sibling-index() 为每个元素设置递增的延迟时间,创建波浪式动画效果。Hover 卡片可翻转查看背面,点击「播放动画」重新触发动画。
import React, { useState } from 'react';
import { Button, InputNumber, Slider } from 'antd';
import '@demos/playground/sibling-index/stagger-animation.less';
const Demo: React.FC = () => {
const [staggerDelay, setStaggerDelay] = useState(50);
const [staggerRotate, setStaggerRotate] = useState(3);
const [animationKey, setAnimationKey] = useState(0);
const triggerAnimation = () => {
setAnimationKey((prev) => prev + 1);
};
return (
<div className="space-y-4">
<div className="grid @lg:grid-cols-[1fr_auto] grid-cols-1 gap-6">
<div className="flex h-full flex-wrap content-start gap-4">
{Array.from({ length: 12 }).map((_, i) => {
const itemKey = `stagger-${animationKey}-${i}`;
return (
<div
key={itemKey}
className="stagger-card"
style={
{
'--stagger-delay': staggerDelay,
'--stagger-rotate': staggerRotate,
'--item-index': i + 1,
} as React.CSSProperties
}
>
<div className="stagger-card-inner">
<div className="stagger-card-front">{i + 1}</div>
<div className="stagger-card-back">✨</div>
</div>
</div>
);
})}
</div>
<div className="flex @lg:w-80 w-full flex-col gap-4">
<Button type="primary" onClick={triggerAnimation} block size="large">
播放动画
</Button>
<div className="card-default-border">
<div className="card-header">
<div className="card-header-title">参数配置</div>
</div>
<div className="space-y-4">
<div className="space-y-2">
<div className="flex items-center justify-between">
<label htmlFor="stagger-delay-input" className="font-medium text-gray-8 text-sm dark:text-white">
延迟时间
</label>
<InputNumber
id="stagger-delay-input"
value={staggerDelay}
onChange={(val) => setStaggerDelay(val || 50)}
min={10}
max={200}
step={10}
size="small"
className="w-25"
suffix="ms"
/>
</div>
<Slider
value={staggerDelay}
onChange={setStaggerDelay}
min={10}
max={200}
step={10}
tooltip={{ formatter: (val) => `${val}ms` }}
/>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<label htmlFor="stagger-rotate-input" className="font-medium text-gray-8 text-sm dark:text-white">
旋转角度
</label>
<InputNumber
id="stagger-rotate-input"
value={staggerRotate}
onChange={(val) => setStaggerRotate(val || 3)}
min={0}
max={15}
step={1}
size="small"
className="w-25"
suffix="deg"
/>
</div>
<Slider
value={staggerRotate}
onChange={setStaggerRotate}
min={0}
max={15}
step={1}
tooltip={{ formatter: (val) => `${val}deg` }}
/>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default Demo;
.stagger-card {
width: 90px;
height: 90px;
perspective: 1000px;
opacity: 1;
transform: translateY(0) scale(1);
transition:
opacity 0.6s cubic-bezier(0.4, 0, 0.2, 1),
transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
transition-delay: calc((var(--item-index, 1) - 1) * var(--stagger-delay, 50) * 1ms);
&:hover .stagger-card-inner {
transform: rotateY(180deg);
}
}
@supports (animation-timeline: scroll()) {
@starting-style {
.stagger-card {
opacity: 0;
transform: translateY(20px) scale(0.8) rotate(calc((var(--item-index, 1) - 1) * var(--stagger-rotate, 3) * 1deg));
}
}
}
.stagger-card-inner {
position: relative;
width: 100%;
height: 100%;
transition: transform 0.6s;
transform-style: preserve-3d;
}
.stagger-card-front,
.stagger-card-back {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
display: flex;
align-items: center;
justify-content: center;
border-radius: 16px;
font-weight: 700;
font-size: 1.75rem;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.stagger-card-front {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.stagger-card-back {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
transform: rotateY(180deg);
}
使用 sibling-index() 为每个卡片计算独立的 transition-delay,实现交错入场效果。结合 @starting-style 定义元素的初始状态(透明度为 0、向下偏移 20px、缩放 0.8、根据索引旋转),配合 transition 属性实现平滑过渡动画。3D 翻转效果通过 perspective + preserve-3d + rotateY(180deg) 实现,Hover 时卡片正反面翻转。
#2. 渐变文字效果 (Gradient Text)
结合 oklch() 颜色函数,根据元素索引自动生成渐变色文字和柱状图效果。
import React, { useState } from 'react';
import { InputNumber, Slider } from 'antd';
import '@demos/playground/sibling-index/gradient-text.less';
const Demo: React.FC = () => {
const [hueStep, setHueStep] = useState(30);
const text = 'SIBLING INDEX MAGIC';
const chars = text.split('');
return (
<div className="space-y-6">
<div className="grid @lg:grid-cols-[1fr_auto] grid-cols-1 gap-6">
<div className="flex h-full flex-col items-center justify-center gap-8">
<div className="gradient-text-container flex flex-wrap justify-center gap-2">
{chars.map((char, i) => {
const charKey = char === ' ' ? `space-${hueStep}-${i}` : `char-${hueStep}-${i}-${char}`;
if (char === ' ') {
return <div key={charKey} className="w-4" />;
}
return (
<div
key={charKey}
className="gradient-char"
style={
{
'--hue-step': hueStep,
'--char-index': i + 1,
} as React.CSSProperties
}
>
{char}
</div>
);
})}
</div>
<div className="gradient-bars flex gap-3">
{Array.from({ length: 8 }, (_, i) => {
const barKey = `bar-${hueStep}-${i}`;
return (
<div
key={barKey}
className="gradient-bar"
style={
{
'--hue-step': hueStep,
'--bar-index': i + 1,
} as React.CSSProperties
}
/>
);
})}
</div>
</div>
<div className="flex @lg:w-80 w-full flex-col gap-4">
<div className="card-default-border">
<div className="card-header">
<div className="card-header-title">参数配置</div>
</div>
<div className="space-y-4">
<div className="space-y-2">
<div className="flex items-center justify-between">
<label htmlFor="hue-step-input" className="font-medium text-gray-8 text-sm dark:text-white">
色相步进
</label>
<InputNumber
id="hue-step-input"
value={hueStep}
onChange={(val) => setHueStep(val || 30)}
min={10}
max={60}
step={5}
size="small"
className="w-25"
/>
</div>
<Slider value={hueStep} onChange={setHueStep} min={10} max={60} step={5} />
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default Demo;
.gradient-char {
font-size: 2rem;
font-weight: 900;
letter-spacing: 0.05em;
opacity: 1;
transform: translateY(0) scale(1);
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
transition-delay: calc((var(--char-index, 1) - 1) * 30ms);
background: linear-gradient(
135deg,
oklch(70% 0.25 calc((var(--char-index, 1) - 1) * var(--hue-step, 30))),
oklch(60% 0.2 calc((var(--char-index, 1) - 1) * var(--hue-step, 30) + 60))
);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
&:hover {
transform: translateY(-4px) scale(1.2);
}
}
@supports (animation-timeline: scroll()) {
@starting-style {
.gradient-char {
opacity: 0;
transform: translateY(20px) scale(0.5);
}
}
}
.gradient-bars {
width: 100%;
max-width: 400px;
height: 80px;
align-items: flex-end;
}
.gradient-bar {
flex: 1;
min-width: 0;
border-radius: 8px 8px 0 0;
background: linear-gradient(
to top,
oklch(70% 0.25 calc((var(--bar-index, 1) - 1) * var(--hue-step, 30))),
oklch(80% 0.2 calc((var(--bar-index, 1) - 1) * var(--hue-step, 30) + 40))
);
height: calc(30px + (var(--bar-index, 1) * 6px));
opacity: 1;
transform: scaleY(1);
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
transition-delay: calc((var(--bar-index, 1) - 1) * 50ms);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
&:hover {
transform: scaleY(1.1);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
}
@supports (animation-timeline: scroll()) {
@starting-style {
.gradient-bar {
opacity: 0;
transform: scaleY(0);
}
}
}
使用 sibling-index() 为每个字符计算索引值,结合 oklch() 色彩空间,通过 calc((var(--char-index) - 1) * var(--hue-step)) 计算色相值,实现基于位置的渐变色效果。oklch() 相比 hsl() 颜色过渡更自然均匀。文字渐变通过 background-clip: text + -webkit-text-fill-color: transparent 实现。柱状图的高度同样基于 sibling-index() 计算,配合 @starting-style 实现从底部升起的动画效果。
#3. 卡牌扇形布局 (Deck of Cards)
使用 sibling-index() 和 sibling-count() 计算每张卡片的旋转角度,实现手札般的扇形排列。删除卡片后,剩余卡片会自动重新计算位置。
import React, { useState } from 'react';
import { Button, InputNumber, Slider } from 'antd';
import '@demos/playground/sibling-index/deck-of-cards.less';
const Demo: React.FC = () => {
const [cardRotateStep, setCardRotateStep] = useState(20);
const [cards, setCards] = useState([1, 2, 3, 4, 5, 6, 7]);
const removeCard = (index: number) => {
setCards(cards.filter((_, i) => i !== index));
};
const resetCards = () => {
setCards([1, 2, 3, 4, 5, 6, 7]);
};
return (
<div className="space-y-6">
<div className="min-h-[480px] overflow-hidden rounded-xl bg-gradient-to-br from-orange-50 to-pink-50 p-8 dark:from-gray-800 dark:to-gray-900">
<div className="deck-container relative mx-auto" style={{ width: '100%', height: '400px' }}>
<div className="deck-cards flex items-center justify-center" style={{ height: '100%' }}>
{cards.map((card, index) => (
<div
key={card}
className="deck-card group"
style={
{
'--card-rotate-step': cardRotateStep,
'--card-index': index + 1,
'--card-count': cards.length,
} as React.CSSProperties
}
>
<div className="deck-card-content">
<div className="flex h-full flex-col items-center justify-center gap-2">
<div className="font-bold text-5xl text-gray-8 dark:text-white">{card}</div>
<div className="text-gray-5 text-xs dark:text-white-5">CARD #{card}</div>
</div>
<button
className="deck-card-close opacity-0 transition-opacity group-hover:opacity-100"
onClick={() => removeCard(index)}
type="button"
aria-label="删除卡片"
>
✕
</button>
</div>
</div>
))}
</div>
</div>
</div>
<div className="flex gap-3">
<Button onClick={resetCards} disabled={cards.length === 7}>
重置卡片
</Button>
<div className="flex items-center text-gray-6 text-sm dark:text-white-6">
Hover 卡片查看删除按钮,点击可删除,观察剩余卡片自动重排
</div>
</div>
<div className="card-default-border">
<div className="card-header">
<div className="card-header-title">参数配置</div>
</div>
<div className="space-y-4">
<div className="space-y-2">
<div className="flex items-center justify-between">
<label htmlFor="card-rotate-step-input" className="font-medium text-gray-8 text-sm dark:text-white">
旋转步进角度
</label>
<InputNumber
id="card-rotate-step-input"
value={cardRotateStep}
onChange={(val) => setCardRotateStep(val || 20)}
min={10}
max={40}
step={5}
size="small"
className="w-25"
suffix="deg"
/>
</div>
<Slider
value={cardRotateStep}
onChange={setCardRotateStep}
min={10}
max={40}
step={5}
tooltip={{ formatter: (val) => `${val}deg` }}
/>
</div>
</div>
</div>
</div>
);
};
export default Demo;
.deck-cards {
position: relative;
}
.deck-card {
position: absolute;
width: 140px;
height: 200px;
left: 50%;
bottom: 0;
transform-origin: center bottom;
transform: translateX(-50%)
rotate(calc((var(--card-index, 1) - (var(--card-count, 7) + 1) / 2) * var(--card-rotate-step, 20) * 1deg))
translateY(-160px);
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
&:hover {
transform: translateX(-50%)
rotate(calc((var(--card-index, 1) - (var(--card-count, 7) + 1) / 2) * var(--card-rotate-step, 20) * 1deg))
translateY(-180px);
z-index: 10;
}
}
.deck-card-content {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #fff 0%, #fafafa 100%);
border: 2px solid #e5e7eb;
border-radius: 16px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
position: relative;
transition: all 0.3s ease;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #f093fb 0%, #f5576c 50%, #4facfe 100%);
}
.dark & {
background: linear-gradient(135deg, rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.4) 100%);
border-color: rgba(255, 255, 255, 0.15);
}
.deck-card:hover & {
border-color: #fa6323;
box-shadow: 0 12px 32px rgba(250, 99, 35, 0.3);
}
}
.deck-card-close {
position: absolute;
top: 12px;
right: 12px;
width: 28px;
height: 28px;
border-radius: 50%;
background: linear-gradient(135deg, #ff3325 0%, #ff5722 100%);
color: white;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: 700;
transition: transform 0.2s ease;
box-shadow: 0 2px 8px rgba(255, 51, 37, 0.3);
&:hover {
transform: scale(1.15) rotate(90deg);
}
}
使用 sibling-index() 和 sibling-count() 计算每张卡片的旋转角度。
公式: (sibling-index() - (sibling-count() + 1) / 2) * 角度
中央卡片旋转 0°,左右对称展开,卡片数量变化时自动重新计算。例如 7 张卡片时:中央位置为第 4 张 = (7+1)/2,第 1 张旋转 -60deg,第 4 张旋转 0deg,第 7 张旋转 60deg。通过设置 transform-origin: center bottom 使卡片以底部中心为旋转轴,实现扇形展开效果。
