Layout

Rows put in, ticked off and reordered, a grid filtered, a section opened, a label that grew. Change the page as you always do, and its elements move to where they now are on springs that keep their speed. Works with any element, and with Liquid Blur glass.

Today
  • Book the train to Lyon
  • Call the dentist
  • Water the plants
  • Buy oat milk
On this page

Get started

Layout watches an element and its items, its children unless you say otherwise. When you put an item in, take one out, reorder them, change a class or let a size change, every item that moved is shown where it was and goes to where it is now on a spring. A size that changed changes for real, so text is never squeezed, and what comes after it in the page moves along.

  1. Install.

    pnpm add @fluidkit/layout
  2. Call createLayout on the element whose items move.

    import { createLayout } from "@fluidkit/layout";
    
    createLayout({ root: document.querySelector<HTMLElement>(".list")! });
  3. Change the page as you always would. Layout sees it before it’s painted and moves everything from where it was.

    list.prepend(row); // put in: the rows after it slide down, it fades in
    row.remove(); // taken out: it fades where it was, the rest close up
    list.append(...[...list.children].reverse()); // reordered: each slides to its place
    section.classList.add("open"); // a size that changed: it grows, what's under it moves down
  1. Install. The React components are included in the package (React 18+).

    pnpm add @fluidkit/layout
  2. Wrap the list in Layout.Root. Its children are the items. Render it as your own element with render.

    import { Layout } from "@fluidkit/layout/react";
    
    export function Reminders({ todos }: { todos: Todo[] }) {
      return (
        <Layout.Root render={<ul className="list" />}>
          {todos.map((todo) => (
            <li key={todo.id}>{todo.text}</li>
          ))}
        </Layout.Root>
      );
    }
  3. Change your state as you always would. Rows React puts in, takes out or moves by their keys, classes it changes and sizes that change are followed.

Components are marked "use client". On the server they render plain markup.

  1. Install. The Vue components are included in the package (Vue 3.5+).

    pnpm add @fluidkit/layout
  2. Wrap the list in Layout.Root. Its children are the items. With as-child it renders your element instead of a div.

    <script setup lang="ts">
    import { Layout } from "@fluidkit/layout/vue";
    
    defineProps<{ todos: { id: string; text: string }[] }>();
    </script>
    
    <template>
      <Layout.Root as-child>
        <ul class="list">
          <li v-for="todo in todos" :key="todo.id">{{ todo.text }}</li>
        </ul>
      </Layout.Root>
    </template>
  3. Change your state as you always would. Rows Vue puts in, takes out or moves by their keys, classes it changes and sizes that change are followed.

