Build avatar stacks, notification piles, image strips, and radial menus with a token-driven CSS architecture—rest vs open states, no JavaScript for the interaction—using the overlapui FSCSS module.
Reference: overlapui.fscss
Stacked faces in a members column, cards that peek under each other, photos that share one edge, or a center control that fans into actions: that look is usually a pile of one-off rules. overlapui treats it as a small architecture—shared --ou-* tokens, four cascade patterns, hover and focus-within open states—and compiles to plain CSS.
This article is description- and code-based: how the layers fit, with copy-paste examples. The only project reference is the module itself.
What “cascaded UI” means here
A cascade has two densities:
Rest — children share space (negative margin, peek, or stacked under a hub).
Open — :hover or :focus-within restores gap, full card height, or a radial orbit.
Tokens describe density and chrome. Mixins encode the rest/open contract. Markup stays light lists, card roots, or a center-plus-satellites control.
| Pattern | Rest | Open |
| Avatar | horizontal pull-in | sibling gap |
| Card | vertical peek | expanded margins |
| Image | overlap + tilt | align + gap |
| Circle | satellites hidden under center | polar fan-out |
Setup
Use FSCSS 1.2.3+ (1.2.5+ recommended). Load the runtime with defer or async when prototyping.
<script src="https://cdn.jsdelivr.net/npm/fscss@1.2.5/runtime.min.js" defer></script>
@import((*) from overlapui)
@ou-root()
@avatar-overlap(.avatar-overlap)
@card-overlap(.card-overlap)
@img-overlap(.img-overlap)
@circle-overlap(.circle-overlap)
@ou-reduced-motion()
One-shot install (default class names):
@import((*) from overlapui)
@overlapui()
Selective import:
@import((ou-root, ou-avatar-colors avatar-overlap, circle-overlap, ou-reduced-motion) from overlapui)
@ou-root()
@avatar-overlap(.team)
@circle-overlap(.fab)
@ou-reduced-motion()
Layer 1 — tokens (--ou-*)
Theme the product by overriding tokens after @ou-root(), not by forking mixin internals.
@ou-root()
:root {
--ou-ring: #0f172a;
--ou-time: 0.35s;
--ou-ease: cubic-bezier(0.3, 1.3, 0.5, 1);
--ou-center-bg: #6366f1;
@ou-avatar-size(44px, -14px)
@ou-card-size(34px, 76px)
@ou-img-size(96px, -52px)
@ou-circle-size(48px, 80px)
}
| Token area | Variables (examples) |
| Motion / chrome | --ou-time, --ou-ease, --ou-ring, --ou-ring-width |
| Avatar | --ou-size, --ou-overlap, --ou-gap, --ou-c, --ou-letter |
| Card | --ou-peek, --ou-card-h, --ou-card-gap, --ou-card-bg, --ou-card-title |
| Image | --ou-img, --ou-img-overlap, --ou-img-gap, --ou-img-tilt |
| Circle | --ou-item, --ou-r, --ou-center-bg, --n |
Dense UI (tables, toolbars): smaller size, stronger overlap.
:root {
@ou-avatar-size(28px, -10px)
}
Layer 2 — avatar cascade
Role: presence clusters—members, assignees, “also on this thread.”
<ul class="avatar-overlap" tabindex="0">
<li data-alph="g">Gina Park</li>
<li data-alph="h">
<img src="https://i.pravatar.cc/80?img=12" alt="">
Hank Lee
</li>
<li data-alph="5">5001</li>
</ul>
@ou-root()
@avatar-overlap(.avatar-overlap)
Behavior encoded by the module:
Rest: each face after the first uses margin-left: var(--ou-overlap).
Open: li + li uses var(--ou-gap).
Letter from data-alph via ::before; font-size: 0 on the face keeps names in the DOM for assistive tech.
Optional img covers the letter; letter remains fallback.
A–Z / 0–9 map to a palette through FSCSS arrays at compile time.
Custom selector:
@avatar-overlap(.members)
<ul class="members" tabindex="0">
<li data-alph="a">Ada</li>
<li data-alph="n">Nia</li>
</ul>
Layer 3 — card cascade
Role: notification piles, activity stacks, ticket previews without a drawer component.
<div class="card-overlap" tabindex="0">
<div>
<img src="https://picsum.photos/96?random=1" alt="">
<h3>Invoice paid</h3>
Acme Corp · $2,400
</div>
<div>
<img src="https://picsum.photos/96?random=2" alt="">
<h3>Seat added</h3>
workspace update
</div>
<div>
<img src="https://picsum.photos/96?random=3" alt="">
<h3>Comment</h3>
“Ship the cascade”
</div>
</div>
@card-overlap(.card-overlap)
:root {
--ou-peek: 34px;
--ou-card-h: 76px;
--ou-card-gap: 10px;
--ou-card-bg: #1f2a48;
--ou-card-title: #fff;
}
Rest uses a negative top margin derived from peek and card height so only a band of each card shows. Open restores var(--ou-card-gap) between siblings. Prefer animating margin/transform over toggling display.
Layer 4 — image cascade
Role: editorial strips, gallery teasers, “recent media.”
<div class="img-overlap" tabindex="0">
<img src="https://picsum.photos/120?random=4" alt="">
<img src="https://picsum.photos/120?random=5" alt="">
<img src="https://picsum.photos/120?random=6" alt="">
</div>
@img-overlap(.img-overlap)
:root {
@ou-img-size(96px, -48px)
--ou-img-tilt: -5deg;
--ou-img-tilt-even: 4deg;
}
Rest: overlap plus alternating tilt. Open: rotate(0) and gap between images.
Layer 5 — circle cascade
Role: settings hub, FAB-style actions, compact toolbars.
<div class="circle-overlap" style="--n:5" tabindex="0">
<button type="button" aria-label="Settings menu">+</button>
<a href="#profile" aria-label="Profile">P</a>
<a href="#billing" aria-label="Billing">B</a>
<a href="#team" aria-label="Team">T</a>
<a href="#api" aria-label="API keys">K</a>
<a href="#logout" aria-label="Sign out">×</a>
</div>
@circle-overlap(.circle-overlap)
:root {
@ou-circle-size(48px, 80px)
--ou-center-bg: #6366f1;
}
Rules of the pattern:
First child is the center control (background --ou-center-bg).
--n is the number of satellites (exclude the center).
Satellites sit under the center at rest (opacity: 0, scaled down), then fan with angle 360deg / var(--n) * var(--i).
Built-in --i / --c defaults apply per :nth-child; override --c when needed.
Topbar-friendly footprint:
.settings-wrap {
position: relative;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
.settings-orbit {
--n: 5;
--ou-item: 40px;
--ou-r: 70px;
margin: 0;
width: var(--ou-item);
height: var(--ou-item);
}
<div class="settings-wrap">
<div class="settings-orbit circle-overlap" style="--n:5" tabindex="0">
<button type="button" aria-label="Settings">+</button>
<a href="#profile" aria-label="Profile">P</a>
<a href="#billing" aria-label="Billing">B</a>
<a href="#team" aria-label="Team">T</a>
<a href="#api" aria-label="API">K</a>
<a href="#out" aria-label="Sign out">×</a>
</div>
</div>
Minimal full page (all four patterns)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>overlapui cascades</title>
<script src="https://cdn.jsdelivr.net/npm/fscss@1.2.5/runtime.min.js" defer></script>
<style>
@import((*) from overlapui)
@ou-root()
@avatar-overlap(.avatar-overlap)
@card-overlap(.card-overlap)
@img-overlap(.img-overlap)
@circle-overlap(.circle-overlap)
@ou-reduced-motion()
:root {
--ou-ring: #0f172a;
--ou-center-bg: #6366f1;
@ou-avatar-size(44px, -14px)
@ou-circle-size(48px, 80px)
}
body {
margin: 0;
min-height: 100vh;
display: grid;
gap: 48px;
place-content: center;
padding: 32px 16px;
background: #0b1220;
font-family: system-ui, sans-serif;
color: #e2e8f0;
}
</style>
</head>
<body>
<ul class="avatar-overlap" tabindex="0">
<li data-alph="g">G</li>
<li data-alph="h">H</li>
<li data-alph="i">I</li>
<li data-alph="j">J</li>
</ul>
<div class="card-overlap" tabindex="0">
<div>
<img src="https://picsum.photos/96?random=1" alt="">
<h3>Title</h3>
subtitle
</div>
<div>
<img src="https://picsum.photos/96?random=2" alt="">
<h3>Title</h3>
subtitle
</div>
</div>
<div class="img-overlap" tabindex="0">
<img src="https://picsum.photos/96?random=4" alt="">
<img src="https://picsum.photos/96?random=5" alt="">
<img src="https://picsum.photos/96?random=6" alt="">
</div>
<div class="circle-overlap" style="--n:4" tabindex="0">
<button type="button" aria-label="Menu">+</button>
<a href="#a" aria-label="A">A</a>
<a href="#b" aria-label="B">B</a>
<a href="#c" aria-label="C">C</a>
<a href="#d" aria-label="D">D</a>
</div>
</body>
</html>
Responsive behavior (tokens, not new markup)
Scale density at breakpoints:
@media (max-width: 520px) {
:root {
@ou-avatar-size(36px, -11px)
@ou-img-size(72px, -36px)
@ou-circle-size(40px, 64px)
}
}
Keep horizontal stacks from breaking the page:
body { overflow-x: hidden; }
.center-block {
display: flex;
flex-wrap: wrap;
justify-content: center;
max-width: 100%;
}
Accessibility (part of the contract)
Keep real labels or names in the DOM for avatars; faces may hide the decorative letter with font-size: 0.
Put aria-label on the circle center and on each satellite control.
Add tabindex="0" on stack roots so :focus-within matches hover.
Call @ou-reduced-motion() so transitions drop out when the user prefers reduced motion.
How the pieces fit (checklist)
Choose a cascade: avatar, card, image, or circle.
Set density with --ou-* / size helpers.
Mark the stack root for keyboard open when needed.
Theme only through tokens.
Compile with FSCSS or use the runtime; the browser runs CSS.
Overlapped UI is a rest/open contract plus a token layer. overlapui is that contract as an FSCSS module.
Reference