Ani
Showcase

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 layoutId and radius: true follows 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. useLayout and usePresence share 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 with layout: '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 layoutId and 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@freestylejs/ani-layout

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.

15 examplesevery frame checked in Chromium, Firefox and WebKitinterrupt anything mid-flight

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.

layout
Or click a switch.
Wi-Fi
Bluetooth
AirDrop
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.

layoutId

Three projects shipped this week, two more are in review.

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.

layoutId + radius
Hover the bar, click to select.
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.

size + position
Any change of an element’s box across a commit: flex, grid, size, order, or a move to another parent.
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.

reorder
1
2
3
4
5
6
7
8
9
10
11
12
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.

useLayoutGroup
Drag a row.
Design review
Write the changelog
Ship 2.0
Celebrate
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.

presence + layout
  • 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.

size + presence
Notification 3
Arrived just now
Notification 2
Arrived just now
Notification 1
Arrived just now
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>
    )
}
E5

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.

presence + layout

Shared elements

One layoutId on two elements: the new one starts where the old one was drawn.

E2

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.

shared ids
+Create

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.

size + popLayout

Send money

From: Everyday wallet
To: Robin
Network fee: 0.12
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>
E3

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.

nested ids + radius
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>
E4

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.

nested ids

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.

grid / list + radius
Select a tile to open it.
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.

E1

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.

layoutAnchor