On the server it renders plain markup.

  1. Install. The Svelte components are included in the package (Svelte 5.29+).

    pnpm add @fluidkit/layout
  2. Wrap the list in Layout.Root. Its children are the items. A child snippet renders your element instead of a div: spread the props it gives onto it.

    <script lang="ts">
      import { Layout } from "@fluidkit/layout/svelte";
    
      let { todos }: { todos: { id: string; text: string }[] } = $props();
    </script>
    
    <Layout.Root>
      {#snippet child({ props })}
        <ul {...props} class="list">
          {#each todos as todo (todo.id)}<li>{todo.text}</li>{/each}
        </ul>
      {/snippet}
    </Layout.Root>
  3. Change your state as you always would. Rows Svelte puts in, takes out or moves by their keys, classes it changes and sizes that change are followed.

On the server it renders plain markup.

Examples

Reminders

Put one in at the top and the rest slide down while it fades in. Tick one off and it fades where it was while the rows under it close up. Shuffle, and each row slides to its new place. Do it again before they’ve stopped: they turn around from where they are, keeping their speed.

Today
  • Book the train to Lyon
  • Call the dentist
  • Water the plants
  • Buy oat milk

Photos

Filter the grid and the photos that don’t match fade where they were, while the rest slide to their new cells. Back to All, the others fade in where they belong. Nothing here knows it’s a grid: Layout follows whatever the browser lays out.

Settings

A section opens by its real height on a spring: its text is revealed, never squeezed, and the sections under it and the card after them move down with it. Open another while the first is still opening and both carry on together.

Connected to Home. Networks you've joined before connect on their own when they're near.

AirPods Pro and Magic Keyboard are connected. Two more devices are remembered.

Do Not Disturb until 7:00. Calls from your favorites come through, and repeated calls too.

Battery 82%

A label that grows

Follow becomes Following: the glass button widens on the spring, its label shown whole as it does, and the name beside it gives way. Any item whose size changes does the same, whatever changed it: a label, an image loading, a font.

How it moves

From where it was drawn. A change is seen before the browser paints it, and each item that moved is shown exactly where it was, then moves to where it is now. Another change on the way sets off from where everything is drawn at that moment, keeping its speed: nothing jumps, however fast the changes come.

Positions, then sizes. A moved item goes on translate. An item whose size changed, and the root itself, change size for real: width and height on the spring, what’s past the size clipped along that axis while it grows. Text isn’t squeezed, corners and shadows aren’t stretched, and what comes after them in the page moves with them instead of jumping. With size: false, sizes change at once and only positions move.

In and out. A new item comes in where it is, faded and a little smaller unless told (enter, any of Motion’s transitions). One taken out of the page goes out where it was (exit): the element itself, put back in its place for the while, out of the flow, inert and hidden from assistive technology, then removed. Put back in the page meanwhile, it stays and moves from where it was. An item hidden with display: none just goes.

What it touches. While an item moves, its inline translate is the layout’s, added to its own: a drag’s offset, or its stylesheet’s. transform is never touched, so a press stretching glass or a morph flying it goes on as it does, and an item being dragged (data-dragging) isn’t moved. While it comes in or goes out, its scale (times its own) and opacity are the layout’s; while its size changes, its width, height and what would hold them back, including its flex along the container’s main axis. Everything is put back as it was once it’s at rest.

What it sees. Items put in, taken out or moved, the class and hidden of the root and its items, and their sizes. A change none of these show, say an item’s inline style or a class on an ancestor that moves items without resizing them, is followed on update(). Scrolling isn’t a change: positions are read inside the root, its scroll included. With reduced motion everything changes at once, and so it does while the root is hidden (in a tab not shown, say) and as it’s shown again.

Layout or Morph?

Both move things on springs, for different changes. Morph turns one element into another: a button into a panel, a row into a page. Layout moves an element to where the page has put it now: the same row, further down; the same section, taller. A list with rows that open into panels can use both, Layout on the list and Morph on each row.

Performance

Moves are translations and cost almost nothing. Sizes change for real, so the browser lays out what depends on them each frame, only while they move: in a component, a list or a card, under a couple of milliseconds a frame on a phone. A section high up in a long page lays out everything under it each frame; measured over five hundred rows on a mid-range phone’s CPU, under 8ms a frame. If a page is much heavier, use size: false there, or give what’s under it less to lay out.

OptionsTypeScript

createLayout(options: LayoutOptions): Layout

Option Description
root: HTMLElement The element whose items move
items?: string Its items, as a selector inside it. Default: its children
spring?: SpringParams The spring everything moves on. Default: springs.snappy(), { duration: 0.26, bounce: 0.15 }. See Motion
size?: boolean Sizes change on the spring too. Default: true. false changes them at once and moves only positions
enter?: TransitionParams | false How a new item comes in: a transition. Default: transitions.scale(). false: it’s there at once
exit?: TransitionParams | false How an item taken out goes. Default: the same. false: it’s gone at once
onStart?: () => void Called when something sets off moving, from rest
onRest?: () => void Called when everything has come to rest

APITypeScript

Member Description
moving true while something is on its way
update() Reads the layout again and follows it, for a change no watcher sees
set(settings) Updates any option except root and items, without recreating it
destroy() Stops watching. Everything still moving is put where it is at once, and every style it wrote put back

PartsReactVueSvelte

Part Renders Description
Layout.Root div The element whose items move: its children. Has data-moving while something is on its way

Layout.Root props:

Prop Description
items?: string Its items, as a selector inside it. Default: its children
spring?: SpringParams The spring everything moves on. Default: springs.snappy()
size?: boolean Sizes change on the spring too. Default: true
enter?: TransitionParams | false How a new item comes in: a transition. Default: transitions.scale()
exit?: TransitionParams | false How an item taken out goes. Default: the same
onStart?: () => void Called when something sets off moving, from rest
onRest?: () => void Called when everything has come to rest

Changing props never recreates it. Changing items does.

Part Renders Description
Layout.Root div The element whose items move: its children. Has data-moving while something is on its way. Takes as-child, see Composition

Layout.Root props and events:

Prop or event Description
items?: string Its items, as a selector inside it. Default: its children
spring?: SpringParams The spring everything moves on. Default: springs.snappy()
size?: boolean Sizes change on the spring too. Default: true
enter?: TransitionParams | false How a new item comes in: a transition. Default: transitions.scale()
exit?: TransitionParams | false How an item taken out goes. Default: the same
@start Something set off moving, from rest
@rest Everything has come to rest

Changing props never recreates it. Changing items does.

Part Renders Description
Layout.Root div The element whose items move: its children. Has data-moving while something is on its way. Takes a child snippet, see Composition

Layout.Root props:

Prop Description
items?: string Its items, as a selector inside it. Default: its children
spring?: SpringParams The spring everything moves on. Default: springs.snappy()
size?: boolean Sizes change on the spring too. Default: true
enter?: TransitionParams | false How a new item comes in: a transition. Default: transitions.scale()
exit?: TransitionParams | false How an item taken out goes. Default: the same
onStart={() => …} Something set off moving, from rest
onRest={() => …} Everything has come to rest

Changing props never recreates it. Changing items does.

CompositionReactVueSvelte

Render as your own element. Layout.Root accepts render, as in Base UI: pass an element and our props are merged into it, or a function that receives the props and state ({ moving }).

<Layout.Root render={<ul className="list" />}>{rows}</Layout.Root>

Glass. Render it as Glass, and its items as glass too: a glass group melts with them as they move.

<Layout.Root render={<Glass.Group className="toolbar" />}>{tools}</Layout.Root>

Render as your own element. Layout.Root accepts as-child, as in Reka UI: it renders the first element in its slot, with our attributes and ref merged in. The slot also receives its state ({ moving }).

<Layout.Root as-child>
  <ul class="list">…</ul>
</Layout.Root>

Glass. Render it as Glass, and its items as glass too: a glass group melts with them as they move.

<Layout.Root as-child>
  <Glass.Group class="toolbar">…</Glass.Group>
</Layout.Root>

Render as your own element. Layout.Root accepts a child snippet, as in bits-ui: spread the props it gives onto your element. They carry the attachment that connects it. The snippet also receives its state ({ moving }).

<Layout.Root>
  {#snippet child({ props })}<ul {...props} class="list">…</ul>{/snippet}
</Layout.Root>

Glass. Put the props on a glass group, and its items glass too: the group melts with them as they move.

<Layout.Root>
  {#snippet child({ props })}
    <Glass.Group {...props} class="toolbar">…</Glass.Group>
  {/snippet}
</Layout.Root>

The hookReactVueSvelte

Use useLayout(options) when the component doesn’t fit your markup. It takes the same options as Layout.Root and returns a ref.

import { useLayout } from "@fluidkit/layout/react";

const layout = useLayout({ size: false });

<ul ref={layout.root}>{rows}</ul>
Member Description
root The ref for the element whose items move
moving true while something is on its way
update() Reads the layout again, for a change no watcher sees
layout The underlying engine, once mounted

Use useLayout(options) when the component doesn’t fit your markup. Options can be an object, a ref or a getter; a getter is read again when what it uses changes. It returns a function ref for :ref.

<script setup lang="ts">
import { useLayout } from "@fluidkit/layout/vue";

const layout = useLayout({ size: false });
</script>

<template>
  <ul :ref="layout.root">…</ul>
</template>
Member Description
root Function ref for the element whose items move
moving Ref: true while something is on its way
update() Reads the layout again, for a change no watcher sees
layout Shallow ref to the underlying engine, once mounted

Use useLayout(() => options) when the component doesn’t fit your markup. Call it in a component’s script; the options getter is read again when what it uses changes. It returns an attachment for {@attach}, and state you read like any other.

<script lang="ts">
  import { useLayout } from "@fluidkit/layout/svelte";

  const layout = useLayout(() => ({ size: false }));
</script>

<ul {@attach layout.root}>…</ul>
Member Description
root Attachment for the element whose items move
moving true while something is on its way
update() Reads the layout again, for a change no watcher sees
layout The underlying engine, once mounted
PlaygroundIts hard cases, live: slowed down, on other backdrops, with their boxes drawn.