---
name: center-outward-expansion
description: Elements start clustered at screen center and expand outward to their final positions, driven by a shared progress value.
metadata:
tags: expansion, scatter, center, reveal, layout, sync, burst
---
# Center-Outward Expansion
Elements begin at a shared center point and radiate outward to their final positions. The expansion can be the entry beat itself, or **driven by another animation's progress** (e.g. a counting number growing) for coordinated motion.
## How It Works
Each element has a `targetX/Y` (its final layout position) and a shared `centerX/Y`. A `progress` value (0→1) interpolates each element between center and target:
```js
const x = centerX + (targetX - centerX) * progress;
const y = centerY + (targetY - centerY) * progress;
```
When `progress = 0` all elements overlap at the center; when `progress = 1` they're at their final spots.
## HTML
```html
{itemA}
{itemB}
{itemC}
{itemD}
{itemE}
{itemF}
```
## CSS
```css
.scene {
position: relative;
width: 100%;
height: 100%;
display: grid;
place-items: center;
background: {bgColor};
}
.burst-wrap {
position: relative;
width: 100%;
height: 100%;
display: grid;
place-items: center;
}
.burst-item {
position: absolute;
/* Items start at the wrap center via the absolute + 50% trick.
We tween translate offsets via GSAP, not left/top. */
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: {itemSize};
height: {itemSize};
display: grid;
place-items: center;
background: {itemBgColor};
border-radius: 28px;
font-family: {font};
font-weight: 900;
font-size: 96px;
color: {textColor};
will-change: transform;
}
```
## GSAP Timeline
```html
```
## How to Choose Values
- **ITEM_COUNT** — number of elements in the burst
- Range: 3–8
- Effects: 3 = sparse; 8 = busy. > 8 causes visual chaos where cards overlap mid-expansion
- Constraints: at low counts, prefer wider angular spread (target positions further apart)
- **EXPAND_DUR** — duration of each item's center → target tween
- Range: 1.0–1.8 s
- Effects: shorter = snappy burst; longer = floats outward
- Constraints: if driven by a counter, must equal the counter's duration (chord)
- **EXPAND_EASE** — shared ease across all items
- Discrete choice: `power2.out`, `power3.out`, `expo.out`
- Selection: `power3.out` is the default — fling out then settle. `power2.out` is gentler. `expo.out` makes them stop dramatically. Avoid `in` easings (they read as items being sucked back in mid-air).
- Constraint: if driven by another animation, must be identical to the driver's ease
- **STAGGER** — gap between successive items' start times
- Range: 0.04–0.08 s
- Effects: < 0.04 = simultaneous chord; > 0.08 feels lazy / arpeggiated
- Constraints: ITEM_COUNT × STAGGER must be < EXPAND_DUR or the last items still moving when others have landed reads as ragged
- **ENTRY_AT** — offset applied to the whole burst start
- Range: 0 – 0.5 s
- Effects: > 0 gives a beat of compositional quiet before the burst
- **START_PROGRESS** — fraction of the center→target path where items begin (for partially-spread variant)
- Range: 0 (exact center) – 0.5
- Effects: 0 = full cluster, dramatic spread; 0.3 = avoids initial pile-up at center
## Variations
### Synced expansion (driven by a counter)
If the burst should mirror a counting animation's progress:
```js
// Counter tween defines a state.value 0 → TARGET over COUNT_DUR
const counterState = { value: 0 };
const burstState = { p: 0 };
// Shared tween — same duration, same ease — visually a "chord"
tl.to(
counterState,
{
value: COUNT_TARGET,
duration: COUNT_DUR,
ease: COUNT_EASE,
onUpdate: () => (counterEl.textContent = Math.round(counterState.value).toLocaleString()),
},
0,
);
tl.to(
burstState,
{
p: 1,
duration: COUNT_DUR,
ease: COUNT_EASE,
onUpdate: () =>
items.forEach((el) => {
const tx = Number(el.dataset.targetX) * burstState.p;
const ty = Number(el.dataset.targetY) * burstState.p;
el.style.transform = `translate(-50%, -50%) translate(${tx}px, ${ty}px)`;
}),
},
0,
);
```
### Starting partially-spread
To avoid the initial clustered mess (6+ elements stacked at center), start at `START_PROGRESS`:
```js
{ x: targetX * START_PROGRESS, y: targetY * START_PROGRESS, scale: 0.4, opacity: 0 }
```
### Idle micro-float at final position
Pair with `sine-wave-loop` after expansion lands — keeps elements alive instead of frozen.
## Key Principles
- **Driver vs driven** — if the burst stands on its own, use a per-item stagger; if it shadows another animation (counter, audio beat), share the same eased progress so they read as one beat
- **Stagger inside the 0.04-0.08 s band** — too tight and the cluster never separates visually, too loose and the burst feels lazy
- **Out-easing for the expansion** — out-easing makes items "fling" out then settle. In-easing looks like they're sucked back in mid-air
- **Element count: 3-8** — fewer feels empty, more causes visual chaos at the center where cards overlap mid-expansion
- **❗ Don't put a label below the burst as the "real headline"** — if you do, the eye snaps to the label and ignores the burst. The burst IS the beat. If a label is needed, use big block-caps and reveal it post-burst, in the same stacked layout.
## Critical Constraints
- **Timeline must be paused**: `gsap.timeline({ paused: true })`
- **Registry key = `data-composition-id`**
- **Use translate, not left/top** — translating composes cleanly with the centering `translate(-50%, -50%)` trick; mutating `left`/`top` fights the centering and causes pixel jitter
- **`will-change: transform`** on burst items — many simultaneous transforms benefit from compositor hints
- **No `position: absolute` parents inside `burst-wrap` other than items themselves** — sibling absolute elements would steal the centered baseline
## Combinations
- [counting-dynamic-scale.md](counting-dynamic-scale.md) — counter peak drives the burst peak (chord)
- [sine-wave-loop.md](sine-wave-loop.md) — idle motion after the burst lands
- [card-morph-anchor.md](card-morph-anchor.md) — burst out of a morphed card
## Pairs with HF skills
- `/hyperframes-animation` — timeline + stagger
- `/hyperframes-core` — composition wiring
- `/hyperframes-cli` — `hyperframes lint`