Dynamic Sibling Calculations with sibling-count() and sibling-index()
Calculate sibling counts and indices directly in CSS using sibling-count() and sibling-index() to power distinct color spectrums and staggered entry animations without JavaScript.
Key Implementation Points
Markup Structure
Semantic list elements styled dynamically without hardcoded index attributes or classes.
<main>
<div class="feed-widget">
<header class="feed-header">
<div>
<h2 class="feed-title">Activity Feed</h2>
<p class="feed-subtitle">Staggered sequential animations</p>
</div>
<button type="button" id="replay-btn" class="action-btn">Replay</button>
</header>
<ul class="animated-list is-animating" id="animated-list">
<li class="list-item">
<div class="item-badge">01</div>
<div class="item-body">
<div class="item-header">
<span class="item-title">Deployment completed</span>
<span class="item-time">Just now</span>
</div>
<p class="item-desc">Production build #142 deployed to edge servers</p>
</div>
</li>
<li class="list-item">
<div class="item-badge">02</div>
<div class="item-body">
<div class="item-header">
<span class="item-title">Review requested on PR #27</span>
<span class="item-time">4m ago</span>
</div>
<p class="item-desc">Implementation proposal ready for review</p>
</div>
</li>
<li class="list-item">
<div class="item-badge">03</div>
<div class="item-body">
<div class="item-header">
<span class="item-title">Automated backup created</span>
<span class="item-time">12m ago</span>
</div>
<p class="item-desc">Database snapshot completed successfully</p>
</div>
</li>
<li class="list-item">
<div class="item-badge">04</div>
<div class="item-body">
<div class="item-header">
<span class="item-title">Security audit passed</span>
<span class="item-time">30m ago</span>
</div>
<p class="item-desc">0 vulnerabilities detected across dependencies</p>
</div>
</li>
<li class="list-item">
<div class="item-badge">05</div>
<div class="item-body">
<div class="item-header">
<span class="item-title">Cache invalidated</span>
<span class="item-time">1h ago</span>
</div>
<p class="item-desc">Global edge caches purged after deployment</p>
</div>
</li>
</ul>
</div>
</main>
Staggered Entry Animation (sibling-index + sibling-count)
Normalize sequential animation delays across total siblings with (sibling-index() - 1) / sibling-count() so the entire sequence finishes smoothly within a consistent duration.
/* Staggered entry animation normalized across sibling-count() */
.is-animating .list-item {
animation: slideIn 0.4s cubic-bezier(0.16, 1, 0.3, 1) both;
animation-delay: calc((sibling-index() - 1) / sibling-count() * 320ms);
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (prefers-reduced-motion: reduce) {
.is-animating .list-item {
animation: none;
}
}
Dynamic Color Spectrum (sibling-count + sibling-index)
Distribute color hues evenly across the 360-degree color wheel using calc(sibling-index() * (360deg / sibling-count())), giving each sibling a distinct accent color.
/* Dynamic color spectrum: compute hue on the parent item so children inherit the resolved angle */
.list-item {
--hue: calc(sibling-index() * (360deg / sibling-count()));
border-left: 3.5px solid hsl(var(--hue), 70%, 50%);
}
.item-badge {
display: grid;
place-items: center;
width: 30px;
height: 30px;
border-radius: 0.375rem;
font-family: ui-monospace, monospace;
font-size: 0.75rem;
font-weight: 700;
color: #ffffff;
background-color: hsl(var(--hue), 70%, 45%);
flex-shrink: 0;
} <main>
<div class="feed-widget">
<header class="feed-header">
<div>
<h2 class="feed-title">Activity Feed</h2>
<p class="feed-subtitle">Staggered sequential animations</p>
</div>
<button type="button" id="replay-btn" class="action-btn">Replay</button>
</header>
<ul class="animated-list is-animating" id="animated-list">
<li class="list-item">
<div class="item-badge">01</div>
<div class="item-body">
<div class="item-header">
<span class="item-title">Deployment completed</span>
<span class="item-time">Just now</span>
</div>
<p class="item-desc">Production build #142 deployed to edge servers</p>
</div>
</li>
<li class="list-item">
<div class="item-badge">02</div>
<div class="item-body">
<div class="item-header">
<span class="item-title">Review requested on PR #27</span>
<span class="item-time">4m ago</span>
</div>
<p class="item-desc">Implementation proposal ready for review</p>
</div>
</li>
<li class="list-item">
<div class="item-badge">03</div>
<div class="item-body">
<div class="item-header">
<span class="item-title">Automated backup created</span>
<span class="item-time">12m ago</span>
</div>
<p class="item-desc">Database snapshot completed successfully</p>
</div>
</li>
<li class="list-item">
<div class="item-badge">04</div>
<div class="item-body">
<div class="item-header">
<span class="item-title">Security audit passed</span>
<span class="item-time">30m ago</span>
</div>
<p class="item-desc">0 vulnerabilities detected across dependencies</p>
</div>
</li>
<li class="list-item">
<div class="item-badge">05</div>
<div class="item-body">
<div class="item-header">
<span class="item-title">Cache invalidated</span>
<span class="item-time">1h ago</span>
</div>
<p class="item-desc">Global edge caches purged after deployment</p>
</div>
</li>
</ul>
</div>
</main>
<style>
@property --hue {
syntax: "<angle>";
inherits: true;
initial-value: 0deg;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
color: #0f172a;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
main {
display: grid;
place-items: center;
min-height: 100vh;
padding: 2rem 1rem;
}
.feed-widget {
width: 100%;
max-width: 440px;
background-color: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 0.75rem;
box-shadow:
0 1px 3px 0 rgba(0, 0, 0, 0.05),
0 1px 2px -1px rgba(0, 0, 0, 0.05);
padding: 1.5rem;
}
.feed-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.25rem;
}
.feed-title {
font-size: 1.125rem;
font-weight: 700;
color: #0f172a;
line-height: 1.25;
}
.feed-subtitle {
font-size: 0.8125rem;
color: #64748b;
margin-top: 0.125rem;
}
.action-btn {
padding-block: 0.375rem;
padding-inline: 0.75rem;
font-size: 0.8125rem;
font-weight: 600;
color: #111827;
background-color: #ffffff;
cursor: pointer;
border-radius: 9999px;
border: 1px solid #d1d5db;
transition: background-color 0.15s ease, border-color 0.15s ease;
}
.action-btn:hover {
background-color: #f9fafb;
border-color: #9ca3af;
}
.animated-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 0.625rem;
}
.list-item {
display: flex;
align-items: flex-start;
gap: 0.875rem;
padding: 0.75rem 0.875rem;
background-color: #f8fafc;
border: 1px solid #f1f5f9;
border-radius: 0.5rem;
transition: background-color 0.15s ease, border-color 0.15s ease;
}
.list-item:hover {
background-color: #f1f5f9;
border-color: #e2e8f0;
}
/* Dynamic color spectrum: compute hue on the parent item so children inherit the resolved angle */
.list-item {
--hue: calc(sibling-index() * (360deg / sibling-count()));
border-left: 3.5px solid hsl(var(--hue), 70%, 50%);
}
.item-badge {
display: grid;
place-items: center;
width: 30px;
height: 30px;
border-radius: 0.375rem;
font-family: ui-monospace, monospace;
font-size: 0.75rem;
font-weight: 700;
color: #ffffff;
background-color: hsl(var(--hue), 70%, 45%);
flex-shrink: 0;
}
.item-body {
flex: 1;
min-width: 0;
}
.item-header {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 0.5rem;
}
.item-title {
font-size: 0.875rem;
font-weight: 600;
color: #1e293b;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.item-time {
font-size: 0.75rem;
color: #94a3b8;
flex-shrink: 0;
}
.item-desc {
font-size: 0.8125rem;
color: #64748b;
margin-top: 0.125rem;
line-height: 1.35;
}
/* Staggered entry animation normalized across sibling-count() */
.is-animating .list-item {
animation: slideIn 0.4s cubic-bezier(0.16, 1, 0.3, 1) both;
animation-delay: calc((sibling-index() - 1) / sibling-count() * 320ms);
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (prefers-reduced-motion: reduce) {
.is-animating .list-item {
animation: none;
}
}
</style>
<script>
// Replay button logic
const replayBtn = document.getElementById('replay-btn');
const list = document.getElementById('animated-list');
if (replayBtn && list) {
replayBtn.addEventListener('click', () => {
list.classList.remove('is-animating');
void list.offsetWidth; // Trigger reflow to restart animation
list.classList.add('is-animating');
});
}
</script>