Layout Showcase
Fifteen live layout examples built on @freestylejs/ani-react/layout - switches, tabs, lists, drag to reorder, shared elements, presence and anchors.
Each example below is plain React with the public @freestylejs/ani-react/layout API:
elements marked with useLayout, LayoutGroup, and AnimatePresence with usePresence.
There are no capture or play calls. Interrupt any of them mid-flight: every element continues
from where it is drawn, with the speed it had. Open Code under an example for the pattern
it uses. The Layout guide explains the API.
The browser tests drive these same components on a manual clock in Chromium, Firefox and WebKit and check every frame of every marked element against an independent model of the motion, including changes made mid-flight.
Essentials
- Switch. Flexbox moves the knob;
useLayout()on the knob draws the move. - Tabs. One underline with
layoutId, rendered under the selected tab. - Smooth tabs. A highlight pill with
layoutIdandradius: truefollows the pointer. - Accordion. The card resizes by layout and reveals its body; header and body use
layout: 'position'so their text never stretches.
Lists
- Shuffle. Reorder the array; every tile moves from where it is drawn.
- Drag to reorder. Built on the public API: the row follows the pointer with its own
transform, and
useLayoutGroup().update()draws it into its slot on release. - To-do list. Add, tick and remove under
AnimatePresence mode="popLayout". - Stacked notifications. A collapsed stack that fans out into a list.
- E5 Notifications.
useLayoutandusePresenceshare each toast; a removed toast exits in place while its siblings close the gap from the first frame.
Shared elements
- E2 Create Button. A button opens into a menu through shared
layoutIds on the surface, label and icon; the label keeps its glyph size withlayout: 'position'. - Family-style dialog. A bottom sheet whose height follows its step, with steps swapped
under
popLayout. - E3 iOS Folder. The closed icon and open panel are one shared, radius-aware surface; four nested shared tiles morph inside it, while the five staggered items are position-only layout participants so the surface can scale without stretching them.
- E4 App Store. A card opens into a detail view through three nested ids: surface, image and title.
- Gallery. Sixteen radius-aware tiles switch between a grid and a list; any tile opens a
detail view that shares its
layoutIdand keeps the handoff's corners stable.
Precision
- E1 Layout Anchor. A child anchored with
layoutAnchor: { x: 0.5, y: 0.5 }stays centred on its growing parent at every frame, and keeps its size until its own delay has passed.
Layout changes that move
from what you saw to where it lands.
Change the DOM the way you always do. Ani measures every participant before and after the commit and draws each one from its old box to its new one with translate and scale, on the same clock as every other Ani animation.
Essentials
One hook per moving element. Change state; layout draws the difference.
Switch
Flexbox moves the knob from start to end; layout draws the move with a bouncy spring. Flick it back mid-flight and it turns around with its speed.
Code
function Switch({ on }) {
const knob = useLayout()
return (
<button style={{ justifyContent: on ? 'flex-end' : 'flex-start' }}>
<span ref={knob} />
</button>
)
}Tabs
One underline id, rendered under whichever tab is selected: it slides and resizes from the old tab to the new one while the panel swaps.
Code
{tabs.map((tab, i) => (
<button key={tab} onClick={() => setSelected(i)}>
{tab}
{selected === i ? <Underline /> : null}
</button>
))}
function Underline() {
const ref = useLayout({ layoutId: 'tab-underline' })
return <span ref={ref} className="underline" />
}Smooth tabs
A pill follows the pointer across items of different widths. Sweep across quickly: every retarget keeps its velocity, and radius keeps the corners round.
Code
<nav onPointerLeave={() => setHovered(null)}>
{items.map((item, i) => (
<button key={item} onPointerEnter={() => setHovered(i)}>
{(hovered ?? selected) === i ? <Pill /> : null}
{item}
</button>
))}
</nav>
const ref = useLayout({ layoutId: 'nav-highlight', radius: true })Accordion
The card grows by layout and reveals its body; the items below slide. Header and body are position-only participants, so the text never stretches.
Code
function Item({ open }) {
const card = useLayout({ radius: true })
const header = useLayout({ layout: 'position' })
const body = useLayout({ layout: 'position' })
return (
<div ref={card} style={{ height: open ? 'auto' : 48, overflow: 'hidden' }}>
<button ref={header}>…</button>
<div ref={body}>…</div>
</div>
)
}Lists
Reorder, add and remove: siblings make room from the first frame.
Shuffle
Reorder the array, nothing else. Every tile moves from where it is drawn, so a shuffle during a shuffle is continuous.
Code
{order.map((id) => <Tile key={id} id={id} />)}
function Tile({ id }) {
const ref = useLayout()
return <div ref={ref}>{id}</div>
}Drag to reorder
Built in user land. The row follows the pointer with its own transform; crossing a slot reorders the state and the others make room; the release draws it into place.
Code
// move: outside React, so nothing re-renders
row.style.transform = `translateY(${offset}px)`
// crossing a slot: reorder, then re-base the offset before the flush
setOrder(moved(order, from, to))
// release: layout draws the row into its slot
group.update(() => { row.style.transform = '' })To-do list
Add, tick and remove. A ticked item sorts to the bottom; a removed one exits in place under popLayout while the rest close the gap.
- Buy oat milk
- Book the dentist
- Water the plants
Code
<AnimatePresence mode="popLayout">
{sorted.map((todo) => <Row key={todo.id} todo={todo} />)}
</AnimatePresence>
function Row({ todo }) {
const presence = usePresence(rowMotion)
const ref = useLayout({ layout: 'position', ref: presence.ref })
return <li ref={ref} style={presence.style}>…</li>
}Stacked notifications
Collapsed, older toasts peek out behind the newest one; hover to fan them out. Every toast moves and resizes by layout, and its text never scales.
Code
function Toast({ index, expanded }) {
const presence = usePresence(toastMotion)
const ref = useLayout({ radius: true, ref: presence.ref })
const text = useLayout({ layout: 'position' })
return (
<div ref={ref} style={{ ...presence.style, ...stackBox(index, expanded) }}>
<div ref={text}>…</div>
</div>
)
}Notifications
CSS lays the stack out. A new toast enters from below at scaleX 0.8 while the others move by layout; an exit pops out of the flow and its siblings close the gap.
Shared elements
One layoutId on two elements: the new one starts where the old one was drawn.
Create button
The button and the menu share their wrapper, icon and label ids: one surface morphs, the label keeps its glyph size, and a quick close and reopen continues from where it is.
Family-style dialog
A bottom sheet whose height follows its step. The sheet resizes by layout, the steps swap under popLayout, and the button morphs with a sharp label.
Code
const sheet = useLayout({ radius: true })
<div ref={sheet} style={{ overflow: 'hidden' }}>
<AnimatePresence mode="popLayout">
<Step key={step} /> {/* usePresence + layout: 'position' */}
</AnimatePresence>
<Action /> {/* radius button, position-only label */}
</div>Folder
The closed folder and open panel share one radius-aware surface; four nested shared tiles inherit the group's single spring, while the five staggered apps are position-only layout participants so the surface never stretches them.
Code
const surface = useLayout({
layoutId: 'folder',
radius: true,
ref: presence.ref,
})
const tile = useLayout({
layoutId: 'folder-tile-' + i,
radius: true,
})
const item = useLayout({ layout: 'position', ref: itemPresence.ref })
<LayoutGroup motion={{ duration: DURATION, timing: SPRING }}>
<AnimatePresence mode="popLayout">
{open ? <Panel ref={surface} /> : <Folder ref={surface} />}
</AnimatePresence>
</LayoutGroup>Card to detail
Surface, image and title are nested shared ids, one track each: the image covers the card at every frame and the title keeps its size.
Gallery
16 radius-aware tiles move between a grid and a list; a tile and its detail view share the same id so the handoff keeps its corners stable.
Code
const tile = useLayout({
layoutId: 'gallery-' + i,
radius: true,
})
const detail = useLayout({
layoutId: 'gallery-' + i,
radius: true,
ref: presence.ref,
})Precision
Anchored children stay on their parent's point at every frame, at any depth.
Layout anchor
The parent grows; the anchored child keeps its point at the parent's centre (the crosshair) and changes its own size after a delay.