Utility Components

All components and factory functions listed on this page are imported from retend-utils/components.

Input

A reactive input component with two-way data binding. The model prop accepts a Cell whose value stays synchronized with the input element. The cell type should correspond to the input type.

Props:

  • type (string) — The HTML input type (e.g. "text", "number", "checkbox", "date", "file").
  • model (Cell) — A reactive cell for two-way binding.
  • ref (Cell<HTMLInputElement | null>, optional) — Reference to the underlying input element.
  • All other standard HTML input attributes are forwarded.
import { Cell } from 'retend';
import { Input } from 'retend-utils/components';

function LoginForm() {
  const username = Cell.source('');
  const password = Cell.source('');

  return (
    <form>
      <Input type="text" model={username} placeholder="Username" />
      <Input type="password" model={password} placeholder="Password" />
      <p>Hello, {username}</p>
    </form>
  );
}

FluidList

Renders a list with dynamic sizing, staggered animations, and flexible layouts. Transitions between list states are handled automatically using FLIP-based animation.

Props:

  • items (required) — A reactive cell containing the array of items.
  • Template (required) — A function returning JSX for each item. Receives { item, index, previousIndex, list }.
  • itemKey (required for object items) — A unique key property name for each item.
  • direction ('block' | 'inline', default 'block') — Flow direction. 'block' flows vertically; 'inline' flows horizontally.
  • gap (string, default '0px') — Space between items.
  • speed (string, default '0.2s') — Transition duration.
  • easing (string, default 'ease') — Transition easing function.
  • staggeredDelay (string, default '0ms') — Per-item stagger delay for animations.
  • itemHeight (string, optional) — Fixed height for each item.
  • itemWidth (string, optional) — Fixed width for each item.
  • animateSizing (boolean, default false) — Animate item size changes.
  • maxColumns (number, optional) — Maximum columns before wrapping (for direction: 'inline').
  • maxRows (number, optional) — Maximum rows before wrapping (for direction: 'block').
  • ref (Cell, optional) — Reference to the <ul> container element.
  • style (optional) — Custom styles for the container.
  • All other standard <ul> attributes are forwarded.
import { Cell, For } from 'retend';
import { FluidList, type ListTemplateProps } from 'retend-utils/components';

interface Task {
  id: number;
  title: string;
}

const tasks = Cell.source<Task[]>([
  { id: 1, title: 'Design' },
  { id: 2, title: 'Develop' },
  { id: 3, title: 'Deploy' },
]);

function TaskItem({ item, index }: ListTemplateProps<Task>) {
  return (
    <div style="padding: 12px; border: 1px solid #ccc;">
      <strong>{item.title}</strong>
      <span> (#{index})</span>
    </div>
  );
}

function TaskBoard() {
  return (
    <FluidList
      items={tasks}
      itemKey="id"
      itemHeight="60px"
      gap="8px"
      speed="0.3s"
      easing="ease-in-out"
      staggeredDelay="40ms"
      Template={TaskItem}
    />
  );
}

createUniqueTransition

A factory function that creates unique components with smooth FLIP animations. When an element produced by createUniqueTransition moves between positions in the tree, it animates from its previous position and size to its new one using CSS transforms.

This is the animated counterpart to the core createUnique function described in the Unique Instances page.

Parameters:

  • renderFn (required) — A function returning the JSX to render. Receives props as a reactive Cell.
  • options (required):
    • transitionDuration (string) — Duration of the animation (e.g. '300ms').
    • transitionTimingFunction (string, default 'ease') — Easing function.
    • maintainWidthDuringTransition (boolean, optional) — Disable horizontal scaling during transitions.
    • maintainHeightDuringTransition (boolean, optional) — Disable vertical scaling during transitions.
    • onSave ((element) => data, optional) — Called before the element moves. Return data to preserve (e.g. scroll position).
    • onRestore ((element, data) => void, optional) — Called after the element arrives. Receives the data returned from onSave.
    • container (optional) — Attributes applied to the wrapper element.

Returns: A unique component. Pass an id prop to distinguish multiple instances.

Basic Usage

import { createUniqueTransition } from 'retend-utils/components';
import { Cell } from 'retend';

const PersistentVideo = createUniqueTransition(
  (props) => {
    const src = Cell.derived(() => props.get().src);
    return <video src={src} controls />;
  },
  { transitionDuration: '300ms' }
);

function App() {
  return (
    <div>
      <PersistentVideo id="main-video" src="/video.mp4" />
    </div>
  );
}

Picture-in-Picture Transition

import { Cell, If } from 'retend';
import { createUniqueTransition } from 'retend-utils/components';

const styles = {
  main: { width: '640px', height: '360px' },
  pip: {
    position: 'fixed',
    bottom: '20px',
    right: '20px',
    width: '200px',
    height: '112px',
  },
};

const VideoPlayer = createUniqueTransition(
  () => <video src="video.mp4" controls />,
  {
    transitionDuration: '300ms',
    transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
  }
);

function App() {
  const isPip = Cell.source(false);
  const isMain = Cell.derived(() => !isPip.get());
  const toggle = () => isPip.set(!isPip.get());

  return (
    <div>
      {If(isMain, () => (
        <div style={styles.main}>
          <VideoPlayer />
        </div>
      ))}
      {If(isPip, () => (
        <div style={styles.pip}>
          <VideoPlayer />
        </div>
      ))}
      <button type="button" onClick={toggle}>
        Toggle PiP
      </button>
    </div>
  );
}

Preserving State During Transitions

Use onSave and onRestore to capture and restore element state (such as scroll position) across transitions.

import { Cell } from 'retend';
import { createUniqueTransition } from 'retend-utils/components';

const AnimatedCard = createUniqueTransition(
  (props) => {
    const title = Cell.derived(() => props.get().title);
    return (
      <div class="card">
        <h3>{title}</h3>
        <div class="content" style="overflow-y: auto; height: 100px;">
          {/* Long scrollable content */}
        </div>
      </div>
    );
  },
  {
    transitionDuration: '300ms',
    onSave: (el) => ({
      scrollTop: el.querySelector('.content')?.scrollTop,
    }),
    onRestore: (el, data) => {
      if (data?.scrollTop) {
        const contentEl = el.querySelector('.content');
        if (contentEl) contentEl.scrollTop = data.scrollTop;
      }
    },
  }
);