v133 · css

Fan Menu

Two demos driven purely by sibling-index() and sibling-count(): a radial fan menu where each item's angular position is calc((sibling-index() - 1) * arc / sibling-count()), and a cascading list where width and animation delay are fractions of the sibling count. Add or remove items — every calculation reflows automatically with zero JavaScript.

Feature detection: checking…
180°
100px
6 items
Radial fan — position from sibling-index() / sibling-count() angle = (i−1) × 180° / 6
  • A
  • B
  • C
  • D
  • E
  • F
hub
Cascading reveal list — width & delay from sibling-index() / sibling-count()
  • 1delay: 1×80ms · width: 58%
  • 2delay: 2×80ms · width: 67%
  • 3delay: 3×80ms · width: 75%
  • 4delay: 4×80ms · width: 83%
  • 5delay: 5×80ms · width: 92%
  • 6delay: 6×80ms · width: 100%
Key CSS — everything derives from sibling-index() / sibling-count()
/* Fan menu: angular position */
.fan-item {
  --angle: calc(
    (sibling-index() - 1) * 1deg * var(--arc) / sibling-count()
  );
  transform: rotate(var(--angle)) translateY(-var(--radius));
}
.fan-item-inner {
  /* Counter-rotate to keep label upright */
  transform: rotate(calc(-1 * var(--angle)));
  /* Hue gradient across siblings */
  border-left-color: oklch(0.55 0.18 calc(sibling-index() * 240deg / sibling-count()));
}

/* Cascading list: width and animation delay */
.delay-item {
  width: calc(50% + 50% * sibling-index() / sibling-count());
  animation-delay: calc(sibling-index() * 80ms);
}
/*
  Fan positions are computed entirely in CSS.
  The only JS is adding/removing <li> elements and
  updating the --arc CSS variable from a slider.
  sibling-count() automatically updates when items are added/removed.
*/

.fan-menu {
  --arc: 180;  /* degrees — updated by JS slider */
  --radius: 100px;
}

.fan-item {
  --angle: calc(
    (sibling-index() - 1) * 1deg * var(--arc) / sibling-count()
  );
  transform: rotate(var(--angle)) translateY(calc(-1 * var(--radius)));
  transform-origin: 16px 16px; /* hub center */
}

.fan-item-inner {
  /* Counter-rotate so the label reads straight regardless of angle */
  transform: rotate(calc(-1 * var(--angle)));
}

see also