# painter() (/docs/canvas/painter) ## Usage [#usage] ```ts import { painter } from "@remold-ui/canvas"; const orb = painter(canvas, { values: { radius: 40, glow: 0 }, spring: { duration: 0.6, bounce: 0 }, draw(context, { width, height, values }) { context.beginPath(); context.arc(width / 2, height / 2, values.radius, 0, Math.PI * 2); context.fill(); }, }); orb.to({ radius: 80, glow: 1 }); // springs, drawing every frame until it settles orb.destroy(); ``` ## Options [#options] | Option | Default | Description | | --------------- | -------- | -------------------------------------------------------------------------------------------- | | `draw` | | `(context, frame) => void`. The canvas is cleared and scaled; draw in CSS pixels. | | `values` | `{}` | Starting numbers. `frame.values` has each one where it is now. | | `spring` | smooth | One `SpringConfig`, or one per value name. | | `precision` | `0.001` | How close counts as arrived. | | `continuous` | `false` | Draw every frame (drift, breathing, live audio), not only while springing. | | `clear` | `true` | Clear before each frame. | | `reducedMotion` | `"user"` | `"always"` or `"never"` override the system setting. | | `maxPixelRatio` | `2` | Cap on stored pixels per CSS pixel. | | `label` | | Accessible name; sets `role="img"`. | | `loop` | browser | A frame loop to run on, for tests. | `frame` has `width`, `height`, `pixelRatio`, `values`, `time` (seconds of motion), `dt` (0 for a frame drawn outside the loop) and `still` (motion is reduced). ## Returns [#returns] | Member | Description | | -------------------------- | --------------------------------------------------------------------------- | | `to(targets, { spring, velocity })` | Spring to new targets. `velocity` hands over a flick, per name. | | `set(values)` | Jump and stop. Use it to follow a finger. | | `redraw()` | Draw again for a change `draw` reads that isn't a value. | | `label(text)` | Change the accessible name. | | `values`, `still` | Every value now; whether motion is reduced. | | `destroy()` | Stop and let go of the canvas. | It pauses while the canvas is off screen. `Remold.Canvas` is the React part. # particles() (/docs/canvas/particles) ## Usage [#usage] ```ts import { particles, sampleTextPoints } from "@remold-ui/canvas"; import { samplePath } from "@remold-ui/core"; const dots = particles(canvas, { points: sampleTextPoints("Hello", { count: 2000 }), label: "Hello" }); dots.to(samplePath(logoPath, { count: 2000 }), { label: "Our logo" }); ``` Each dot pairs with a nearby spot in the next shape and springs there; spare dots shrink away and new ones grow in. Dots near the pointer lean away. ## Options [#options] fill)", default: "CSS color", description: "One fill for every dot, such as a gradient." }, palette: { type: "string[] | Record", description: "Colours by tone: a list a numeric tone picks from, 0 (first) to 1 (last), or colours by name that points name in their tone. Any CSS colour, var(--name) included, which is read from the canvas and follows theme changes." }, camera: { type: "true | { spin, angle, tilt, distance }", description: "Turn the cloud in 3D and draw it with perspective: spin in turns a second (default 0.04), angle and tilt in degrees (default 0 and 20), distance in shape radii (default 4), and sway, degrees to swing either side instead of turning round. Not with a frame fit. Change it later with setCamera, and the palette with setPalette.", }, blend: { type: '"normal" | "add"', default: '"normal"', description: "Add brightens where dots overlap, for glowing dots on a dark ground." }, pointer: { type: "false | { radius, strength }", default: "{ radius: 90, strength: 22 }", description: "How dots lean away from the pointer.", }, label: { type: "string", description: "The canvas’s accessible name." }, spring: { type: "{ duration, bounce }", default: "{ duration: 0.9, bounce: 0.2 }", description: "How dots move." }, }} /> ## Changing shape [#changing-shape] `to(points, options)` flows the dots into the next shape. ### Scatter [#scatter] ```ts dots.to(nextShape, { transition: "scatter" }); dots.to(nextShape, { transition: { type: "scatter", spread: 1.6, hold: 0.5 } }); ``` With reduced motion the dots go straight to the new shape, scatter or not. In core, `pointCloud().to(points, { scatter })` takes the same settings. ## Sampling shapes [#sampling-shapes] | Function | Gives points for | | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `samplePath(d, options)` | An SVG path's inside, or its outline with `mode: "outline"`. | | `sampleTextPoints(text, options)` | Text in a CSS font. Load web fonts first. | | `loadPixels(source, { size })` | An image's RGBA pixels, from a URL, a picked file or an image on the page, for `shapes.fromPixels` in `@remold-ui/core`. | | `sampleImagePoints(image, options)` | An image's opaque, dark, light, or saturated pixels; pass the same `palette` (any CSS colours) to sample colour tones. Optional normalized `details` preserve tiny features outside the mask. | | `sampleElementPoints(element, options)` | A piece of the page as it looks: backgrounds, text and images, each dot toned from `palette`. Positions are in `relativeTo`'s CSS pixels, so they land on the element under an overlay canvas with `fit: "none"`. | | `sampleMask(mask, options)` | Any pixel data, such as `ImageData`. | All take `count` (exactly that many points) or `spacing`, plus `pattern` and `jitter`. `dots.settled()` resolves once the dots stop moving, so you can wait for a change to play out. To pour part of the page somewhere else (a row into a bin, a product into a cart), use `dust(container)`: `await trail.away(row, bin)` and `await trail.back(row, bin)`. It's built from `sampleElementPoints`, which turns an element into coloured dots, and `clusterPoints` from core, which gathers them into a heap. See [useDust](/docs/hooks/use-dust). # scene() (/docs/canvas/scene) ## Usage [#usage] ```ts import { scene } from "@remold-ui/canvas"; const diagram = scene(canvas, { label: "Our architecture", objects: { api: { look: "chip", label: "API" }, db: { look: "cylinder", label: "Database" } }, layouts: { simple: { objects: { api: { x: 0, y: 0, width: 80, depth: 80 }, db: { x: 160, y: 0, width: 80, depth: 80 } } }, scaled: { objects: { api: { x: 0, y: -60, width: 80, depth: 80 }, db: { x: 200, y: 40, width: 120, depth: 120 } } }, }, links: [{ from: "api", to: "db", flow: true }], }); diagram.to("scaled"); ``` Objects are drawn with a `look`: `plate`, `chip`, `cylinder`, `tower`, `glass`, `wire`, `portrait`, `atlas` or `grid`, or your own. Positions are floor units, so the same layout fits any canvas size. In React, use [Scene](/docs/primitives/scene). ## Placing objects [#placing-objects] `layout` does the arithmetic for common layouts. Each helper returns boxes keyed by id, ready to spread into a layout's `objects`: ```ts import { layout, scene } from '@remold-ui/canvas'; layouts: { hub: { objects: { lumen: layout.square(0, 0, 150), ...layout.ring(tiles, { radius: 280, size: 80 }) } }, stack: { objects: layout.stack(layers, { size: 300, gap: 22 }) }, steps: { objects: layout.row(layers, { gap: 200, size: 150 }) }, board: { objects: layout.grid(cards, { columns: 4, gap: 120, size: 90 }) }, } ``` | Helper | Puts | | ------------------------------------------- | -------------------------------------------------------------------------------- | | `layout.square(cx, cy, size)` | One square, centred. `layout.box(x, y, width, depth)` takes the top-left corner. | | `layout.ring(ids, { radius, size, start })` | Squares round a circle, the first at the top. | | `layout.row(ids, { gap, size, axis })` | Squares in a line, centred. | | `layout.stack(ids, { gap, size, z })` | Squares on top of each other, the first at the bottom. | | `layout.grid(ids, { columns, gap, size })` | Squares in rows, centred. | All take `at: [x, y]` to move the group and `extra` for anything else a box can carry, such as `height`, `opacity` or `label`. ## Methods [#methods] | Method | Returns | What it does | | -------------------------- | ------------------ | ------------------------------------------------------------- | | `to(layout)` | `void` | Spring to another layout. | | `focus(id \| null)` | `void` | Lift an object and dim the rest, or clear focus. | | `points(points, options?)` | `void` | Show a cloud of cubes. Calling again morphs to the new cloud. | | `anchor(id, port?)` | `{ x, y } \| null` | Where an object is on the canvas, for placing HTML over it. | | `pick(x, y)` | `string \| null` | The object at a canvas point. | | `destroy()` | `void` | Stop and remove listeners. | # Amount input (/docs/components/amount-input) } minHeight={360}> ## Things to try [#things-to-try] - **Type an amount.** Each digit rises into place, and the comma slides along as the number grows. - **Ask for too much precision.** A third decimal or an eighth digit is refused with a small shake rather than ignored. - **Go over your balance.** The field turns red once, and "Balance £2,410.50" morphs into how far over you are. - **Press Max.** It fills in your whole balance. ## Install [#install] ## Dependencies [#dependencies] The install command adds these for you. If you copy the code by hand, install them yourself. ## Usage [#usage] ```tsx const [amount, setAmount] = useState(""); ``` `value` is the amount as typed, digits and at most one point. Set `symbol`, `currency` (the code shown at the end of the field), `decimals` and `maxDigits` for other currencies; `decimals={0}` refuses the point altogether. ## How it works [#how-it-works] - **A real input underneath.** An invisible Base UI `Input` covers the number, so typing, pasting, the mobile number pad and screen readers behave as they do on a plain field. The digits you see are drawn from its value. - **Digits stay put.** Whole digits are keyed from the left and separators by their place from the right, so typing adds one digit at the end and only the comma moves. - **The helper morphs.** It's `Remold.Text`, so "Balance" becomes the shortfall by keeping the letters the two lines share. # API keys (/docs/components/api-keys) } minHeight={600}> ## Things to try [#things-to-try] - **Create a key.** The card springs into a name form, and "Create key" travels down to become its submit button. - **Reveal the secret.** Create turns the card into a dark panel where the secret decodes a character at a time. - **Copy it.** The copy icon turns into a check. - **Press Done.** The card springs back to the list, the secret slides into the newest row, and scrambles itself into its masked form. - **Revoke a key.** Hover a row (or tab to it) and press Revoke. It turns into a red Confirm; press it and the row folds out of the list while the header says it's gone. Escape or tabbing away takes it back. ## Install [#install] ## Dependencies [#dependencies] The install command adds these for you. If you copy the code by hand, install them yourself. ## How it works [#how-it-works] - **One card, three faces.** The list, the name form and the secret are faces of one card. It springs its real height between them and clips what's inside, so nothing is scaled; each face's parts blur and scale in from just below their size. - **Things that travel.** "Create key" and the secret move with Motion's `layoutId`: the button into the form, the secret from the panel into its row. - **Decode and mask.** Revealing and hiding the secret use the same scramble hook, run in opposite directions. The subtitle reshapes with `Remold.Text` to say what each step wants. - **Revoking asks in place.** The first press only arms it: Revoke turns into a red Confirm. Once it's gone the row folds away by height, and the card follows it exactly. - **Built on Base UI.** The Live and Test switch is Base UI's Toggle Group. The keys are made up. # Booking steps (/docs/components/booking-steps) } minHeight={620}> ## Things to try [#things-to-try] - **Pick the kind of visit.** A check-up, a follow-up or a consultation. The option fills in dark, then that same dark shape flies up into the first slot of the summary and becomes a chip, and the next step slides in. - **Pick a day, then a time.** Full days and taken times can't be chosen. Each choice follows the first into the summary. - **Tap a chip.** You're back on that step, and the chip flies back down onto the option it came from. - **Confirm the booking.** After a short spinner, the steps give way to the confirmation, with the end time worked out from the kind of visit and a reminder promised by text. ## Install [#install] ## Dependencies [#dependencies] ## How it works [#how-it-works] - **The choice is one shape that moves.** A chosen option sits on a dark surface that keeps one layout id from the option to its chip. It's an empty shape, so growing or shrinking it never stretches words; the chip's words fade in where it lands. - **The steps take turns.** Each step cross-fades sideways into the next, and the panel springs its height to fit, so the card changes size without jumping. - **Built on Base UI.** Each step is a Radio Group, so arrow keys move between options and unavailable ones are disabled. The chips are buttons that say what they hold, and the confirmation is announced. # Bulk selection (/docs/components/bulk-select) } minHeight={600}> ## Things to try [#things-to-try] - **Tick a row, then shift-click another.** Everything between is selected, and neighbouring picks join into one band. - **Watch the header box.** It shows when some or all of the rows are selected. - **Look at the foot.** The hint gives way to an action bar whose count rolls as the selection grows. - **Delete.** The chosen rows fold away one after another, and the bar offers Undo. ## Install [#install] ## Dependencies [#dependencies] The install command adds these for you. If you copy the code by hand, install them yourself. ## How it works [#how-it-works] - **TanStack Table models it.** `useReactTable` holds the columns, the row model and row selection, so select-all, the header's mixed state and each row's checkbox read from one selection. Swap in your own data and columns and the rest keeps working. - **Base UI draws the controls.** The checkboxes are Base UI's Checkbox, and the action bar's buttons have Base UI tooltips. - **Motion moves it.** The joined bands, the rolling count and the rows folding away are Motion. The files are made up. # Command palette (/docs/components/command-palette) }> ## Things to try [#things-to-try] - **Press the pill,** or focus it and press ⌘K (Ctrl+K on Windows and Linux). It grows into the palette, with the cursor in the field. - **Type "set".** Results that don't match leave, and the rest slide up to close the gap. Arrow keys move the highlight. Enter opens the highlighted result. - **Choose Projects.** The row's name turns into a chip in the field, and the results narrow to projects. Backspace on an empty field takes the chip out. - **Pick anything.** The palette folds back into the pill, and the pill says what happened ("Opened Billing", "Invite sent") before going back to asking. - **Press Escape,** or click outside, to close it. ## Install [#install] ## Dependencies [#dependencies] ## How it works [#how-it-works] - **One box is both the pill and the palette.** It stays on the page while its contents change, and Motion eases its real width, height and corner radius from pill to panel and back. Nothing is scaled, so the text, the shadow and the rounded corners are drawn as they are at every frame, and the height follows the results as you type. - **It plays in beats.** Opening, the field arrives, then the rows blur in one after another from the top, in a quarter of a second at most however many there are. Closing runs the other way: the contents blur out first, then the panel folds back into the pill. Rows that stay as you type don't arrive again, so only what's new moves. - **Built on Base UI's Autocomplete.** It handles filtering, the highlight, the keyboard and the groups; each row renders as a Motion element so it can arrive in its turn. - **The pill reports back.** It's [Text](/docs/primitives/text), so "Search or jump to…" changes into "Opened Billing" letter by letter, and the same line is announced to screen readers. - **It follows the ARIA combobox pattern.** The field is a `combobox` that owns a `listbox` of `option`s, and `aria-activedescendant` tracks the highlight while focus stays in the field. Groups are labelled. - **⌘K only fires while the demo has focus,** so it doesn't fight the docs' own search. In your app, listen on `window`. # Comment thread (/docs/components/comment-thread) }> A post from an indie developer, Ama Mensah, about shipping her first open-source library, and the readers replying to it. Press play to watch a short scene, or join in yourself. ## Things to try [#things-to-try] - **Watch the typing.** "Abena is typing…" is a small bubble that grows into her comment when it's sent, and Ama's answer arrives the same way. - **Reply.** Yours appears at once marked "Sending…", its line drawing in. When the server confirms it, only the label changes. - **Hover a reply.** The lines from the top down to it darken, and everything off that path fades. - **Like** the post or any comment, and the count ticks over. - **Hide replies.** The branch folds into a pill with the repliers' faces, and its lines retract. - **Sort** by Top or Newest. Whole branches move, lines and all. ## Install [#install] ## Dependencies [#dependencies] The install command adds these for you. If you copy the code by hand, install them yourself. ## How it works [#how-it-works] - Every avatar is an anchor (`data-rm-anchor`), and a reply's avatar links to its parent's (`data-rm-link`). [Connectors](/docs/primitives/connectors) draws the lines. - Motion owns each comment's layout animation when a reply arrives, a branch folds, or the order changes. `useConnectorMotion(connectorRef)` refreshes Remold's SVG paths for the duration, keeping the lines attached without competing for transforms. - **Typing becomes the comment.** The typing indicator is replaced by the sent comment, while the surrounding comments settle into their new positions with a short, interruptible spring. - **Lit lines.** Each avatar on the hovered path carries `data-rm-line="lit"`. Connectors copies it onto the line drawn to that avatar as `data-line`, so CSS can make those lines glow. ### Optimistic replies [#optimistic-replies] A reply is shown before the server has it. Keep its client id when the server confirms it: Motion retains its layout identity, while the connector's anchor stays attached to the same reply. The people, the post and the library are made up. Avatars are illustrations from [DiceBear](https://www.dicebear.com) (the Notionists style), drawn rather than photographed. # Course outline (/docs/components/course-outline) }> Every course platform, creator site and onboarding flow has an outline like this. Here the lines from each module to its lessons are redrawn on every frame of the animation, so they bend and stretch with the rows instead of jumping when something opens, closes or drops away. ## Things to try [#things-to-try] - **Open and close modules.** The lessons slide out of the way and the lines grow and retract with them. - **Tick a lesson off.** The circle becomes a tick, the module's count and the progress bar update, and "Up next" moves on. Finish every lesson in a module and its number fills in. - **Show only what's remaining.** Finished lessons leave, their lines drawing back out, and the rest close up. A finished module disappears entirely. - **Use the keyboard.** Up and down move through the outline; right opens a module, left closes it or goes up to it. ## Install [#install] ## Dependencies [#dependencies] The install command adds these for you. If you copy the code by hand, install them yourself. ## How it works [#how-it-works] Each module's number carries `data-rm-anchor` with the module's id, and each lesson's tick carries `data-rm-link` with its module's id. [Connectors](/docs/primitives/connectors) draws a line from every anchor to everything that links to it. Motion owns the rows' layout animation, while `useConnectorMotion` keeps those lines attached during the transition. ```tsx const connectorRef = useRef(null); const connectorMotion = useConnectorMotion(connectorRef); {rows.map((row) => row.module ? ( {row.n}} /> ) : ( } /> ), )} ``` The course and its lessons are made up. # Currency converter (/docs/components/currency-converter) } minHeight={560}> ## Things to try [#things-to-try] - **Type what you send.** What they get rolls with it, at today's rate. - **Change a currency.** Tap either currency and the card turns into a list of every currency, with a search at the top. Type a code or a name (try "ced"), then pick one with a click or Enter. Picking the currency already on the other side swaps them. - **Swap.** The two currencies trade places and the swap button turns. - **Press Send.** It turns into Sending, then Sent, and goes back. ## Install [#install] ## Dependencies [#dependencies] The install command adds these for you. If you copy the code by hand, install them yourself. ## How it works [#how-it-works] - **Real data, nothing made up.** The rates come from [ExchangeRate-API](https://www.exchangerate-api.com)'s free endpoint, fetched once and shared by every converter on the page. Currency names come from the browser's `Intl.DisplayNames`, decimal places from `Intl.NumberFormat`, and each flag is built from the country letters at the start of the currency's code. While the rates load the card says so, and if they can't be reached it offers to try again. - **One tray that springs.** The card animates its real height to whichever face is showing and clips what's inside, so its edge bounces while text never stretches. - **The label and pill travel.** "You send" and the currency pill fly up to become the list's header (`layoutId`), blurring slightly on the way; the pill turns into the close button. - **Everything else scales in place.** The other parts wait at 94% size, blurred and transparent, and spring in with the rows a beat behind each other. Leaving is quicker than arriving. Pressing a pill grows it a little. - **Built on Base UI.** Autocomplete handles the search, the keyboard and the list; Escape or the pill closes it and focus returns to where you were. - **Motion does every animation:** the tray, the travelling header, the rows, the rolling digits and Send's label. Send is pretend. Wire your transfer up in `send`. ExchangeRate-API's free tier updates daily and asks for the credit shown under the card. # Donation goal (/docs/components/donation-goal) } minHeight={520}> ## Things to try [#things-to-try] - **Give.** Pick an amount and press Donate. A stream of dots rises from below the drop and settles at the waterline, so the drop fills by exactly the share you added. - **Watch the numbers.** The total and the backer count roll and the bar grows. - **Reach the goal.** The water scatters and gathers into a heart, and the buttons give way to a thank-you line. The reset button empties the drop back to where it began. ## Install [#install] ## Dependencies [#dependencies] The install command adds these for you. If you copy the code by hand, install them yourself. ## How it works [#how-it-works] - **One set of dots, always the same size.** The drop is sampled once into points, ordered from the bottom up. Every frame draws all of them: a faint copy for the empty drop and a second copy for the water. `fit` is the picture's frame, so the drop stays still while it fills. - **Filling is moving dots.** Water that isn't raised yet waits, invisible, below the drop. A gift moves the next few into their places in the drop, a handful at a time, so they arrive as a stream. - **Colour is a tone.** Each dot carries a tone that springs with it, and the palette maps it to empty, water or hidden. The colours come from the card's CSS variables, so they follow dark mode. - **The heart is another shape.** At the goal the particles flow into a heart path drawn in the same frame, with a scatter transition, then back into the drop when it starts again. The campaign and the backers are made up. Swap the drop for the shape of what your campaign pays for: any SVG path works. # Mentions (/docs/components/editor-mentions) } minHeight={600}> ## Things to try [#things-to-try] - **Type @.** A picker springs open right at the caret, not under the box. - **Keep typing.** The list narrows to the people who still match, and the highlight glides between them as you point or use the arrow keys. - **Pick someone.** Their face flies out of the list into the sentence as a chip, and they join the row of people who'll be notified. - **Send.** The comment rises into the thread above. ## Install [#install] ## Dependencies [#dependencies] The install command adds these for you. If you copy the code by hand, install them yourself. ## How it works [#how-it-works] - **One face, a new place.** The avatar in the picker and the avatar in the chip are the same element moving, which is what Motion's `layoutId` is for, so the face travels from the list into the text instead of appearing there. - The picker is Base UI's Popover, so focus, dismissal and positioning at the caret are handled for you; the Send button's shortcut hint is Base UI's Tooltip. The people are made up. Avatars are illustrations from [DiceBear](https://www.dicebear.com). # Event RSVP (/docs/components/event-rsvp) } minHeight={600}> ## Things to try [#things-to-try] - **Press RSVP.** The button grows into a sheet around itself and travels down to the bottom of it as Confirm, while the event, the spots left and your choices blur in. - **Pick an answer.** The highlight slides to Going, Maybe or Can’t go. Can’t go turns the reminder off. Hover the ⓘ by the spots left to see what the evening is. - **Confirm.** The button says Saving with a progress bar running along it, and the rest of the sheet locks. - **See your spot.** The sheet springs into the result: a check, "You’re in" with your spot number, and the going count rolls up by one. The button travels down to become Done, which folds it all back into RSVP. ## Install [#install] ## Dependencies [#dependencies] The install command adds these for you. If you copy the code by hand, install them yourself. ## How it works [#how-it-works] - **One tray.** The button, the sheet and the result are faces of one element. It springs its real width, height and radius to whichever face is showing and clips what's inside, so nothing is scaled. - **One part travels.** The main button moves between every face with Motion's `layoutId`; its label reshapes with `Remold.Text` from RSVP to Confirm, Saving and Done. Everything else scales in from just below its size, unblurring, a beat after the part before. - **Built on Base UI.** `Button` for every press, `RadioGroup` and `Radio` for the answer, `Checkbox` for the reminder, `Progress` while it saves, `Tooltip` for what the evening is, and `Separator` between the result's lines. The going count rolls with the Odometer. Nothing is saved. Swap the timeout in `confirm` for a request to your own API. # Expanding search (/docs/components/expanding-search) } minHeight={640}> ## Things to try [#things-to-try] - **Tap the search field.** The greeting above it folds away, the field rises to the top, Cancel slides in beside it, and the shop gives way to your recent searches and what's popular. - **Type "oa".** The list narrows to oat milk, sourdough loaf and rolled oats. What you typed stays quiet and the rest of each name is picked out, and the rows that stay slide into place. - **Press a +.** It turns into a green tick as the product goes in your basket. - **Use the arrow keys.** One highlight slides from row to row, and Enter picks it. - **Press Cancel or Escape.** The greeting unfolds and the shop comes back. ## Install [#install] ## Dependencies [#dependencies] ## How it works [#how-it-works] - **Nothing stretches.** The greeting only folds its height, the field only shortens to make room for Cancel, and Cancel only grows its width from nothing. The shop and the results cross-fade in place. - **Results where the shop was.** It's Base UI's Autocomplete, with the matches rendered into the screen itself rather than a floating menu, so it reads as the screen changing rather than something opening over it. - **Built for the keyboard.** The field is a combobox, the first match is highlighted as you type so Enter picks it, Escape closes, and the recent searches are buttons that fill the field. Each add button says what it adds. # Feedback prompt (/docs/components/feedback-prompt) } minHeight={380}> ## Things to try [#things-to-try] - **Pick a thumb.** The pill grows into a card and your thumb travels into its top-left corner, tinted green or red. The reasons rise in one after another. - **Choose a few reasons.** Each one fills in and gains a tick that slides in from its left. - **Send.** The word gives way to a spinner while it sends and the card steps back, then the card draws into a pill that says thanks and the thumb turns into a check. - **Change your mind.** Press the close button or Escape, and the card folds back into the question. ## Install [#install] ## Dependencies [#dependencies] ## How it works [#how-it-works] - **The card sizes itself to what's inside.** It measures its contents and springs its own width, height and corner radius to fit, so the pill becomes a card without anything in it being stretched. - **The thumb you chose is the one that moves.** It keeps the same layout id from its button to the card's corner, and [Icon](/docs/primitives/icon) later reshapes it into a check. - **Everything else arrives in order.** The title, then the reasons, then the comment box fade and blur in a beat apart, after the card has started to open. - **Sending is its own step.** Pass `sending` while your request is in flight: the card locks, Send keeps its width as its word becomes a spinner, and only when you set `sent` does the card collapse. It's announced at each step. - **Built on Base UI.** The thumbs are Buttons, the reasons a Toggle Group, so each says whether it's pressed, and the comment is a labelled Field. Opening the card moves focus to the first reason, closing returns it to your thumb, and the thank-you is announced. # Filter bar (/docs/components/filter-bar) } minHeight={560}> ## Things to try [#things-to-try] - **Open Filter.** A menu of fields opens, each with its values in a submenu. Picking one adds a segmented token to the bar. - **Change a token's value.** Click it and the new value rolls into place. - **Remove a token** with its ×. The bar closes the gap. - **Watch the list.** The transactions slide into their new places, and both the count and the total spent roll to their new figures. ## Install [#install] ## Dependencies [#dependencies] The install command adds these for you. If you copy the code by hand, install them yourself. ## How it works [#how-it-works] - The menu and its submenus are Base UI's Menu, so keyboard navigation between fields and values works out of the box. - The tokens, the reordering rows and the rolling figures are Motion. The spending is made up. The merchants' marks are from [Simple Icons](https://simpleicons.org). # Focus timer (/docs/components/focus-timer) } minHeight={560}> ## Things to try [#things-to-try] - **Start and pause.** Sand streams through the neck and piles up below while the clock counts down. Pause and the stream hangs in the air until you resume. - **Skip.** The rest of the sand rushes down, the hourglass flips, and the break starts full. Skipping a break moves you on to the next round. - **Reset.** The sand pours back up into the top bulb and the clock returns to the start of the session. - **Watch the rounds.** The line under the clock counts the rounds, and turns green while you rest. ## Install [#install] ## Dependencies [#dependencies] The install command adds these for you. If you copy the code by hand, install them yourself. ## How it works [#how-it-works] - **The sand is a fixed set of grains.** The top bulb is filled with a grid of grains, ordered from the surface down. The bottom bulb holds the same grains mirrored, so grain 10 always has a place in each bulb. - **Time is how many have fallen.** The share of the session that has passed decides how many grains sit in the bottom pile. The next few wait in a thin stream between the neck and the pile, so as time moves on each grain slides down the stream and lands. - **Flipping is a rotation, not a rebuild.** At the end of a session the canvas turns half a circle, and the grains are laid out mirrored from then on. A full bottom bulb is already a full top bulb, so nothing jumps. - **Colour is a tone.** Glass and sand each carry a tone, and the palette is `var(--name)` entries from the card's CSS, so it follows dark mode. The demo runs fast so you can watch a whole session. Set `SPEED` to `{ focus: 1, break: 1 }` for real time, and change `FOCUS`, `BREAK` and `ROUNDS` to suit your app. # Inbox triage (/docs/components/inbox-triage) } minHeight={620}> ## Things to try [#things-to-try] - **Archive or delete one.** Hover a message and use its actions, or swipe it: right to archive, left to bin. The whole row, avatar included, breaks into dust that pours into the Archive or Trash tab, and the tab's count goes up as it lands. - **Select a few.** Tick them, or tick the box at the top for all of them, and use the toolbar. Every selected row pours in together. - **Undo.** The toast counts down for five seconds, and pauses while you hover it. Undo pours the dust back out of the tab into the rows. - **Look in the other tabs.** Archive and Trash list what you moved, and can send it back to the inbox the same way. - **Empty a list.** Its empty state draws itself in dots. ## Install [#install] ## Dependencies [#dependencies] The install command adds these for you. If you copy the code by hand, install them yourself. ## How it works [#how-it-works] - **`useDust` does the pouring.** `trail.away(rows, tab)` samples the rows as they look (avatars, text, colours), pours the dots into the tab, and resolves once they land. Undo is `trail.back(rows, tab)`. - **State is where each message is.** Moving mail just changes its mailbox. An effect compares that with where rows are drawn: rows that left the view pour out, rows that came back pour in, and the rows fold and the counts change once the dust lands. - **Colours come from CSS.** The palette is `var(--name)` entries, so the dust and the empty state follow dark mode. - **Base UI for the parts.** Tabs, Checkbox (with select all), Toolbar, Tooltip, Avatar and Toast. The empty state is particles on their own. The messages are made up and the avatars come from DiceBear. If an avatar can't be read by the canvas, the dust is made without it. # Components (/docs/components) # Invite teammates (/docs/components/invite-team) }> ## Things to try [#things-to-try] - **Type an email and press Enter, Space or a comma.** It becomes a chip with the person's initials. - **Paste several at once.** A list separated by commas, spaces or new lines becomes a chip each. Backspace in an empty field takes the last one back. - **Press Invite.** It says "Sent to 2" for a moment, and each chip moves down into the list as an invite. They accept one at a time, and each invite travels on into its place among the team, by name, where it says "Joined" before it settles in. - **Revoke one while it's pending.** It leaves the list and never joins. - **Try a bad address, or someone already invited.** The field shakes and says what's wrong under it. ## Install [#install] ## Dependencies [#dependencies] ## How it works [#how-it-works] - The chip, pending row and member row use Motion layout and a shared identity, so a person stays legible while moving between each state. - The team is listed by name, so someone who joins moves to their place, and the rows around them make room. - **Invite lives inside the field**, at the end of its last line. However many lines the emails wrap onto, the button stays level with the typing instead of hanging off the first line. - **It says what just happened.** Invite turns green and says "Sent" (or "Sent to 2") for a moment, and someone who joins says "Joined" in green where their role goes, then settles into it. - **Refusals shake.** A bad address, or someone already invited, gives the field a small shake and opens the reason under it; a new reason morphs from the old one. - **The card springs to fit.** Its height follows what's inside with one spring, and it clips while it moves, so nothing in it is ever stretched. Sent chips step out of the field at once, so it doesn't grow and then shrink. - **Monograms say who's in.** Grey while someone is only an email or an invite, warming into their own colour as they join. The member count in the header morphs as the team grows. - Motion runs when people change: a chip added, an invite sent, someone joining or revoked. Typing in the field doesn't animate anything. - The invites are pretend. Send your real ones in `send` and call `join` when someone accepts. # Kitchen ticket (/docs/components/kitchen-ticket) } minHeight={560}> ## Things to try [#things-to-try] - **Open the ticket.** It springs open from the status button, and a highlight glides to whatever you point at. - **Cook the order.** The ring fills: empty when the ticket comes in, half on the grill, most of the way when it's at the pass. - **Hand it over.** Picked up fills the circle and draws a check. Canceled draws a cross. Switch between the two and they reshape into each other. - **Read the line.** Ticket in, on the grill, at the pass. For here and to-go use the same menu. ## Install [#install] ## Dependencies [#dependencies] The install command adds these for you. If you copy the code by hand, install them yourself. ## How it works [#how-it-works] - **One picker, used twice.** The ticket and for-here or to-go share a `Picker` built on Base UI's Select, with Motion on top for the opening spring and the gliding highlight. - **Check and cross.** The two marks are reshaped into each other with `Remold.Icon`; the ring's fill is a plain stroke animation. The order and the people are made up. # Label picker (/docs/components/label-picker) } minHeight={560}> ## Things to try [#things-to-try] - **Add a label.** "Add label" opens a picker with a search box, and a highlight glides to what you point at. - **Pick one.** It drops onto the issue as a chip while the others make room. - **Type a new name.** The list offers to create it, with a colour that pops in. - **Take a chip off** with its ×. ## Install [#install] ## Dependencies [#dependencies] The install command adds these for you. If you copy the code by hand, install them yourself. ## How it works [#how-it-works] - The picker is Base UI's Popover, so focus moves into the search box and back to the button when it closes. - The gliding highlight and the chips making room are Motion. The issue is made up. # Logo morph (/docs/components/logo-morph) } /> Pick a brand or flip **Text / Logo**, or press Play to run each name into its logo and back. ## Take it apart [#take-it-apart] Every colour below is one piece of the outline, from start to finish, so you can see which letter becomes which part of the logo. Drag the slider, or press play. Switch **Leftover pieces** to `merge` and drag to 100%: the extra letters are still there, parked as small shapes beside the logo. That's why the component uses `collapse`. ## Things to try [#things-to-try] - **Flip Text and Logo quickly.** Each change starts from the outline as it is at that moment, so it turns round mid-air instead of snapping back to the start. - **Go from one logo to another.** With Logo on, pick a brand that has a mark too: the marks morph straight into each other. - **Turn on Reduce motion** in your system settings. It jumps to the new shape instead of moving. ## Install [#install] ## Dependencies [#dependencies] ## Usage [#usage] Each brand is a name and, if you have one, its logo as SVG path data. The path can be in any coordinates: it's scaled into the same box as the word. ```tsx import { LogoMorph } from "@/components/remold/logo-morph"; export function Partners() { return ( ); } ``` One brand with autoplay off is a header mark that turns into your name when someone reaches for it. ```tsx ``` ```tsx ``` ## How it works [#how-it-works] ### Letters become shapes [#letters-become-shapes] The name is laid out from real glyph outlines (Barlow Condensed), letter by letter with its kerning, so every letter is a closed shape rather than text. ### Both are fitted into one box [#both-are-fitted-into-one-box] The word and the logo are each scaled to fit a 640 × 220 box and centred in it, so a long name and a square mark end up the same visual size, whatever coordinates the logo's path was drawn in. ### Pieces are paired [#pieces-are-paired] Core's [`createMorph`](/docs/core/matching) splits both outlines into pieces and pairs them by position and size, using the Hungarian method, so each letter goes to the nearest part of the logo. The side with fewer pieces gets padding: here, extra pieces that shrink to nothing (`padding: "collapse"`). ### A spring drives it, from wherever it is [#a-spring-drives-it-from-wherever-it-is] A spring takes the morph from 0 to 1. Every change builds a new morph from the outline on screen right now, so flipping halfway turns round smoothly and keeps going. ## API reference [#api-reference] ### Props [#props] ### `brands` [#brands] ## Picking marks that morph well [#picking-marks-that-morph-well] - **Solid, filled marks** morph best. If a logo is drawn with strokes, convert the strokes to outlines first (in Figma: _Outline stroke_). - **One colour, one path.** The morph uses the shape, not the colours. Join a logo's paths into one `d` string. - **Roughly the visual weight of the word.** A heavy wordmark into a hairline mark looks thin halfway. - **Fewer pieces than the name is fine.** The extra letters shrink into their partners. ## Accessibility [#accessibility] - The picture is an image labelled with what it shows ("Cursor" or "Cursor logo"), and the label changes with it. - The brand buttons are tabs, and Text / Logo are toggle buttons with their pressed state, so screen readers hear what's chosen. - Autoplay is off unless you pass `autoplay`. When it's on, it only runs while it's on screen and stops for good as soon as someone clicks or types inside it ([WCAG 2.2.2](https://www.w3.org/WAI/WCAG22/Understanding/pause-stop-hide)). Play and Pause stay available. - With reduced motion, it changes shape without moving. The Cursor and X marks come from [Simple Icons](https://simpleicons.org) (CC0) and belong to their owners. Simple Icons doesn't carry AWS or OpenAI marks, so those two are names only. # Morphing dialog (/docs/components/morphing-dialog) } minHeight={480}> ## Things to try [#things-to-try] - **Press New trip.** The button's dark surface grows into the dialog while everything behind it dims, turning white as it arrives. Then the form fades in. - **Cancel, or press Escape.** The dialog shrinks back into the button it came from. - **Type a destination and press Create trip.** The button shows a spinner, then the dialog shrinks into the new trip at the top of the list, marked New. The other trips make room. - **Pick who's going.** The selection slides between Solo, Couple, Family and Friends. ## Install [#install] ## Dependencies [#dependencies] ## How it works [#how-it-works] - **One surface, three places.** The button's background, the dialog's background and the new row's background share a layout id, so it's one surface moving between them. It's an empty shape, so growing it never stretches any words; the form fades in once it has arrived, and its colour and corners change on the way. - **The dialog becomes what it made.** On create, the surface is handed to the new row instead of back to the button, and the button gets a fresh one. Pass `surface` and bump it each time something is created. - **A real modal.** It's Base UI's Dialog: focus moves to the destination field and stays inside, Escape and Cancel close it, focus returns to the button, and the title and description are announced. The destination is a labelled Field and who's going a Toggle Group. - **It waits for the request.** While creating, the form locks and the button shows a spinner at the same width, so nothing shifts. In the demo a timeout stands in for the request. - **Mount it where you like.** By default it mounts on the page and dims all of it. The demo passes its own stage as `container`, so the dim covers just the frame it sits in, and it turns `modal` off until you touch it, so it can play on its own without taking over the page. # Onboarding checklist (/docs/components/onboarding-checklist) } minHeight={600}> ## Things to try [#things-to-try] - **Tick a step.** The next one opens in place while the finished one settles back, and the ring fills as you go. - **Finish the last step.** The whole card folds down into a small "All set" pill that stays out of your way. - **Hide it early.** The pill remembers how far you got; open it to bring the card back. ## Install [#install] ## Dependencies [#dependencies] The install command adds these for you. If you copy the code by hand, install them yourself. ## How it works [#how-it-works] - **Card to pill.** The card and the pill are two different pieces of markup, so [Morph](/docs/primitives/morph) animates the change: it pairs the ring, the title and the count across both shapes, with no ids. - The ring and the ticks are Motion; the checkboxes are Base UI's Checkbox. # Order tracker (/docs/components/order-tracker) } minHeight={460}> ## Things to try [#things-to-try] - **Watch it play.** Each stage comes alive on its own: the lid lifts off the pot and the steam puffs out, the sealed bag hops and its handle swings, the scooter's wheels spin as the road rushes past, and the front door swings open as the badge pings. - **Tap a step.** It stops playing and jumps there: the drawing holds still, reshapes, and that stage comes alive. - **Jump around.** Go from Preparing straight to Delivered and the pot reshapes into the house in one move. ## Install [#install] ## Dependencies [#dependencies] The install command adds these for you. If you copy the code by hand, install them yourself. ## How it works [#how-it-works] - **Four layers, one drawing.** The illustration is four `Remold.Icon` layers on the same frame: the dark ink, the green, the white details and the soft grey. Each one morphs its own shapes, so every colour stays where it belongs. - **Live, then morph.** Each stage's motion is plain CSS on the layers: the whole object (a hop, a bump) and its moving part on top (the lid, the handle, the door), plus spokes that spin on the hubs. It holds still while the layers reshape, then the next stage's motion starts, so the two never fight. - **Swap in your own art.** Each stage is plain path data per layer. Draw your stages on the same viewBox and the morph pairs the shapes for you. - **The words follow.** The headline and the line under it are `Remold.Text`, so only the letters that change move. The kitchen, the rider and the times are made up. # One-time code (/docs/components/otp-input) }> ## Things to try [#things-to-try] - **Type six digits.** The last one lands, and the cells fold into a dark "Verifying" pill with a spinner. - **See an error.** Enter `482910`. The field shakes once, then reshapes into a red “Try again” pill. Choose another code to return to the cells. - **Watch it pass.** The pill turns green, the spinner becomes a check and "Verifying" becomes "Verified". The same envelope morphs into one with a tick. A wrong code morphs it into a removed envelope first. - **Paste a code**, or let your phone fill it in. One real input sits under the cells, so typing, pasting and one-time-code autofill all work. - **Use another code** (it was "Resend code" a moment ago) to start again. ## Install [#install] ## Dependencies [#dependencies] The install command adds these for you. If you copy the code by hand, install them yourself. ## How it works [#how-it-works] - Motion lays out the field as it contracts into a status pill, and gives the state word a short enter/exit transition. - The envelope is one icon the whole way, so it morphs between the open, removed and validated marks. The status icons and copy are ordinary React elements; the colour transition and error shake are plain CSS. - **The colour** is plain CSS: the pill's background eases from ink to red or green. The error shake uses only `translate`, runs once, and is removed for reduced motion. In an app the result would come from your server; here `482915` passes and other six-digit codes fail. The address is made up. # Particle shapes (/docs/components/particle-shapes) } minHeight={520}> ## Things to try [#things-to-try] - **Watch it play.** The galaxy turns, then its arms unwind and stretch into two twisting strands. The strands loop into the Lorenz attractor's two wings, which fold into a honeybee, stripes, wings and all, then open out into a butterfly and stand up as the Remold mark before it gathers back into the galaxy. - **Move your cursor through it.** The dots part around the pointer and drift back as it leaves, as if you were moving through space. - **Pick a shape.** It stops playing and flows straight there, from wherever the dots are. ## Install [#install] ## Dependencies [#dependencies] The install command adds these for you. If you copy the code by hand, install them yourself. ## How it works [#how-it-works] - **Shapes from parts.** Each shape is built with `shapes` from `@remold-ui/core`: `spiral`, `helix` and `attractor("lorenz")` are ready-made, the bee and the butterfly are each a `compose` of ellipsoids, spheres, curves and ovals, and the mark is its SVG path made solid with `fromPath`. The parts are moved with `.at`, `.rotate` and `.mirror` and coloured with `.tone` and `.stripes`. - **Remold draws it.** `` takes the shape and a `count`, turns it with `camera`, and makes overlapping dots glow with `blend="add"`. Colours come from a named `palette`, so each part says `"gold"` or `"wing"` rather than a number. - **A scatter in between.** `transition="scatter"` dissolves each shape into a glittering cloud that spreads through the depth too, holds a moment, then gathers into the next. - **One flow, not a swarm.** `pairing="flow"` pairs dots by where they sit along each shape, so the galaxy's core becomes the top of the helix and the path's start becomes the bee's tail. - **Cheap when it can be.** It only draws while on screen, stops turning when paused (`camera={{ spin: 0 }}`), and uses fewer dots on narrow screens. Behind it, a `` paints a still field of faint stars once. # Payment status (/docs/components/payment-status) } minHeight={480}> ## Things to try [#things-to-try] - **Watch it play.** The card floats, becomes a lock whose shackle clicks shut, comes back with a warning when the bank says no, then goes through and becomes a receipt. - **Pick a status.** It stops playing and jumps there. Go from Card to Paid and the card reshapes straight into the receipt. ## Install [#install] ## Dependencies [#dependencies] The install command adds these for you. If you copy the code by hand, install them yourself. ## How it works [#how-it-works] - **Status in, drawing out.** Pass `status` as `card`, `processing`, `declined` or `paid`, straight from your payment's state, along with the `amount`, the `card` and the receipt `email`. - **Layers that reshape.** The card, the lock and the receipt are the same `Remold.Icon` layers drawn differently, so each change morphs instead of swapping. The accent turns red for a refusal and green once it's paid. - **Motion that says what's happening.** The shackle clicks while the bank confirms, and the card shakes its head when it's declined. Both are plain CSS and hold still while the drawing reshapes. The amount, the card and the email are made up. # Phone input (/docs/components/phone-input) } minHeight={360}> ## Things to try [#things-to-try] - **Type a number.** Spaces appear as the number reaches them. - **Search countries.** Search by name, country abbreviation, or dialing code across the ten example countries. - **Change the country.** The same digits regroup into the new format when the input is not focused. Extra digits are preserved and a length hint appears. - **Paste a full number.** A pasted country code or leading zero is dropped. ## Install [#install] ## Dependencies [#dependencies] The install command adds these for you. If you copy the code by hand, install them yourself. ## How it works [#how-it-works] - **Built on Base UI.** `Field` gives the label and description, and `Combobox` provides the searchable country picker. - **Native editing.** The real input is visible while focused, supporting caret placement, selection, and deletion. The formatted animated preview returns on blur. - **Separators slide.** Digits are keyed by their position and separators by their order, so a space shared by both formats moves instead of being redrawn. Formats are simplified examples, not full numbering plans. # Music player (/docs/components/podcast-player) }> } /> Press play, or tap the bar, to open the player; the chevron folds it back. ## Things to try [#things-to-try] - **Open the player.** The thumbnail grows into the cover, the title and artist lift into place under it, and the play button settles into the middle of the controls. - **Press play.** The sound waves start to move, and the part already played darkens. The player has a scrubber, skip back 15 and forward 30, and on this page it plays the real song preview. - **Fold it back** with the chevron. The cover shrinks back into exactly the thumbnail it came from. - **Open it halfway through closing.** Every change starts from where things are on screen, so it just turns around. ## Install [#install] ## Dependencies [#dependencies] ## How it works [#how-it-works] - The mini bar and full player are different markup. A Motion `LayoutGroup` gives the artwork, title, show and play control matching `layoutId`s, so the compact player genuinely expands rather than being swapped. - The play button is the same dark disc in both shapes, only bigger, so it reads as one control. Play and pause are one [Icon](/docs/primitives/icon) that morphs between the two symbols. - `episodes` takes `{ id, show, title, artwork, colors, audio, seconds }`, and the first one plays. With `audio` the player plays the file; without it, play just runs the clock. On this page the songs are Kofi Kinaata's 30-second previews from Apple Music, through a small cached server route; the music and artwork belong to their creators. Installed, the component shows a made-up show with drawn artwork. # Popover menu (/docs/components/popover-menu) }> ## Things to try [#things-to-try] - **Open the menu.** The ellipsis grows into the tray around itself, and the rows blur in one after another from the top. - **Pick an action.** The menu closes cleanly after the click, leaving the trigger as the familiar ellipsis for the next action. ## The split of responsibilities [#the-split-of-responsibilities] Use Base UI's `Popover` for the things that must be correct: anchored positioning, focus, Escape, and outside-click dismissal. Motion drives one clipped tray: the ellipsis key expands into the photo menu, ending up in its top-right corner as the close button, then the rows rise out from beneath it. The rows are action links, so a product can attach its own navigation or side effect without the menu inventing a selected state. In this demo they use placeholder targets and simply close the menu. ## Details worth keeping [#details-worth-keeping] - The tray is one element, rather than a trigger plus a detached popup card. - The tray expands from the trigger's centre; menu rows arrive only after there is room for them. - Closing runs the other way: the rows blur out first (Base UI waits for the popup's `data-ending-style` transition), then the tray shrinks back around the trigger. - Reduced-motion users get an opacity-only transition. # Product tour (/docs/components/product-tour) } wide> ## Things to try [#things-to-try] - **Press Take the tour.** The pill in the corner grows into the first tip, the app dims, and a window opens around New report. A dotted line runs from the button to the tip, and a light travels along it. - **Press Next.** The window springs to the search field, the tip moves beside it, and its title and text change letter by letter. The old line draws out and a new one draws in. - **Use the arrow keys,** or Back and Next. Escape ends the tour from any step. - **Finish.** The window opens past the edges, the dim fades, and the tip folds back into the pill it came from, ready for next time. ## Install [#install] ## Dependencies [#dependencies] ## Make it yours [#make-it-yours] - **Your steps** are `STEPS`: the control to point at, a title, a line of text, and which side of the control the tip sits on. The tip stays inside the window, whichever side you choose. - **Your app** is the mock between the comments. Wrap each control you want to point at in `Remold.Anchor` with a `name`, and use that name as a step's `target`. - **Your look:** the dim, the ring round the window and the line's dash are CSS custom properties and plain styles in `product-tour.css`. ## How it works [#how-it-works] - **The spotlight** is one element with a wide shadow that dims everything outside it. It springs to each control with a CSS transition whose easing is a spring curve, so it overshoots a touch and settles. - **The tip is the help button.** One element is both. When the tour starts, a [Morph](/docs/primitives/morph) grows it out of the corner into the tip. Each step, it moves the tip to its new place. At the end, it folds the tip back into the pill. - **The line** is [Connectors](/docs/primitives/connectors): each control is an anchor, and the tip links to the current one. Changing the link draws the old line out and the new one in, and `pulse={step}` sends a light along it on every step. It stays attached while the tip moves. - **The words** are [Text](/docs/primitives/text), so the title, the text and "1 of 4" change letter by letter instead of swapping. - **Every control is measured once,** and again on resize, so the spotlight and the tip are placed in the same render that changes the step. - **It's a dialog while it's open.** It's labelled by the tip's title and described by its text. Focus moves to Next when the tour starts and back to the pill when it ends. The app behind is `inert`, so keyboard focus stays in the tip. # Project setup (/docs/components/project-setup) }> ## Things to try [#things-to-try] - **Tick a step.** Its piece drops from where it hovered, as a ghost, into its place in the picture, and turns solid. - **Tick the ones it connects to.** The line between two pieces appears as soon as both exist: the domain flows into the API, the API reaches the database. - **Untick a step.** Its piece lifts back out of the picture and turns into a ghost again. - **Finish every step.** The picture is the whole project. ## Install [#install] ## Dependencies [#dependencies] ## How it works [#how-it-works] - The picture is one [Scene](/docs/primitives/scene). Each piece is an `extrude()` solid with an icon on its top, drawn in the `sketch` palette as ink lines. - There's one layout for every set of finished steps, so the steps can be done in any order. Ticking one switches layouts, and the scene springs every piece to its place in the new one. - A piece that isn't built yet is a `ghost`: its fill fades and its outline stays, a little above where it'll land. Built, it drops into place on an `arc`. - Lines say which layouts they appear in, so each one only shows once both of its ends are built. - Setup is pretend. Mark steps done from your own data, and describe your project's pieces in `STEPS` and `PLACES`. # Promo code (/docs/components/promo-code) } minHeight={520}> ## Things to try [#things-to-try] - **Try SUMMER.** The field gives a small shake and says the code has ended, keeping what you typed. - **Try SAVE20.** It checks the code, then the field springs down into a green ticket chip, “SAVE20 · 20% off”, the discount line opens and the total rolls down, with the old price struck through and a “Saved” badge beside it. - **Press ×.** The chip springs back out into the empty field and the total rolls back up. ## Install [#install] ## Dependencies [#dependencies] The install command adds these for you. If you copy the code by hand, install them yourself. ## How it works [#how-it-works] - **Built on Base UI.** `Field` handles the invalid state and the error message; the code box is its `Control`. - **One box.** The field and the chip are the same element. It springs its real width, height and radius and clips what's inside, so the text never stretches while it shrinks. The field's contents blur out quickly as the chip's arrive. - **Choreographed, not all at once.** Apply checks the code first, then the box springs into the chip, and the chip's ticket, code, offer and × arrive one after another. The discount line opens a beat later, the old total slides in and is struck through, and only then does the new total roll down. Removing runs back at once and puts focus back in the field. - **Torn like a ticket.** The total sits on a stub: a dashed tear line between two notches cut into the card with a CSS mask. - **One spring.** Everything that changes shape shares it, with a little bounce. The total rolls with the Odometer, and the shake runs on a wrapper so it never fights the spring. SAVE20 and WELCOME10 work; SUMMER has expired. Swap the timeout in `apply` for a request to your own API. # Quantity stepper (/docs/components/quantity-stepper) } minHeight={260}> ## Things to try [#things-to-try] - **Press Add.** The button stretches into minus, count and plus. - **Count up and down.** The number rolls like a counter. - **Go back to one.** The minus becomes a bin; press it and the stepper folds back into Add. ## Install [#install] ## Dependencies [#dependencies] The install command adds these for you. If you copy the code by hand, install them yourself. ## How it works [#how-it-works] - **Built on Base UI.** `NumberField` gives increment, decrement, the arrow keys and the min and max. - **The button becomes the stepper.** Both share a Motion `layoutId`. The minus reshapes into the bin with `Remold.Icon`, and the count rolls with the Odometer. The product picture is an illustration. # Range slider (/docs/components/range-slider) } minHeight={340}> ## Things to try [#things-to-try] - **Drag a handle.** Its price moves from the label to above your finger. - **Let go.** The price flies back into the label. - **Watch the bars.** They darken inside your range, and the count below updates. ## Install [#install] ## Dependencies [#dependencies] The install command adds these for you. If you copy the code by hand, install them yourself. ## How it works [#how-it-works] - **Built on Base UI.** `Slider` with a thumb for each end gives dragging, keyboard and the minimum gap between them. - **The price follows you.** The label's price and the bubble share a Motion `layoutId`, so it travels between them. The prices and counts are made up. # Revenue (/docs/components/revenue-card) } minHeight={600}> ## Things to try [#things-to-try] - **Switch between 7, 30 and 90 days.** The chart line doesn't redraw: it reshapes into the new range. - **Watch the numbers.** The MRR rolls like a counter, and the change badge rolls too, its arrow turning between up and down. - **Scrub along the chart.** A dot rides the line and the headline shows that day's figure. ## Install [#install] ## Dependencies [#dependencies] The install command adds these for you. If you copy the code by hand, install them yourself. ## How it works [#how-it-works] - **One line, reshaped.** The chart line is a path morphed with Remold's [`createMorph`](/docs/core/shapes) and the badge's arrow is a `Remold.Icon`, so a new range is the same line moving into new points, and up and down are the same arrow turning. - The range switch is Base UI's Toggle Group; the figures roll with a small odometer built on Motion. The numbers are made up. # Seat calculator (/docs/components/seat-calculator) } minHeight={720}> ## Things to try [#things-to-try] - **Drag the slider, or type a number.** Faces join and leave your team and the price rolls like a counter. - **Cross a tier.** The plan steps up and brings its extra features with it. - **Switch to yearly.** The monthly price is struck through and the saving is shown. - **Go past the last tier.** The price gives way to a conversation. ## Install [#install] ## Dependencies [#dependencies] The install command adds these for you. If you copy the code by hand, install them yourself. ## How it works [#how-it-works] - **One number drives everything.** The slider and the number field are Base UI's Slider and Number Field bound to the same seat count, and every other part is derived from it. - The faces, features and plan changes are Motion; the price rolls with a small odometer. Billing period is Base UI's Toggle Group. The prices are made up. # Send a message (/docs/components/send-message) } minHeight={560}> ## Things to try [#things-to-try] - **Start typing.** The mic turns into a send arrow. - **Send.** Your message rises out of the composer into the thread, pushing the others up. - **Wait for a reply.** Their face slides down to your message once they've read it, a typing bubble bounces in, and their reply takes its place. ## Install [#install] ## Dependencies [#dependencies] The install command adds these for you. If you copy the code by hand, install them yourself. ## How it works [#how-it-works] - **Mic to arrow.** The composer's button reshapes between the two icons with `Remold.Icon`. - **The same face, a newer message.** The read receipt is one small avatar moving to the latest message it has seen, which is what Motion's `layoutId` is for. The people are made up. Avatars are illustrations from [DiceBear](https://www.dicebear.com). # Send money (/docs/components/send-money) } minHeight={720}> ## Things to try [#things-to-try] - **Tap in an amount.** Each digit rolls in from below and the commas make room as the number grows. A long amount shrinks to fit. On a computer you can also click the amount and type. - **Go over your balance.** The digit that would take you past it is refused with a shake, rather than accepted and flagged later. - **Send.** The arrow turns into a spinner, then a check. ## Install [#install] ## Dependencies [#dependencies] The install command adds these for you. If you copy the code by hand, install them yourself. ## How it works [#how-it-works] - **Arrow, spinner, check.** The Send button's icon is one `Remold.Icon` reshaped from icon to icon, so each state grows out of the last. - The rolling digits, the shake and the fit-to-width are Motion. The people are made up. The avatar is an illustration from [DiceBear](https://www.dicebear.com). # Share link (/docs/components/share-link) }> ## Things to try [#things-to-try] - **See who has access.** The faces are the people who can already open the file; hover one to see who it is and what they can do. - **Press Share.** The faces tighten to the left as the pill springs open into the link, a Can view / Can edit switch and a Copy button, each arriving a beat after the one before. - **Choose who it lets in.** The highlight slides between Can view and Can edit. - **Copy it.** The button turns green and says Copied, then the row folds back into the pill, which says "Link copied" for a moment before going back to Share. - **Close it** with Escape or by clicking anywhere else. ## Install [#install] ## Dependencies [#dependencies] The install command adds these for you. If you copy the code by hand, install them yourself. ## How it works [#how-it-works] - **One pill.** The closed button and the open row live in the same pill. It springs its real width to whatever it's showing and clips what's inside, so the text and buttons are never stretched. - **The label swaps.** Share becomes Link copied, and Copy becomes Copied: Motion blurs the old word and icon out as the new ones blur in. - **Built on Base UI.** `Button` for Share and Copy, `ToggleGroup` and `Toggle` for who the link lets in, and `Tooltip` for each face. Screen readers hear "Link copied" and who can use it. Copy writes the link to your clipboard, with the access you chose as a query string. Swap in your own link and permission call. # Adopt your signature (/docs/components/signature) }> ## Things to try [#things-to-try] - **Draw your signature** with a mouse, pen or finger. The ink is thicker where you slow down and thinner where you speed up, and it tapers where the pen lifts. - **Switch to Type.** The ink you drew flows into your name, set in a script. - **Change your name.** The typed signature follows each letter. - **Switch back to Draw.** The script flows back into your own handwriting. ## Install [#install] ## Dependencies [#dependencies] ## How it works [#how-it-works] - Both styles are filled outlines. A drawn stroke is turned into ink: both edges of the line, its width following the pen's speed. The typed name is the real outline of each letter, read from the font ahead of time, so no font loads in the browser. - One [Icon](/docs/primitives/icon) shows the ink as path data. When the style changes, it pairs the strokes of one with the letters of the other and morphs between them. Strokes with no partner grow out of the nearest ink. - While you draw, the stroke under the pen is drawn plainly and joins the ink when the pen lifts, so drawing never animates. - The script is Great Vibes, under the SIL Open Font License. Set your own with `scripts/extract-signature-glyphs.mjs`. - Adopting is pretend. Save the path data in the Adopt button's handler: it's the signature, ready to place on a document. # Split amount (/docs/components/split-amount) } minHeight={440}> ## Things to try [#things-to-try] - **Add someone.** Tap a face under "Also at the table": it flies up into the split, the bar re-divides and every share rolls down. - **Leave someone out.** Press × on their row and they go back to the table; the others' shares grow. - **Change the tip.** The total and every share roll to the new amount. - **Request the money.** The button asks the others for what they owe, then says it's sent. - **Look at the cents.** The shares always add up to the bill exactly. ## Install [#install] ## Dependencies [#dependencies] The install command adds these for you. If you copy the code by hand, install them yourself. ## How it works [#how-it-works] - **One face, two places.** Each person's face is the same Motion element in the tray and in the list, so it travels between them when they join or leave the split. - **The bill re-divides.** Each slice of the bar is a Motion flex item, so they resize together; the amounts roll with the Odometer. - **Built on Base UI.** `Button` for adding, removing and requesting; `ToggleGroup` for the tip. `SplitAmount` takes `value` / `onValueChange` (who's paying and the tip) or runs on its own, plus your `diners` and an `onRequest` hook. - **No lost cents.** What's left after an even split goes a cent at a time to the first people. # Stepper slider (/docs/components/stepper-slider) } minHeight={320}> ## Things to try [#things-to-try] - **Press + or −.** The number moves a step at a time and rolls to its new value. - **Drag the number sideways.** The stepper stretches into a slider and the number rides on the thumb in a dark pill. Let go and it folds back into the stepper, the number springing back to the middle. - **Click the number and type one.** It's a real number field, kept within the range and to the step. ## Install [#install] ## Dependencies [#dependencies] ## Usage [#usage] ```tsx const [months, setMonths] = useState(6); ``` Use it anywhere people usually nudge a number but sometimes need a big jump: a quantity, a tip, a number of payments, a duration. ## How it works [#how-it-works] - **One control, two shapes.** It's a Base UI Number Field until you drag the number, then a Base UI Slider. Only the control's width changes, so nothing in it stretches, and one value pill springs between the stepper's centre and the thumb. - **The drag starts where you are.** The slider takes over relative to where you started dragging, so the thumb begins under the value you already had rather than jumping. - **Built for every input.** − and + are buttons with clear labels, the number is a real input you can type in, the slider works with the arrow keys, and pass `stretched` to show it as a slider without dragging. # Streak (/docs/components/streak) } minHeight={560}> ## Things to try [#things-to-try] - **Keep the streak.** The grey ember reshapes into a full flame and catches colour. - **Watch the burst.** Sparks fly out of the flame as it lands, and the day count rolls up by one. - **Look at the week.** Today's dot fills in alongside the days you've already kept. ## Install [#install] ## Dependencies [#dependencies] The install command adds these for you. If you copy the code by hand, install them yourself. ## How it works [#how-it-works] - **The ember grows into the flame.** The flame is one path reshaped with `Remold.Icon`, so the ember genuinely becomes the flame rather than one icon swapping for another. - The sparks, the colour and the rolling count are Motion. # Table booking (/docs/components/table-booking) } minHeight={500}> ## Things to try [#things-to-try] - **Pick a new time.** The card springs into the updated booking, and the time you chose travels from its option into the booking's header while the notice and the other option step out of the way. - **Change the party.** Press the guest count to open the party-size picker. Pick a number and you're back on the same booking with the new count already in place. - **Go back** with the arrow. The booking settles back into the options it came from. Hover the confirmation code to see what it's for. ## Install [#install] ## Dependencies [#dependencies] The install command adds these for you. If you copy the code by hand, install them yourself. ## How it works [#how-it-works] - **One card, three faces.** The notice, the booking and the party-size picker are faces of one card. It springs its real height between them and clips what's inside, so nothing is scaled. - **The time carries over.** The time you pick is the same chip in the option and in the booking, so Motion's `layoutId` moves it without resizing it. Everything else scales in from just below its size, unblurring, a beat after the part before. - **Built on Base UI.** `Button` for the options and every press, `ToggleGroup` and `Toggle` for the party size, whose highlight slides to your pick, and `Tooltip` on the confirmation code. The restaurant and the booking are made up. # Tag input (/docs/components/tag-input) } minHeight={380}> ## Things to try [#things-to-try] - **Type a word and press comma.** The chip forms around the word without it moving. - **Pick a suggestion.** It flies up into the field. A new word is offered as “Create”. - **Press Backspace in an empty field.** The last tag comes off. ## Install [#install] ## Dependencies [#dependencies] The install command adds these for you. If you copy the code by hand, install them yourself. ## How it works [#how-it-works] - **Built on Base UI.** `Combobox` in `multiple` mode gives the chips, the suggestions, keyboard navigation and removal. - **The text becomes the chip.** A new chip starts with no padding, no tint and no dot, exactly where the typed text sat, then all three grow in with Motion. - **Every tag keeps its colour.** Known tags have one; a new tag gets one from its letters, so it's the same every time. The chip is a soft tint of it, and fills with it when you arrow back onto it to remove it. - **Picked tags fly in.** Each suggestion is the tag's dot and word. Pick one and the word lifts out of its row and flies up into the field as a chip; its remove button grows in once it lands. Added tags leave the list, and come back when removed. - **A limit you can see.** The counter under the field rolls as tags come and go, and turns amber at six, when the field stops taking more. The suggested tags and their colours are examples. Pass your own. # Task list (/docs/components/task-list) }> ## Things to try [#things-to-try] - **Click an issue's status to complete it.** The row leaves its group, and its ID travels down into the folded Done row with the latest others. It lands picked out in Done's colour, then settles in, and Done's count rolls up as it arrives. - **Open Done.** The IDs spread out of the folded row into rows of their own, newest first. - **Reopen a done issue.** It goes back to its own place in Todo or In Progress, and the rows around it make room. ## Install [#install] ## Dependencies [#dependencies] ## How it works [#how-it-works] - An issue's row and its ID in the folded Done row are different markup in different places. Motion gives both the same `layoutId`, so the issue travels between them instead of vanishing. - Opening Done is the same shared-layout transition in reverse: each ID in the folded row becomes the ID of a row. - **The counts roll.** Each group's count is an `Odometer`, so the number turns over instead of swapping. Done's count waits for the ID to land before it ticks, so the number follows what you see happen. - **The newest is picked out for a moment.** Only an issue that just joined Done gets the tint, so reopening one doesn't light up its neighbour. - Groups are just filtered from one list of issues and rendered again. Nothing tracks positions, so a reopened issue lands back in its original place. - Completing is pretend. Save it in `toggle`, and pass your own `ISSUES`. # Two-factor setup (/docs/components/two-factor) }> ## Things to try [#things-to-try] - **Switch two-factor on.** The setup opens between the rows and "Sign-in alerts" steps down to make room. - **Type any six digits, or paste them.** Once the code is full it's accepted, and the setup folds into "On · Authenticator app". - **Switch it off.** Everything returns to where it was. ## Install [#install] ## Dependencies [#dependencies] ## How it works [#how-it-works] - **The setup unfolds by its height.** Motion springs it from nothing to its natural height with just a touch of give, so "Sign-in alerts" is pushed down smoothly rather than jumping. What's inside fades in once there's room for it. Closing runs the other way: the contents fade out first, and only then does the box fold, so its shrinking edge never cuts through the code or the QR. The box only ever changes height, so no words stretch. - **The status line swaps in place.** "Ask for a code when signing in" lifts away as "On · Authenticator app" settles in. - **The switches slide.** Each knob is a Base UI Switch thumb that Motion springs along its track, so it slides to the other end instead of jumping there, and the track's colour fades with it. - The code boxes sit over one real input, so typing, pasting and one-time-code autofill all work as usual. - The QR code is drawn. Put your real one in its place. # Unit chart (/docs/components/unit-chart) } minHeight={560}> ## Things to try [#things-to-try] - **Regroup.** Switch between Plan, Region and Channel. Each dot is a customer, and it moves from its old column to its new one, so you can see where people went. - **Read the numbers.** Each column's count rolls to its new value. ## Install [#install] ## Dependencies [#dependencies] The install command adds these for you. If you copy the code by hand, install them yourself. ## How it works [#how-it-works] - **Dot `i` is always customer `i`.** Every grouping lays out the same list, and `pairing="index"` carries each customer to their new place. - **Equal columns.** Each group gets an equal share of the width and stacks from the bottom, plans in bands. The labels use the same grid, so they line up. - **A fixed frame.** Four unseen corner dots keep the bounds still, so columns grow in place instead of the chart rescaling. The customers are made up. Swap in your own rows: any record with a few categories works. # Usage meter (/docs/components/usage-meter) } minHeight={680}> ## Things to try [#things-to-try] - **Watch the month go by.** The bars fill, turn amber when you're running low and red once you're over. - **Look at the bill.** The overage slides into the projected bill. - **Upgrade.** The new limits make every bar spring back down while the warnings fold away. - **Point at the ⓘ beside a meter** to see what it counts and what going over costs. ## Install [#install] ## Dependencies [#dependencies] The install command adds these for you. If you copy the code by hand, install them yourself. ## How it works [#how-it-works] - Each bar is Base UI's Meter, so it carries the right role and values for assistive tech; the details are Base UI's Tooltip. - The fills, colours and folding warnings are Motion, and the numbers roll with a small odometer. The plans and prices are made up. # Voice assistant (/docs/components/voice-assistant) } minHeight={520}> ## Things to try [#things-to-try] - **Watch it play.** The orb breathes while it waits, ripples while you speak and your words appear under it, swirls into a spinning ring while it thinks, then pulses in lobes as it answers. - **Press the mic.** It stops playing and starts listening; press again and it goes off to think. ## Install [#install] ## Dependencies [#dependencies] The install command adds these for you. If you copy the code by hand, install them yourself. ## How it works [#how-it-works] - **One orb, four shapes.** Each state is a shape from `shapes` in `@remold-ui/core`: a `sphere` that breathes with `.scale`, the same sphere rippled or bulged with `.warp`, and a `torus` for thinking. `` turns it with a `camera` and flows between them. - **Your voice, if you have it.** Pass `level` (0 to 1) from the microphone or the reply's audio, and the ripples follow it. Left out, the orb makes up a lively one. - **Dots that stay put.** Ripples use `pairing="index"`, so each dot keeps its place on the ball and only moves in and out. Going to or from the ring uses `pairing="flow"`, so the ball unrolls into it in one sweep. - **Base UI for the mic.** The mic is a Base UI `Toggle`, pressed while listening, and the status reads out to screen readers as it changes. It holds still with reduced motion, and offscreen. The question and the answer are made up. # Voice orb (/docs/components/voice-orb) }> Tap the orb to translate your own voice between Spanish and English. ## Things to try [#things-to-try] - **Tap the orb and speak.** It goes through each state in turn: *listening* (the microphone is open and the orb waits), *hearing you* (it moves like a voice as your words appear), *translating* (the clouds gather while the last words are finished) and *speaking* (the translation is read aloud). Tap again to finish early or stop it talking. - **Watch the translation correct itself.** Mid-sentence, "la estación" can read as *season*; once the sentence ends it becomes *train station*. `Remold.Text` changes only the words that changed, so the line settles instead of flickering. - **Swap languages** with the arrows between them. Live mode uses [react-speech-recognition](https://www.npmjs.com/package/react-speech-recognition) for captions, the on-device [Translator API](https://developer.chrome.com/docs/ai/translator-api) in recent Chrome, and the browser's speech synthesis. Where translation isn't available it still captions what you said, and says so. ## The orb on its own [#the-orb-on-its-own] ```tsx // Feed a 0–1 level from your own audio or agent: ``` Without a `level`, it simulates a believable voice while listening or speaking. `state` is `idle | listening | thinking | speaking`; `theme` is `aurora | ember | mono`; `size` sets the square size. ## Install [#install] ## Dependencies [#dependencies] ## How it works [#how-it-works] - The sphere is a clipped circle filled with a sky gradient, a few soft cloud blobs drifting on their own orbits, a deep current along the bottom, a highlight on the top left and a gentle shade at the edge. No outline, no rings. - It's a [`Remold.Canvas`](/docs/primitives/canvas). Each state is a set of `values` (size, drift speed, breathing, how tightly the clouds gather), so the orb springs from wherever it is into the next state. The canvas handles sizing, the frame loop and reduced motion; the component is the paint function. - The voice level, from your `level` prop or the simulation, rises fast and falls slowly, so the orb leaps to a word and eases back. It also speeds the clouds up. - It's `continuous`, so it keeps drifting between state changes. It pauses off screen, and reduced motion holds it still. The orb never touches the microphone. Live translation hands your voice to the browser's speech recognition, which in Chrome is processed by Google's servers; translation itself runs on the device. The component sends nothing anywhere itself. # Wallet (/docs/components/wallet-stack) } minHeight={620}> ## Things to try [#things-to-try] - **Tap the holder.** The cards slide up out of the front pocket, fanned so you can read each one, still tucked in at the bottom. Tap again and they slip back in. - **Choose a card.** It rises to the top while the holder sinks out of the way, and its latest transactions arrive row by row in the space between. - **Make it your default.** Flip the switch and the Default badge moves over to it. ## Install [#install] ## Dependencies [#dependencies] The install command adds these for you. If you copy the code by hand, install them yourself. ## How it works [#how-it-works] - **Springs, a beat apart.** Every card and the holder are placed by Motion springs, staggered slightly, so each change reads as one movement rather than a jumble. - **Drawn leather.** The holder is SVG: a pebbled grain from a noise filter, a burnished edge and close stitching. Nothing is an image. - The default-card switch is Base UI's Switch. The cards and transactions are made up. # Accessibility (/docs/concepts/accessibility) ## Reduced motion [#reduced-motion] By default every part follows the system's reduced-motion setting: changes happen at once, with no movement. Override it with `reducedMotion`, a prop on every part and an option on `transition()` in plain JavaScript. | Value | Behaviour | | ---------- | ------------------------------------------------------------ | | `"user"` | Follow the system setting. The default. | | `"always"` | Never animate movement, for example behind your own setting. | | `"never"` | Always animate. Use sparingly. | ## Announcements [#announcements] Pass `announce` to have a short summary of each change read to screen-reader users, such as "2 moved, 1 added", or a function that writes your own message. ```tsx tab="React" tab-group="lang" `Sorted. ${summary.moved} files moved.`}> ``` ```js tab="JavaScript" transition(list, update, { announce: (summary) => `Sorted. ${summary.moved} files moved.` }); ``` ## Focus [#focus] If the focused element is replaced by its new version, focus moves to the new one, so keyboard users don't lose their place. Text morphs keep the real text available to screen readers while the letters move. ## Canvas [#canvas] Scenes and particles take a `label`, which becomes the canvas's accessible name. Scenes also describe their objects and can be explored with the keyboard. # How matching works (/docs/concepts/matching) ## The four steps [#the-four-steps] 1. **Read the page.** Before the update, every element that has its own text, an image or a visible box is measured: its kind, text, media, size, position and colour. After the update, the same is read again. 2. **Score every pair.** Each old element is compared with each new one. Same text scores high, a similar image or size scores a little, and an element whose parent was already paired gets a bonus. 3. **Pick partners.** The best overall pairing is chosen at once (with the Hungarian algorithm), so one strong pair is given up when that lets two others do better. Weak pairs are left unmatched rather than guessed. 4. **Move.** Paired elements spring from their old box to their new one. Unmatched new elements fade in; unmatched old ones fade out from where they were. Text that splits into several elements, or several that merge into one ("3 items" becoming "3" and "items"), is detected too. ## Steering it [#steering-it] Pass `match` to [Morph](/docs/primitives/morph), `useRemold` or `transition()`. ## Leaving things out [#leaving-things-out] Wrap anything that shouldn't move, such as a video or a live chart, in [Ignore](/docs/primitives/ignore), or add a `data-rm-ignore` attribute. It and everything inside it is skipped. ```tsx tab="React" tab-group="lang" ``` ```html tab="JavaScript"
``` To see what was paired, add a style for `[data-rm-matched]`, `[data-rm-entering]` and `[data-rm-exiting]`. # Springs (/docs/concepts/springs) ## Duration and bounce [#duration-and-bounce] A spring has no fixed end time, so it can be redirected at any moment and keep its speed. It's described the way SwiftUI describes it: ```tsx tab="React" tab-group="lang" ``` ```js tab="JavaScript" transition(list, update, { spring: { duration: 0.6, bounce: 0.2 } }); ``` ## Presets [#presets] | Preset | Value | Suits | | ---------------- | --------------------------------- | --------------------------------- | | `springs.smooth` | `{ duration: 0.5, bounce: 0 }` | Continuous and calm changes | | `springs.snappy` | `{ duration: 0.5, bounce: 0.15 }` | Morph changes, controls and text | | `springs.bouncy` | `{ duration: 0.5, bounce: 0.3 }` | Playful moments | ```ts tab="React" tab-group="lang" import { springs } from "@remold-ui/react"; ``` ```js tab="JavaScript" import { springs } from "@remold-ui/core"; ``` ## Interruptions [#interruptions] When something changes again mid-animation, nothing restarts. Each element heads for its new place from where it is now, at the speed it was moving, so rapid clicks feel continuous. Motion runs at the same speed on 60 Hz and 120 Hz screens. # Styling (/docs/concepts/styling) ## Class names [#class-names] `className` takes a string, or a function of the part's state. ```tsx tab="React" tab-group="lang" (state.animating ? 'list busy' : 'list')}> ``` ```js tab="JavaScript" list.classList.add("busy"); transition(list, update).finished.finally(() => list.classList.remove("busy")); ``` In plain JavaScript you style the element yourself. The transition's `finished` promise tells you when it has settled. ## Data attributes [#data-attributes] | Attribute | Set on | | ------------------- | -------------------------------------------------------- | | `data-rm-animating` | A part while it's animating. | | `data-rm-matched` | Elements paired with an old version during a transition. | | `data-rm-changed` | Matched elements whose box or text changed. | | `data-rm-entering` | New elements fading in. | | `data-rm-exiting` | Copies of removed elements fading out. | `data-rm-matched`, `data-rm-changed`, `data-rm-entering` and `data-rm-exiting` are set by `transition()` itself, so the same selectors work without React. `data-rm-animating` is set by the React parts. ## Rendering your own element [#rendering-your-own-element] `render` replaces the default element and keeps Remold's props and ref. Pass an element, or a function of the props and state. ```tsx }> ``` In plain JavaScript there is nothing to replace: `transition()` animates whichever element you pass it. # Matching (/docs/core/matching) ## matchNodes() [#matchnodes] Pairs two lists of element descriptions (kind, text, box, colour, parent). This is what [transitions](/docs/concepts/matching) use. ```ts const { pairs, added, removed, splits, merges } = matchNodes(before, after, { threshold: 0.5 }); ``` ## matchPoints() [#matchpoints] Pairs two clouds of points so the total distance travelled is as small as possible. Exact for small clouds; for large ones a fast approximation lands within a few percent of the best. ```ts const { pairs, leaving, arriving } = matchPoints(oldPoints, newPoints, { maxDistance: 80 }); ``` # Motion values (/docs/core/motion) ## value() [#value] ```ts import { value, springs } from "@remold-ui/core"; import { frames } from "@remold-ui/dom"; const x = value(0, { spring: springs.snappy }); x.to(100); frames((dt) => { x.step(dt); element.style.translate = `${x.current}px`; return !x.settled; }); ``` | Member | What it does | | ------------------------------------------ | ---------------------------------------------------------------------- | | `to(target, options?)` | Spring towards a target, keeping position and speed. | | `set(value)` | Jump and stop. | | `hold()` / `release(options?)` | Grab it for a drag, then let go with a velocity. | | `step(dt)` | Advance by `dt` seconds. Returns true while moving. | | `current`, `velocity`, `target`, `settled` | Where it is, how fast, where it's heading, and whether it has stopped. | ## values() [#values] The same, for many numbers in flat typed arrays, so a renderer can read thousands of positions each frame without allocating. ```ts const dots = values(positions, { stride: 2 }); // [x0, y0, x1, y1, …] dots.toAll(next); dots.step(dt); draw(dots.current); ``` `frames()` comes from `@remold-ui/dom`. In tests or a worker, call `step` yourself. # Shapes (/docs/core/shapes) ## createMorph() [#createmorph] ```ts import { createMorph } from "@remold-ui/morph"; const morph = createMorph(fromPath, toPath); path.setAttribute("d", morph.d(0.5)); // halfway ``` Handles different numbers of points, several pieces and holes. `t` can go past 0 and 1, so springs can overshoot. `@remold-ui/morph` only solves the geometry. Drive `t` with Motion, GSAP, WAAPI, or your own renderer. ## Motion for React [#motion-for-react] `@remold-ui/react` includes a small adapter for the common case. Motion owns the spring; Remold turns its progress into path data. ```tsx import { MorphPath } from "@remold-ui/react"; ; ``` ## pointCloud() [#pointcloud] The motion behind [particles()](/docs/canvas/particles), without any drawing: give it new points and read positions, sizes and tones from `data` each frame. Use it with WebGL, SVG or anything else. ```ts import { pointCloud, samplePath } from "@remold-ui/core"; const cloud = pointCloud(samplePath(a, { count: 3000 })); cloud.to(samplePath(b, { count: 3000 })); cloud.step(dt); // then draw cloud.data: [x, y, size, tone, …] ``` # useAutoplay (/docs/hooks/use-autoplay) ## Usage [#usage] ```tsx import { Remold, useAutoplay } from "@remold-ui/react"; function Showcase() { const autoplay = useAutoplay(VIEWS.length, { every: 2000 }); return (
{renderView(VIEWS[autoplay.step])}
); } ``` React only for now. In plain JavaScript, step your own state on a timer and run each change through `transition` from `@remold-ui/dom`. - It only plays while the element with `ref` is on screen, so a page full of demos doesn't run them all at once. - The first click, tap or key press inside the element stops it for good: the person is in charge now. The button with `toggleProps` is the exception, so it can pause and play. - Give the steps different lengths with a function: `every: (step) => (step === last ? 4000 : 2000)`. - Anything that moves by itself for more than five seconds needs a way to pause it (WCAG 2.2.2). `toggleProps` makes any button that control. ## Options [#options] number)", default: "2400", description: <>{"Milliseconds per step, or a function of the step."}, }, initial: { type: "number", default: "0", description: <>{"The first step."} }, stopOnInteraction: { type: "boolean", default: "true", description: <>{"Stop when someone clicks, taps or types inside the element."}, }, autoStart: { type: "boolean", default: "true", description: <>{"Start playing."} }, }} /> ## Returns [#returns] | Field | What it is | | --------------------------- | --------------------------------------------------------- | | `ref` | Put it on the element that holds the demo. | | `step` | The current step, from 0 to `count - 1`. | | `setStep(step)` | Go to a step yourself. | | `next()` | Go to the next step, wrapping round. | | `playing`, `setPlaying(on)` | Whether it's playing. | | `toggleProps` | Spread on a button to make it the play and pause control. | # useDragToDismiss (/docs/hooks/use-drag-to-dismiss) ## Usage [#usage] ```tsx tab="React" tab-group="lang" import { Remold, useDragToDismiss } from "@remold-ui/react"; function CardPage({ card, onClose }) { const drag = useDragToDismiss({ onDismiss: onClose }); return
{/* the card */}
; } // The view swap is a Morph transition, so the card flies home from where the finger let go. {open ? : }; ``` ```js tab="JavaScript" import { dragToDismiss, transition } from "@remold-ui/dom"; const drag = dragToDismiss(card, { onDismiss: () => transition(view, () => view.replaceChildren(renderWallet())), }); // Later, when the card goes away for good: drag.destroy(); ``` Without React, `dragToDismiss` takes the element and returns `reset()` and `destroy()`. There's no `disabled` option: call `destroy()` to turn it off. - It follows the finger along one axis. Pulling the wrong way stretches with resistance instead of moving freely. - Let go past `distance`, or flick faster than `velocity`, and `onDismiss` runs. Otherwise it springs back, starting at the speed of your finger. - After a dismiss the offset stays on the element, so the `Morph` transition that follows flies it from exactly there. - Taps and clicks inside still work: a press only becomes a drag after a few pixels, and a drag never ends in a click. Text fields and anything marked `data-rm-no-drag` never start one. - The page can still scroll along the other axis (it sets `touch-action` for you). - With reduced motion it jumps back instead of springing. ## Options [#options] void", required: true, description: <>{"Called once when it's pulled far enough or flicked."} }, axis: { type: '"x" | "y"', default: '"y"', description: <>{"The axis it moves along."} }, direction: { type: "1 | -1", default: "1", description: <>{"Which way dismisses: 1 is down or right, -1 up or left."} }, distance: { type: "number", default: "110", description: <>{"Pixels past which letting go dismisses."} }, velocity: { type: "number", default: "700", description: <>{"A flick faster than this, in pixels per second, dismisses once it has moved 24 px."} }, resistance: { type: "number", default: "200", description: <>{"How far the wrong way can stretch before it stiffens."} }, spring: { type: "{ duration, bounce }", default: "{ duration: 0.45, bounce: 0.2 }", description: <>{"The spring back."} }, slop: { type: "number", default: "4", description: <>{"Movement before a press becomes a drag."} }, onDrag: { type: "(offset: number) => void", description: <>{"Called on every move, for dimming a backdrop as it goes."} }, disabled: { type: "boolean", default: "false", description: <>{"Turn dragging off."} }, reducedMotion: { type: '"user" | "always" | "never"', default: '"user"', description: <>{"Follow the system setting, or override it."} }, }} /> ## Returns [#returns] ", description: <>{"Put it on the element that can be dragged away."} }, reset: { type: "() => void", description: <>{"Put the element back where it started, at once."} }, }} /> # useDust (/docs/hooks/use-dust) ## Usage [#usage] ```tsx import { useDust } from "@remold-ui/react/canvas"; function Inbox() { const card = useRef(null); const trail = useDust(card, { palette: ["var(--ink)", "var(--muted)", "var(--surface)"] }); async function archive(row: HTMLElement) { setHidden(row.id); // hide it as its dust takes over await trail.away(row, archiveTab.current!); removeRow(row.id); } async function undo(row: HTMLElement) { await trail.back(row, archiveTab.current!); // the same dots pour back out setHidden(null); } return
…
; } ``` In plain JavaScript, `dust(container, options)` from `@remold-ui/canvas` gives you the same `away`, `back` and `destroy`. - Each pour samples the elements as they look (backgrounds, text and images), so every dot keeps its colour, then gathers them into a heap at the target's middle. - The dots are drawn on a canvas laid over the container for the length of the pour, and it's removed once they land. Nothing runs between pours. - Positions are measured in the container's own pixels, so pours line up inside a CSS transform, such as a scaled-down preview. - The palette takes `var(--name)`, read from the container when the pour starts, so dust follows dark mode. Make the last colour the one the elements sit on, so anti-aliased edges vanish into it. ## Options [#options] {"Colours to sort the dots into: any CSS colours, var(--name) included."} }, count: { type: "number", default: "600", description: <>{"Dots per element."} }, radius: { type: "number", default: "1.2", description: <>{"Dot radius, in CSS pixels."} }, heap: { type: "number", default: "6", description: <>{"How far the heap reaches from the target's middle."} }, background: { type: "boolean", default: "false", description: <>{"Whether each element's own background becomes dots too."} }, transition: { type: "ParticleTransition", default: "a short scatter", description: <>{"How the dots travel."} }, spring: { type: "{ duration, bounce }", default: "{ duration: 0.6, bounce: 0.1 }", description: <>{"How the dots move."} }, }} /> ## Returns [#returns] | Field | What it is | | ------------------------ | --------------------------------------------------------------------------------------- | | `away(elements, target)` | Pours one element or several into `target`. Resolves once the dots land. | | `back(elements, source)` | Pours out of `source` and settles into the elements. Show them once it resolves. | # useRemold (/docs/hooks/use-remold) ## Usage [#usage] ```tsx tab="React" tab-group="lang" const { ref, animate } = useRemold();
    {items.map(renderItem)}
``` ```js tab="JavaScript" import { transition } from "@remold-ui/dom"; sortButton.addEventListener("click", () => { transition(list, () => { list.replaceChildren(...byPrice(items).map(renderItem)); }); }); ``` Without React, `transition` takes the element and the change, and returns the same `Transition`. The options below are the same. ## Options [#options] {"How it moves. See "} {"Springs"} {"."} ), }, match: { type: "MatchNodesOptions", description: ( <> {"Steer the matcher. See "} {"How matching works"} {"."} ), }, choreography: { type: "boolean | { move, enter, stagger, maxStagger }", default: "false", description: ( <> {"Play a change in beats: what leaves, then what moves, then what arrives, one after another. See "} {"Choreography"} {"."} ), }, announce: { type: "boolean | ((summary) => string)", default: "false", description: ( <>{"Read a summary of each change to screen-reader users."} ), }, reducedMotion: { type: '"user" | "always" | "never"', default: '"user"', description: ( <> {"Follow the system setting, or override it. See "} {"Accessibility"} {"."} ), }, loop: { type: "FrameLoop", default: "shared loop", description: ( <>{"The frame loop to run on. Pass your own to slow down or test."} ), }, }} /> ## Returns [#returns] void", description: <>{"Attach to the element whose contents change."}, }, animate: { type: "(update, options?) => Transition | null", description: ( <> {"Runs "} {"update"} { " synchronously and animates what it changed. Use it in event handlers, not effects." } ), }, }} /> # useSpringPress (/docs/hooks/use-spring-press) ```tsx tab="React" tab-group="lang" import { useSpringPress } from "@remold-ui/react"; function PlayButton() { const press = useSpringPress({ scale: 0.94, spring: { duration: 0.24, bounce: 0.12 }, }); return ; } ``` ```js tab="JavaScript" import { value } from "@remold-ui/core"; import { frames } from "@remold-ui/dom"; const scale = value(1, { spring: { duration: 0.24, bounce: 0.12 } }); let stop; function pressTo(target) { stop?.(); scale.to(target); stop = frames((dt) => { scale.step(dt); button.style.scale = String(scale.current); return !scale.settled; }); } button.addEventListener("pointerdown", () => pressTo(0.94)); button.addEventListener("pointerup", () => pressTo(1)); button.addEventListener("pointercancel", () => pressTo(1)); ``` There's no plain-JavaScript `springPress` yet. The hook is a spring from `@remold-ui/core` run on the frame loop from `@remold-ui/dom`, so you can build it in a few lines. Check `prefersReducedMotion()` from `@remold-ui/dom` first if it should stay still for people who ask for less motion. The control springs toward `scale` while its primary pointer is down, then returns to `1` when released. It is interruptible: releasing before it settles hands the spring back from its current position. This is deliberately pointer-only. Keyboard activation stays still, preserving immediate, predictable feedback for high-frequency actions. ## Options [#options] | Option | Default | Description | | --------------- | ---------------- | ----------------------------------------------------------------------- | | `scale` | `0.97` | Scale while pressed. Keep this subtle: `0.94`–`0.98` is usually enough. | | `spring` | `springs.snappy` | The `{ duration, bounce }` spring for press and release. | | `reducedMotion` | `"user"` | Follows the system setting; reduced motion lands immediately. | | `loop` | browser | A custom frame loop, useful in tests or embedded renderers. | # useViewTransition (/docs/hooks/use-view-transition) ## Usage [#usage] ```tsx tab="React" tab-group="lang" const { ref, animate } = useViewTransition();
; ``` ```js tab="JavaScript" import { viewTransition } from "@remold-ui/dom"; openButton.addEventListener("click", () => { viewTransition(main, () => { main.innerHTML = renderDetail(item); }); }); ``` Best for large, route-sized changes, which the browser animates off the main thread. View transitions can't be interrupted, so it falls back to [useRemold](/docs/hooks/use-remold) where the API is missing, when reduced motion is on, or for very large pages. ## Options [#options] {"How it moves. See "} {"Springs"} {"."} ), }, match: { type: "MatchNodesOptions", description: ( <> {"Steer the matcher. See "} {"How matching works"} {"."} ), }, announce: { type: "boolean | ((summary) => string)", default: "false", description: <>{"Read a summary of each change to screen-reader users."}, }, reducedMotion: { type: '"user" | "always" | "never"', default: '"user"', description: ( <> {"Follow the system setting, or override it. See "} {"Accessibility"} {"."} ), }, loop: { type: "FrameLoop", default: "shared loop", description: <>{"The frame loop to run on. Pass your own to slow down or test."}, }, }} /> ## Returns [#returns] void", description: <>{"Attach to the element whose contents change."} }, animate: { type: "(update, options?) => Transition | null", description: ( <> {"Runs "} {"update"} {" and animates it."} ), }, }} /> # Introduction (/docs) Remold UI is an open-source library of animated React components, and the engine underneath them. Copy a component into your app, or use the engine to animate changes in your own interface. ## What is this? [#what-is-this] Two things that work together. **Components** for the screens you actually ship: inputs, menus, checkouts, dashboards, chat and more. Each one solves a specific product problem and animates only the change that matters. They're built on [Base UI](https://base-ui.com) for behaviour and accessibility, and animated with [Motion](https://motion.dev) and Remold. **The engine** that animates change. When your interface updates, Remold compares what was on screen with what's next, finds the elements that correspond, and springs each one into its new place. It works across layouts, text, SVG paths and canvas, including thousands of particles in 3D. ## No animation IDs [#no-animation-ids] Most animation tools need you to pair states by hand, with `layoutId` or `view-transition-name`. Remold matches elements by their content, geometry and surroundings instead, so it keeps working when the markup is rebuilt by a server response, another component or generated content. You render your interface normally. ## Any framework [#any-framework] React gets components and hooks from `@remold-ui/react`. Everywhere else, `@remold-ui/dom` animates any element from plain JavaScript, so it works with Vue, Svelte, Astro or a server-rendered page. `@remold-ui/canvas` draws scenes and particles, and `@remold-ui/core` is the matching and spring maths on its own, with no DOM at all. ## You own the code [#you-own-the-code] Components are distributed through a [shadcn registry](/docs/registry). Installing one drops its source and CSS into your project, where you can read it, change it and version it like anything else you wrote. The engine is a normal package you update when you choose. ## Accessible by default [#accessible-by-default] Remold follows the system's reduced-motion setting: changes happen at once instead of animating. Morphing text stays real text for screen readers, and focus stays on the element you were on when it's replaced. ## Next steps [#next-steps] Requirements, the CLI, and installing the engine. Animate a list that sorts and filters, in a few lines. Every component, live, ready to copy. # Installation (/docs/installation) Install any component with the shadcn CLI, or copy the source straight from its page. Either way, the code ends up in your project. To animate your own interface, add the engine as a package. ## Requirements [#requirements] Components target React 18 or later, and TypeScript is recommended, since every file ships as typed source. Each component lists what it depends on (usually `@remold-ui/react`, `motion` and `@base-ui/react`), and the CLI installs them for you. ## Install a component [#install-a-component] Every component has an entry in the registry. Replace the name at the end with any component's. The CLI drops the files into `components/remold/` in your project. If you haven't used shadcn before, run `npx shadcn@latest init` first. ## Install manually [#install-manually] Prefer not to use a CLI? Open the component's page, switch to its code, copy each file into your project, and install the packages listed under Dependencies. ## Install the engine [#install-the-engine] For React: For any other framework, or none: `@remold-ui/canvas` adds scenes and particles, and `@remold-ui/core` is the matching and spring maths on its own, for a Web Worker or the server. In React, the canvas parts come from `@remold-ui/react/canvas`, so an interface that doesn't use them never ships canvas code. ## Use it [#use-it] Wrap the part of your interface that changes and give it a `trigger` that changes with it. ```tsx import { Remold } from "@remold-ui/react"; export function Files({ files, sort }) { return ( {files.map((file) => ( ))} ); } ``` ## Next steps [#next-steps] The common changes, in React and plain JavaScript. What's in the registry, and how its colours follow your theme. Every component, live, ready to copy. # Canvas (/docs/primitives/canvas) The orb is one `draw` function. Each state is a set of `values` (size, drift speed, breathing, how tightly the clouds gather), so switching state springs every number from wherever it is, even mid-change. ## Installation [#installation] Import canvas primitives from `@remold-ui/react/canvas` so canvas rendering stays out of your interface-only entry point. Using plain JavaScript? Install `@remold-ui/canvas` instead, and use [painter()](/docs/canvas/painter). ## Usage [#usage] Write `draw`. Pass the numbers it reads as `values`; change them and they spring there. ```tsx tab="React" tab-group="lang" import { Remold } from '@remold-ui/react/canvas'; { context.beginPath(); context.arc(width / 2, height / 2, values.radius, 0, Math.PI * 2); context.fill(); }} className="h-64 w-full" /> ``` ```js tab="JavaScript" import { painter } from "@remold-ui/canvas"; const orb = painter(canvas, { values: { radius: 40 }, draw(context, { width, height, values }) { context.beginPath(); context.arc(width / 2, height / 2, values.radius, 0, Math.PI * 2); context.fill(); }, }); orb.to({ radius: active ? 80 : 40 }); ``` What you don't write: - **Sizing.** The canvas stores enough pixels to be sharp on every screen (capped at 2×), refits when it resizes, and is cleared and scaled before each frame. Draw in CSS pixels from `(0, 0)` to `(width, height)`. - **The frame loop.** It runs while a value is springing and stops when they settle. Off screen, it pauses. - **Springs.** `values` become springs. A new target starts from the current position and speed, so interrupting is smooth. - **Reduced motion.** Values jump instead of springing, `continuous` drawing stops, and `draw` gets `still: true` so you can paint a calm frame. ## Things that move on their own [#things-that-move-on-their-own] Set `continuous` for drift, breathing, or anything that follows live input like audio. `draw` then runs every frame, with `time` and `dt` in seconds. Keep per-frame state in a ref. ```tsx tab="React" tab-group="lang" { drift.current += values.flow * dt; // paint with time, drift.current and values }} /> ``` ```js tab="JavaScript" let drift = 0; const orb = painter(canvas, { continuous: true, values: LOOKS.idle, spring: { duration: 0.7, bounce: 0 }, draw(context, { width, values, time, dt }) { drift += values.flow * dt; // paint with time, drift and values }, }); orb.to(LOOKS[state]); ``` ## Gestures [#gestures] `actionsRef` gives you `to`, `set`, `values` and `still` for event handlers. In plain JavaScript they're on the object `painter()` returns. Follow a finger with `set`, then hand the flick over with `to` and a `velocity`: ```tsx tab="React" tab-group="lang" const actions = useRef>(null); actions.current?.set({ turn: start + e.clientX - x })} onPointerUp={() => actions.current?.to({ turn: landing }, { velocity: { turn: speed } })} draw={draw} /> ``` ```js tab="JavaScript" const wheel = painter(canvas, { values: { turn: 0 }, spring: { turn: springs.smooth }, draw }); canvas.addEventListener("pointermove", (e) => wheel.set({ turn: start + e.clientX - x })); canvas.addEventListener("pointerup", () => wheel.to({ turn: landing }, { velocity: { turn: speed } })); ``` `spring` takes one spring for every value, or one per name. ## API reference [#api-reference] | Prop | Type | Default | Description | | --------------- | -------------------------------------- | --------- | --------------------------------------------------------------------------- | | `draw` | `(context, frame) => void` | | Paints a frame. `frame` has `width`, `height`, `pixelRatio`, `values`, `time`, `dt`, `still`. | | `values` | `Record` | | Numbers `draw` reads. New targets spring. | | `spring` | `SpringConfig \| Record` | smooth | How values spring. | | `continuous` | `boolean` | `false` | Draw every frame, not only while springing. | | `clear` | `boolean` | `true` | Clear before each frame. Turn off to paint trails. | | `label` | `string` | | Accessible name. Leave it out for a decorative canvas. | | `reducedMotion` | `"user" \| "always" \| "never"` | `"user"` | Follow the system setting, or override it. | | `maxPixelRatio` | `number` | `2` | Cap on stored pixels per CSS pixel. | | `actionsRef` | `Ref` | | `to`, `set`, `redraw`, `values`, `still`. | Every other prop goes to the ``. In plain JavaScript, [painter()](/docs/canvas/painter) takes the same options apart from `actionsRef`, and returns the actions itself. # Connectors (/docs/primitives/connectors) }> ## Installation [#installation] Using plain JavaScript? Install `@remold-ui/dom` instead. ## Usage [#usage] Mark the points lines run between with `Remold.Anchor`: give each a `name`, and `linkTo` the anchor its line comes from. Put a [Morph](/docs/primitives/morph) inside, and the lines follow every element while it moves. ```tsx tab="React" tab-group="lang" import { Remold } from "@remold-ui/react"; {comments.map((comment) => (
} />

{comment.text}

))}
``` ```js tab="JavaScript" import { connectors, transition } from "@remold-ui/dom"; const thread = document.getElementById("thread"); const svg = thread.querySelector("svg.rm-connectors"); connectors(thread, svg, { radius: 12 }); transition(thread, () => renderThread(comments)); ``` Without React, write the attributes yourself: `data-rm-anchor="c2"` and `data-rm-link="c1"` on each avatar. The root needs `position: relative`, and the `` should cover it (`position: absolute; inset: 0`). `connectors()` returns `update()` and `destroy()`. `linkTo` can be `undefined` for anchors with no line coming in, like the top-level comments. `Anchor` renders a `span`; `render` puts the anchor on your own element instead. Under the hood it's two attributes, `data-rm-anchor` and `data-rm-link`, which you can also write yourself. - Lines start under the anchor's middle and end at the left side of the linked element, so they suit trees and threads where children sit below and to the right. - A line that appears draws itself in; a line whose element goes draws itself out. Lines that are there when the page loads are simply there. - With reduced motion, lines still follow the elements, but appear and disappear at once. - Style them with CSS: each line is a `path.rm-connector`. Lines drawing in or out carry `data-rm-drawing`. - Give the linked element `data-rm-line="something"` and its line gets `data-line="something"`: a way to style one line differently, like lighting up the path to a hovered reply. ```css .my-tree .rm-connector { stroke: #cbd5e1; stroke-width: 1.5; } .my-tree .rm-connector[data-line="lit"] { stroke: #a78bfa; filter: drop-shadow(0 0 6px #a78bfa); } ``` ## API reference [#api-reference] {"Elbow goes straight down from the anchor, then turns across with a rounded corner. Curve is one smooth bend."}, }, end: { type: '"side" | "top"', default: '"side"', description: ( <>{"Where a line meets the linked element: the middle of its left side (trees, threads) or of its top (timelines, steps)."} ), }, radius: { type: "number", default: "12", description: <>{"The elbow's corner radius, in pixels."} }, gap: { type: "number", default: "4", description: <>{"Space between each end of a line and the element it touches, in pixels."} }, spring: { type: "{ duration, bounce }", default: "springs.smooth", description: ( <> {"How lines draw in and out. See "} {"Springs"} {"."} ), }, reducedMotion: { type: '"user" | "always" | "never"', default: '"user"', description: <>{"Follow the system setting, or override it."}, }, loop: { type: "FrameLoop", default: "shared loop", description: <>{"The frame loop to run on. Pass your own to slow down or test."}, }, className: { type: "string | ((state) => string)", description: <>{"A class name, or a function of the state."} }, render: { type: "ReactElement | ((props, state) => ReactElement)", description: <>{"Replace the default element."} }, }} /> # Dot grid (/docs/primitives/dot-grid) The grid above is one ``. Each option is a different `shape`, and Shimmer is `ambient`. ## Installation [#installation] Import the dot grid from `@remold-ui/react/canvas` so canvas rendering stays opt-in. Using plain JavaScript? Install `@remold-ui/canvas` instead, and use `dotGrid()`. ## Usage [#usage] Put it behind your content. It follows the pointer over its parent, so it can keep `pointer-events: none`. ```tsx tab="React" tab-group="lang" import { Remold } from '@remold-ui/react/canvas';

Ship your side project this weekend.

``` ```js tab="JavaScript" import { dotGrid } from "@remold-ui/canvas"; const grid = dotGrid(canvas, { shape: { text: "ship it" } }); grid.ripple({ x: 240, y: 120 }); grid.set({ shape: { path: logoPath }, ambient: true }); ``` What you don't write: - **The grid.** Dots fill the canvas at an even spacing and refit when it resizes. - **The pointer.** Dots near it swell, brighten and lean away, with a soft falloff, and it trails the pointer smoothly. Touch doesn't light it up, so scrolling on a phone stays calm. - **Ripples.** A click sends a ring across the grid that pushes the dots as it passes. - **Shapes.** Text or an SVG path lights up in the dots, centred, and fades across when it changes. - **The plumbing.** It only draws while something is moving, pauses off screen, and holds still for reduced motion: no leaning, no ripples and no shimmer. ## Colours [#colours] Pass `colors`, or set them in CSS. | Colour | CSS fallback | Paints | | --- | --- | --- | | `dot` | `color` | Resting dots. A translucent colour keeps the grid quiet. | | `active` | `accent-color` | Dots under the pointer, in a ripple or in the shape. | To fade the grid at the edges, give the canvas a radial `mask-image`. ## Options [#options] | Prop | Default | | | --- | --- | --- | | `gap` | `22` | Space between dots, in pixels. | | `size` | `1.2` | Resting dot radius, in pixels. | | `reach` | `140` | How far the pointer's glow reaches. | | `swell` | `1.4` | How much dots grow under the pointer. `1` doubles them. | | `push` | `6` | How far dots lean away from the pointer. | | `shape` | `null` | `{ text, font?, scale? }` or `{ path, viewBox?, scale? }` to light up. | | `ambient` | `false` | A slow shimmer across the grid. | | `ripples` | `true` | Ripple on click. | | `colors` | | See above. | | `actionsRef` | | Receives `hover(point \| null)` and `ripple(point?)`, for tours and demos. | | `label` | | The canvas's accessible name. Leave it out for a decorative background. | `dotGrid()` takes the same options plus `track` (the element to follow the pointer over), and returns `hover`, `ripple`, `set`, `redraw` and `destroy`. # Globe (/docs/primitives/globe) The globe above is one ``. The arcs are a list of journeys passed as `arcs`, and each look is a different `colors` prop. Nothing else changes. ## Installation [#installation] Import the globe from `@remold-ui/react/canvas` so canvas rendering stays opt-in. Using plain JavaScript? Install `@remold-ui/canvas` instead, and use `globe()`. ## Usage [#usage] Give every event an `id`. Each id it hasn't seen sends out one pulse or flies one arc, so pass your recent events as they are. ```tsx tab="React" tab-group="lang" import { Remold } from '@remold-ui/react/canvas'; ({ id: s.id, lat: s.lat, lon: s.lon }))} arcs={merges.map((m) => ({ id: m.id, from: m.author, to: m.repo }))} markers={offices} focus={city && { lat: city.lat, lon: city.lon, label: city.name }} scale={0.8} className="aspect-square w-full" /> ``` ```js tab="JavaScript" import { globe } from "@remold-ui/canvas"; const earth = globe(canvas, { colors: { sphere: "#0c1633", glow: "#3d6bff", land: "#7d9be0", arc: ["#ff7ac6", "#5ab0ff"] }, markers: offices, scale: 0.8, }); earth.pulse({ lat: 6.5, lon: 3.4 }); earth.arc({ from: { lat: 6.5, lon: 3.4 }, to: { lat: 51.5, lon: -0.1 } }); earth.focus({ lat: 51.5, lon: -0.1, label: "London" }); earth.set({ colors: { arc: "#f97316" } }); ``` What you don't write: - **The Earth.** Continent outlines ship with it and are filled with evenly spread dots. The side facing you is bright and the far side is faint, so it reads as round. - **Turning.** It spins slowly and leans towards you. A drag spins it, a flick coasts and settles back into the slow turn, and `focus` turns to a place and holds there. - **Arcs.** Each one rises off the surface along the shortest route, a bright head draws it out, and its tail follows it home. It goes behind the globe when the route does, and pulses where it lands. - **Pulses and markers.** A pulse rings out, then stays lit for a few seconds. Markers stay lit. Both wait on the far side until they come round. - **The plumbing.** It stays sharp at every size, sleeps while off screen, and holds still for reduced motion: no spin, arcs drawn whole, and pulses as dots without rings. ## Colours [#colours] Pass `colors`, or leave any of them out and set them in CSS. Changing them recolours everything already on the globe, arcs in flight included. | Colour | CSS fallback | Paints | | --- | --- | --- | | `land` | `color` | The land dots. | | `sphere` | `--globe-sphere` | The ocean, as a solid ball. It also hides the far side. | | `glow` | `--globe-glow` | A halo around the edge. Use `scale` to leave room for it. | | `accent` | `accent-color` | Pulses, markers and the focus tag. The tag's text turns dark or light to stay readable. | | `arc` | `--globe-arc` | Arcs. Pass a list and they take turns, like GitHub's pink and blue. | A single arc, pulse or marker can also take its own `color`. ## Options [#options] | Prop | Default | | | --- | --- | --- | | `pulses` | | Recent events, each `{ id, lat, lon, color? }`. | | `arcs` | | Recent journeys, each `{ id, from, to, color?, pulse? }`. Set `pulse: false` to land quietly. | | `markers` | | Places that stay lit, each `{ lat, lon, color?, size? }`. | | `focus` | `null` | `{ lat, lon, label? }` to turn to and hold. | | `colors` | | See above. | | `spin` | `7` | Degrees per second. `0` holds it still. | | `tilt` | `18` | Degrees it leans towards you. | | `scale` | `1` | How much of the canvas the globe fills. | | `dotSize` | `1` | Scales every land dot. | | `start` | `0` | The longitude facing you at first. | | `draggable` | `true` | Let a drag spin it. | | `dots` | `14000` | Dots over the whole sphere; land keeps about a third. | | `label` | | The canvas's accessible name. | `globe()` takes the same options apart from `pulses`, `arcs` and `focus`, and returns `pulse`, `arc`, `focus`, `set` (for colours, markers, spin, tilt, scale and dot size), `redraw` (after a CSS change) and `destroy`. # Icon (/docs/primitives/icon) }> ## Installation [#installation] Using plain JavaScript? Install `@remold-ui/dom` instead. It brings `@remold-ui/core` with it. ## Usage [#usage] Pass the icon you'd normally render, as a component or an element: ```tsx tab="React" tab-group="lang" import { Remold } from "@remold-ui/react"; import { HugeiconsIcon } from "@hugeicons/react"; import { Cancel01Icon, Menu01Icon } from "@hugeicons/core-free-icons"; } className="size-5" /> ``` ```js tab="JavaScript" import { iconToPath } from "@remold-ui/dom"; const menu = iconToPath(document.querySelector("#menu svg")); const close = iconToPath(document.querySelector("#close svg")); ``` It works with any icon set, or your own SVG components and inline ``. Remold reads the icon's shapes (paths, circles, ellipses, rectangles, lines, polylines and polygons) into one path, and takes its viewBox, fill and stroke from the icon. Props you set yourself, such as `strokeWidth`, win. When the next icon has more strokes or fewer, spare strokes grow out of a neighbour or merge into one. On the server, and on the first paint in the browser, the icon is rendered exactly as it is. Remold takes over before the first frame is drawn, so there's no flash. ### Path data [#path-data] If your icons are SVG path data rather than components, pass `path` instead. Several strokes go in one path: ```tsx tab="React" tab-group="lang" ``` ```js tab="JavaScript" import { createMorph, value } from "@remold-ui/core"; import { frames } from "@remold-ui/dom"; const morph = createMorph(path.getAttribute("d"), "M6 6l12 12M18 6 6 18", { padding: "merge" }); const t = value(0); t.to(1); frames((dt) => { const moving = t.step(dt); path.setAttribute("d", morph.d(t.current)); return moving; }); ``` Without React you drive the morph yourself: `createMorph` pairs the two paths and a spring `value` moves between them. Paths from `iconToPath` (above) morph the same way, using their `d`. ### Limits [#limits] - An icon whose SVG uses `transform` can't be read as one path. It's shown as it is, without morphing, and a warning names the element to flatten. - One look per icon: if an icon's shapes are painted differently from each other (a filled dot inside an outline icon), they all take the icon's own look while it morphs. ## Examples [#examples] ### A single toggle [#a-single-toggle] }> ## API reference [#api-reference] {"The icon to show: an element ("} {""} {") or a component ("} {"Heart"} {"). Give this or "} {"path"} {"."} ), }, path: { type: "string", description: ( <> {"The icon as SVG path data instead of an icon component. Several strokes go in one path ("} {'"M4 6h16M4 12h16"'} {")."} ), }, spring: { type: "{ duration, bounce }", default: "springs.snappy", description: ( <> {"How it moves. See "} {"Springs"} {"."} ), }, match: { type: "MorphOptions", default: '{ padding: "merge" }', description: <>{"How pieces are paired. The default suits outline icons."}, }, reducedMotion: { type: '"user" | "always" | "never"', default: '"user"', description: ( <> {"Follow the system setting, or override it. See "} {"Accessibility"} {"."} ), }, loop: { type: "FrameLoop", default: "shared loop", description: <>{"The frame loop to run on. Pass your own to slow down or test."}, }, className: { type: "string | ((state) => string)", description: <>{"A class name, or a function of the state."} }, render: { type: "ReactElement | ((props, state) => ReactElement)", description: <>{"Replace the default element."} }, }} /> ### Data attributes [#data-attributes] | Attribute | Meaning | | ------------------- | ----------------------------------- | | `data-rm-animating` | Present while the icon is morphing. | # Ignore (/docs/primitives/ignore) ## Installation [#installation] Using plain JavaScript? Install `@remold-ui/dom` instead. ## Usage [#usage] ```tsx tab="React" tab-group="lang" ``` ```js tab="JavaScript" import { transition } from "@remold-ui/dom"; chart.setAttribute("data-rm-ignore", ""); transition(dashboard, () => showView(view)); ``` Anything inside is left out of transitions: it isn't measured, paired or moved. Use it for videos, live charts and anything that animates by itself. It renders a `span` with `data-rm-ignore`; you can put that attribute on any element instead. ## API reference [#api-reference] string)", description: <>{"A class name, or a function of the state."} }, render: { type: "ReactElement | ((props, state) => ReactElement)", description: <>{"Replace the default element."} }, }} /> # Overview (/docs/primitives) ## All parts [#all-parts] | Part | What it's for | | ----------------------------------------- | ------------------------------------------------------------------------------- | | [Morph](/docs/primitives/morph) | Morph everything inside it when a value changes: lists, grids, views, routes. | | [Text](/docs/primitives/text) | Change text letter by letter. | | [Icon](/docs/primitives/icon) | Reshape an SVG icon from one state into the next. | | [PathGroup](/docs/primitives/path-group) | Morph a chart or illustration made of several paths. | | [Scene](/docs/primitives/scene) | Spring a canvas diagram between layouts. | | [Story](/docs/primitives/story) | Tell a Scene in chapters with annotations. | | [Particles](/docs/primitives/particles) | Rebuild thousands of dots into any shape. | | [Canvas](/docs/primitives/canvas) | Paint your own canvas; sizing, the frame loop and springs are handled. | | [Connectors](/docs/primitives/connectors) | Lines between elements that stay attached while they move: trees and threads. | | [Ignore](/docs/primitives/ignore) | Leave something out of transitions. | For changes you make from an event handler, see [useRemold](/docs/hooks/use-remold). # Morph (/docs/primitives/morph) }> ## Installation [#installation] Using plain JavaScript? Install `@remold-ui/dom` instead. ## Usage [#usage] ```tsx tab="React" tab-group="lang" import { Remold } from "@remold-ui/react"; {/* anything that changes when `view` does */} ; ``` ```js tab="JavaScript" import { transition } from "@remold-ui/dom"; transition(list, () => { // change the DOM however you like list.innerHTML = render(view); }); ``` `trigger` is any value that changes when the content does: a view, a sort order, a route. Morph reads the page during the render in which it changes and animates after React commits, so it works with any state library. It renders a `div`. Without React, call `transition` with the element and a function that changes it. It reads the page, runs the change, and animates what moved. The options below are the same. When more than one thing can change, pass them as a list. It's compared item by item, like an effect's dependencies, so a new array with the same items doesn't animate: ```tsx tab="React" tab-group="lang" ``` ```js tab="JavaScript" transition(list, () => render({ view, sort, filter })); ``` ## Examples [#examples] ### Different markup for each view [#different-markup-for-each-view] The grid and the list below are rendered by different elements. Remold pairs each person by what they show, not by where they sit in the tree. }> ```tsx tab="React" tab-group="lang" {view === "Grid" ? ( ) : ( )} ``` ```js tab="JavaScript" transition(people, () => { people.innerHTML = view === "Grid" ? peopleGrid(data) : peopleList(data); }); ``` ### Components from other libraries [#components-from-other-libraries] To animate a component you don't render yourself, like a Base UI or MUI part, use `useRemoldMorph`. It takes the same `trigger` and options as `Remold.Morph`, and gives you a ref to put on any component that forwards it to its element. Base UI and MUI components both do. }> ```tsx tab="React" tab-group="lang" import { useRemoldMorph } from "@remold-ui/react"; const { ref, animating } = useRemoldMorph(billing); … ; ``` ```js tab="JavaScript" import { transition } from "@remold-ui/dom"; const card = document.querySelector(".plan-card"); transition(card, () => showPlan("yearly")); ``` Without React there's nothing to forward: pass the library's own element to `transition`. A few things to know when you use it with component libraries: - **Portals aren't included.** Popovers, menus, selects and dialogs usually render into `document.body`, outside the element with the ref, so Remold doesn't see them. - **Turn off the library's own transitions** inside the animated area (MUI's `Collapse` and `Fade`, Base UI's `data-starting-style` styles). Remold skips elements at zero opacity, so an element that's still fading in won't be matched. - **Skip decoration** that isn't content, such as MUI's ripple, by wrapping it in `` or adding `data-rm-ignore`. ### Choreography [#choreography] By default everything moves at once. With `choreography`, a change plays in beats instead: what leaves goes first, then what moves, then what arrives, one after another in reading order. Each part gets out of the way of the next, so the change is easier to follow. ```tsx tab="React" tab-group="lang" ``` ```js tab="JavaScript" transition(results, () => renderResults(), { choreography: true }); ``` Pass an object to change the timing, in seconds: | Option | Default | | | --- | --- | --- | | `move` | `0.06` | How long moves wait once something starts leaving. Only applies when something leaves. | | `enter` | `0.1` | How long arrivals wait after moves start. Only applies when something moves. | | `stagger` | `0.03` | The gap between one arrival and the next. | | `maxStagger` | `0.24` | The longest the whole stagger may take, so a long list still arrives quickly. | The beats overlap, so a choreographed change takes only a little longer than an unchoreographed one, and it can still be interrupted at any point. With reduced motion, changes fade at once. ### Announcing changes [#announcing-changes] ```tsx tab="React" tab-group="lang" `Sorted. ${summary.moved} files moved.`}> ``` ```js tab="JavaScript" transition(files, () => sortFiles(), { announce: (summary) => `Sorted. ${summary.moved} files moved.`, }); ``` ## API reference [#api-reference] { "Animate whenever this changes (compared with Object.is): a view, a sort order, a route." } ), }, disabled: { type: "boolean", default: "false", description: <>{"Leave changes unanimated."}, }, onTransition: { type: "(transition) => void", description: ( <> {"Called as each transition starts. Await "} {"ready"} {" or "} {"finished"} {", or call "} {"skip()"} {"."} ), }, spring: { type: "{ duration, bounce }", default: "springs.snappy", description: ( <> {"How it moves. See "} {"Springs"} {"."} ), }, match: { type: "MatchNodesOptions", description: ( <> {"Steer the matcher. See "} {"How matching works"} {"."} ), }, lift: { type: "boolean", default: "true", description: ( <> { "Raise elements above their neighbours while they travel, farthest on top, so a card flying across the layout never slides under what it passes. Put back when they land." } ), }, choreography: { type: "boolean | { move, enter, stagger, maxStagger }", default: "false", description: ( <> {"Play a change in beats: what leaves, then what moves, then what arrives, one after another. See "} {"Choreography"} {"."} ), }, announce: { type: "boolean | ((summary) => string)", default: "false", description: ( <>{"Read a summary of each change to screen-reader users."} ), }, reducedMotion: { type: '"user" | "always" | "never"', default: '"user"', description: ( <> {"Follow the system setting, or override it. See "} {"Accessibility"} {"."} ), }, loop: { type: "FrameLoop", default: "shared loop", description: ( <>{"The frame loop to run on. Pass your own to slow down or test."} ), }, className: { type: "string | ((state) => string)", description: <>{"A class name, or a function of the state."}, }, render: { type: "ReactElement | ((props, state) => ReactElement)", description: <>{"Replace the default element."}, }, }} /> ### Data attributes [#data-attributes] | Attribute | Meaning | | ------------------- | -------------------------------------- | | `data-rm-animating` | Present while a transition is running. | # Particles (/docs/primitives/particles) }> Choose **Image** in the demo to see the field jacket photograph become an olive-and-brass halftone, then switch back to text or an SVG shape to watch the dots reform. A dark-pixel mask separates the jacket from its light backdrop. The demo scales its dot count with the preview size, up to 9,000 on wide screens. The photograph is a local demo asset, not part of the package. ## Installation [#installation] Import particles from `@remold-ui/react/canvas` so canvas rendering stays opt-in. Using plain JavaScript? Install `@remold-ui/canvas` instead. ## Usage [#usage] Give `Remold.Particles` a shape, and change it to morph. Each dot heads for its nearest place in the new shape; spare dots shrink away and new ones grow in. Dots near the pointer lean away. ```tsx tab="React" tab-group="lang" import { Remold } from '@remold-ui/react/canvas'; ``` ```js tab="JavaScript" import { particles, sampleImagePoints, sampleTextPoints } from "@remold-ui/canvas"; import { samplePath } from "@remold-ui/core"; const dots = particles(canvas, { points: sampleTextPoints("Just landed", { count: 1200 }) }); dots.to(samplePath(HEART, { count: 1200 })); const logo = new Image(); logo.src = "/logo.png"; await logo.decode(); dots.to(sampleImagePoints(logo, { by: "dark", count: 1200 })); dots.to(myPoints); ``` In plain JavaScript you sample each shape into points yourself and pass them to `to`. An image must have loaded before it's sampled. | `shape` | Dots fill | | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `"words"` or `{ text, font }` | Text in a CSS font. Load web fonts first. | | `{ path, fillRule, mode }` | An SVG path's inside, or its outline with `mode: "outline"`. | | `{ image, by, threshold, pattern, jitter, details }` | An image's opaque, `dark`, `light`, or saturated pixels. Use `pattern: "grid"` and `jitter: 0` for regular halftone dots; `details` adds small features the mask omits. | | `{ points }` | Your own points. A new array is a new shape. | `count` sets how many dots (default 1200). Keep it the same for every shape and no dots need to arrive or leave: each one just travels. `pairing="index"` sends the first point to the first point, and so on, for shapes from the same generator (equaliser bars, a waveform). ### Scatter between shapes [#scatter-between-shapes] By default each dot springs straight to its new place. `transition="scatter"` gives the change a middle instead: the shape dissolves into a loose, glittering cloud, holds there a moment, then gathers into the next one, each dot setting off at a slightly different time. Try **Scatter** and **Direct** in the demo above. ```tsx tab="React" tab-group="lang" ``` ```js tab="JavaScript" dots.to(next, { transition: "scatter" }); dots.to(next, { transition: { type: "scatter", spread: 1.6, hold: 0.5, stagger: 0.4 } }); ``` `spread` is how wide the cloud gets, `hold` how long it stays spread, and `stagger` how unevenly the dots set off again. See [particles()](/docs/canvas/particles#scatter) for each one. With reduced motion the dots go straight to the new shape. ### 3D shapes [#3d-shapes] Build a shape from `shapes` in `@remold-ui/core` and pass it straight in. Give it a `camera` and it turns, with far dots drawn smaller. Shapes are built from a few primitives, moved and coloured with chained methods, and combined with `compose`: ```tsx tab="React" tab-group="lang" import { shapes } from '@remold-ui/core'; import { Remold } from '@remold-ui/react/canvas'; const galaxy = shapes.spiral({ arms: 3 }).tone(({ group }) => (group === 3 ? 'core' : 'arm')); const planet = shapes.compose([ { shape: shapes.sphere().tone('rock') }, { shape: shapes.oval({ radii: [1.8, 1.8], edge: 1 }).rotate({ x: 75 }).tone('ice') }, ]); const vase = shapes.revolve({ profile: [[0.3, -1], [0.7, -0.4], [0.35, 0.5], [0.5, 1]] }); const flower = shapes.oval({ radii: [0.5, 0.18] }).at([0.5, 0, 0]).repeat({ copies: 6, around: 'y' }); const logo = shapes.fromPath(LOGO_PATH, { depth: 0.4 }); ``` ```js tab="JavaScript" import { shapes } from "@remold-ui/core"; import { particles } from "@remold-ui/canvas"; const dots = particles(canvas, { points: galaxy.points(3000), palette, camera: true, blend: "add" }); dots.to(planet.points(3000), { pairing: "flow" }); ``` | Shape | What it is | | --------------------------------------------------------- | ------------------------------------------------------------------ | | `sphere({ radius, solid })` | A ball's surface, or through its volume with `solid`. | | `ellipsoid({ radii, solid })` | A ball stretched along x, y and z. | | `oval({ radii, edge })` | A flat oval, lined at its edge: wings, rings, leaves. | | `curve({ through })` | A smooth curve through your points: stems, antennae, paths. | | `torus({ radius, tube })` | A ring-shaped tube, lying flat. | | `revolve({ profile })` | A side view spun round the y axis: vases, bottles, bells, lamps. | | `fromPath(svgPath, { depth })` | Any SVG path made solid: an icon, a logo, a drawing. | | `fromPixels(pixels, { depth, bulge, colors, by })` | A photo as a relief in its own colours, found for you (`.palette`). | | `spiral({ arms, twist, core })` | A spiral galaxy, lying flat. | | `helix({ strands, turns, radius, height, rungs })` | Twisting strands, with rungs between them. | | `attractor("lorenz")` | The Lorenz attractor's two wings, as one long path. | | `compose([{ shape, share? }, …])` | One shape from several, drawn equally densely, or weighted by `share`. | Every shape has `.at([x, y, z])`, `.scale(by)`, `.rotate({ x, y, z })` in degrees, `.mirror(axis)`, `.tone(name or rule)`, `.stripes({ every, tones })`, `.fuzz(amount)`, `.warp(point => [x, y, z])` for ripples and bulges, and `.repeat({ copies, around, step })` for petals, teeth and stacks, each returning a new shape. `compose` shares the dots by each part's `area`, so a big wing and a small eye come out equally dense; a `share` weights a part by hand. A tone rule sees each point's position, how far `along` its shape it is, and its `group` (an arm, a strand, a part of a composition). Shapes are built with y pointing up and sampled the same way every time, so one shape can be 800 dots on a phone and 4000 on a desktop. - **`pairing="flow"`** pairs dots by where they sit along each shape (out from a spiral's core, down a helix, from one end of a composition to the other), so a change sweeps through as one movement instead of swarming. - **A named `palette`** lets points name their colour. Tones still spring, so a dot changes colour on the way. - **`camera`** takes `true` for the default, or `{ spin, tilt, distance, sway }`. Change `spin` to `0` to pause it where it is. `sway` swings it back and forth that many degrees instead of turning all the way round, which suits flat things like photos. ### Photos in 3D [#photos-in-3d] Give an image shape a `depth` (and a `bulge` to dome it) and it becomes a relief of dots in the image's own colours. `Remold.Particles` loads it, finds its main colours and uses them as the palette, unless you pass one. ```tsx tab="React" tab-group="lang" ``` ```js tab="JavaScript" import { loadPixels, particles } from "@remold-ui/canvas"; import { shapes } from "@remold-ui/core"; const photo = shapes.fromPixels(await loadPixels("/dahlia.png"), { depth: 0.25, bulge: 0.5, colors: 12 }); const dots = particles(canvas, { points: photo.points(5000), palette: photo.palette, camera: { sway: 28 } }); ``` A photo from another site needs to allow it (CORS), or the browser won't let its pixels be read. In plain JavaScript, `dots.setPalette(next.palette)` before `dots.to(next.points(5000))` swaps to the next photo's colours. Image `details` use normalized source-image coordinates from `0` to `1` and count toward `count`. A `tone` of `1` uses the last color in the palette, useful for eye reflections or other tiny highlights. Shapes are sampled in the browser. On the server the canvas renders empty and fills in on load. ## API reference [#api-reference] See [particles()](/docs/canvas/particles) for every option. The React part takes the same ones as props, plus `shape`, `count`, `label`, `pairing` and `transition`. # PathGroup (/docs/primitives/path-group) }> ## Installation [#installation] PathGroup is React only for now. Without React, morph each part with `createMorph` from `@remold-ui/core`, as in the [Icon](/docs/primitives/icon) page's JavaScript tab. ## Usage [#usage] ```tsx ``` Each part is matched by its `id` and morphs from wherever it is when its path changes, even into a shape with a different number of pieces: angry brows lift into arches, a frown turns into a smile and opens into a laugh, and two round eyes become two hearts. Parts are drawn in the order you list them, so the eyes and mouth stay in front of the face. Style the parts with `className` or SVG attributes. Colours aren't morphed by Remold; a CSS `transition` on `fill` and `stroke` keeps them in step, as in the example. ## API reference [#api-reference] {"The parts, back to front: an id, the path data, and any SVG attributes."}, }, viewBox: { type: "string", default: '"0 0 24 24"', description: <>{"Shared by every part."} }, spring: { type: "{ duration, bounce }", default: "springs.snappy", description: ( <> {"How it moves. See "} {"Springs"} {"."} ), }, match: { type: "MorphOptions", description: <>{"How pieces within each part are paired."} }, reducedMotion: { type: '"user" | "always" | "never"', default: '"user"', description: ( <> {"Follow the system setting, or override it. See "} {"Accessibility"} {"."} ), }, loop: { type: "FrameLoop", default: "shared loop", description: <>{"The frame loop to run on. Pass your own to slow down or test."}, }, className: { type: "string | ((state) => string)", description: <>{"A class name, or a function of the state."} }, render: { type: "ReactElement | ((props, state) => ReactElement)", description: <>{"Replace the default element."} }, }} /> ### Data attributes [#data-attributes] | Attribute | Meaning | | --------------- | -------------------- | | `data-rm-paths` | The number of parts. | # Scene (/docs/primitives/scene) }> ## Installation [#installation] Import scenes from `@remold-ui/react/canvas` so canvas rendering stays opt-in. Using plain JavaScript? Install `@remold-ui/canvas` instead. ## Usage [#usage] ```tsx tab="React" tab-group="lang" import { Remold } from "@remold-ui/react/canvas"; ``` ```js tab="JavaScript" import { scene } from "@remold-ui/canvas"; const diagram = scene(canvas, { label: "Our architecture", objects, layouts, initial: "simple" }); diagram.to(layout); ``` Describe the objects once and each layout as plain data: where every object sits, and the camera. Changing `layout` (or calling `to` with a layout's name) springs every object, link and the camera to the new arrangement. People can drag to turn the scene, tap an object to focus it, or tab through the objects with a screen reader. ## API reference [#api-reference] ", required: true, description: <>{"Every object: its look, label and description."} }, layouts: { type: "Record", required: true, description: <>{"Named arrangements: where each object sits, and the camera."}, }, links: { type: "SceneLink[]", description: <>{"Lines between objects, optionally with flowing dots."} }, layout: { type: "string", description: <>{"The layout to show (controlled)."} }, defaultLayout: { type: "string", default: "first layout", description: <>{"The first layout when not controlled."} }, focused: { type: "string | null", description: <>{"The focused object (controlled)."} }, defaultFocused: { type: "string | null", description: <>{"The first focused object when not controlled."} }, onFocusedChange: { type: "(id: string | null) => void", description: <>{"Called when the user focuses an object or clears focus."} }, label: { type: "string", description: <>{"The canvas's accessible name."} }, palette: { type: "PaletteName | Palette", description: <>{"Colours for every object."} }, interact: { type: "{ hover, tap, drag, keyboard }", default: "all true", description: <>{"Which interactions are on."} }, spring: { type: "{ duration, bounce }", default: "{ duration: 0.8, bounce: 0.18 }", description: ( <> {"How it moves. See "} {"Springs"} {"."} ), }, actionsRef: { type: "RefObject", description: ( <> {"Extra controls: "} {"points"} {", "} {"pick"} {", "} {"anchor"} {", "} {"box"} {" and "} {"redraw"} {"."} ), }, reducedMotion: { type: '"user" | "always" | "never"', default: '"user"', description: ( <> {"Follow the system setting, or override it. See "} {"Accessibility"} {"."} ), }, loop: { type: "FrameLoop", default: "shared loop", description: <>{"The frame loop to run on. Pass your own to slow down or test."}, }, className: { type: "string | ((state) => string)", description: <>{"A class name, or a function of the state."} }, render: { type: "ReactElement | ((props, state) => ReactElement)", description: <>{"Replace the default element."} }, }} /> ### Data attributes [#data-attributes] | Attribute | Meaning | | ----------------- | --------------------------- | | `data-rm-layout` | The layout on screen. | | `data-rm-focused` | The focused object, if any. | # Story (/docs/primitives/story) }> ## Installation [#installation] Import stories from `@remold-ui/react/canvas` so canvas rendering stays opt-in. Story is React only for now. The canvas underneath is [scene()](/docs/canvas/scene) from `@remold-ui/canvas`, which you can drive from plain JavaScript. ## Usage [#usage] A Story is a [Scene](/docs/primitives/scene) told in chapters. Each chapter picks a layout, an object to focus and an annotation that follows the object around the canvas. ## API reference [#api-reference] {"In order: an id, a layout, an optional focused object, title, description and annotation."}, }, chapter: { type: "string", description: <>{"The current chapter (controlled)."} }, defaultChapter: { type: "string", default: "first chapter", description: <>{"The first chapter when not controlled."} }, onChapterChange: { type: "(id: string) => void", description: <>{"Called when a chapter is requested."} }, renderAnnotation: { type: "(state) => ReactNode", description: <>{"Your own annotation markup."} }, actionsRef: { type: "RefObject", description: ( <> {"go(id)"} {", "} {"anchor"} {" and "} {"redraw"} {"."} ), }, className: { type: "string | ((state) => string)", description: <>{"A class name, or a function of the state."} }, render: { type: "ReactElement | ((props, state) => ReactElement)", description: <>{"Replace the default element."} }, }} /> # Text (/docs/primitives/text) }> ## Installation [#installation] Using plain JavaScript? Install `@remold-ui/dom` instead. ## Usage [#usage] ```tsx tab="React" tab-group="lang" {status} ``` ```js tab="JavaScript" import { morphText } from "@remold-ui/dom"; morphText(label, status); ``` Letters the old and new text share stay on screen and slide into place; the rest fade out and in. It renders a `span`, and screen readers get the whole new text at once. ## Examples [#examples] ### Status line [#status-line] }> ## API reference [#api-reference] {"The text."} }, spring: { type: "{ duration, bounce }", default: "springs.snappy", description: ( <> {"How it moves. See "} {"Springs"} {"."} ), }, reducedMotion: { type: '"user" | "always" | "never"', default: '"user"', description: ( <> {"Follow the system setting, or override it. See "} {"Accessibility"} {"."} ), }, loop: { type: "FrameLoop", default: "shared loop", description: <>{"The frame loop to run on. Pass your own to slow down or test."}, }, className: { type: "string | ((state) => string)", description: <>{"A class name, or a function of the state."} }, render: { type: "ReactElement | ((props, state) => ReactElement)", description: <>{"Replace the default element."} }, }} /> ### Data attributes [#data-attributes] | Attribute | Meaning | | ------------------- | --------------------------------- | | `data-rm-animating` | Present while letters are moving. | # Quick start (/docs/quick-start) ## Wrap the part that changes [#wrap-the-part-that-changes] Put `Remold.Morph` around the content and give it a `trigger`: any value that changes when the content does. Render your content as you normally would. ```tsx tab="React" tab-group="lang" import { Remold } from "@remold-ui/react"; function Files({ files, sort, kind }) { return ( {files.map((file) => ( ))} ); } ``` ```js tab="JavaScript" import { transition } from "@remold-ui/dom"; function show(files, sort, kind) { transition(list, () => { list.replaceChildren(...visible(files, sort, kind).map(renderRow)); }); } ``` React's `key` is still there for React's own reasons. Remold doesn't read it: it would animate the same if the rows were rebuilt from scratch. Without React, `transition` takes the element and a function that changes it: `innerHTML`, a template, or another framework's render. ## Animate from an event handler [#animate-from-an-event-handler] When there's no single value to watch, use `useRemold` and wrap the state change. ```tsx tab="React" tab-group="lang" const { ref, animate } = useRemold();
    {items.map(renderItem)}
``` ```js tab="JavaScript" shuffleButton.addEventListener("click", () => { transition(list, () => { list.replaceChildren(...shuffle(items).map(renderItem)); }); }); ``` ## Change text in place [#change-text-in-place] ```tsx tab="React" tab-group="lang" {status} ``` ```js tab="JavaScript" import { morphText } from "@remold-ui/dom"; morphText(statusLabel, "Shipped"); ``` Letters the old and new text share stay and slide into place; the rest fade in and out. See [Text](/docs/primitives/text). ## Next steps [#next-steps] - [How matching works](/docs/concepts/matching), and how to steer it. - [Springs](/docs/concepts/springs): choosing how things move. - [Accessibility](/docs/concepts/accessibility): reduced motion, announcements and focus. # shadcn registry (/docs/registry) Every example on this site is in a [shadcn](https://ui.shadcn.com/docs/registry) registry. One command copies the component, every file it uses and its CSS into `components/remold/` in your project, and installs the Remold packages it needs. The code is yours from then on: change it however you like. Assistants that use the shadcn MCP server can install them too: ask for "the Remold phone input" and point it at this registry. ## What's in it [#whats-in-it] | Name | What it is | | ---------------------- | ------------------------------------------------------------------- | | `course-outline` | A course outline whose lines follow modules and lessons. | | `comment-thread` | A post and the conversation under it, with live typing and lit reply paths. | | `phone-input` | A phone field that formats as you type, with a searchable country list. | | `files` | A list that sorts and filters. | | `morph` | People as cards or rows. | | `text` | A status label and a price that morph. | | `icons` | Toggle buttons with morphing icons. | | `feedback-face` | A rating face whose parts each morph. | | `scene` | An isometric canvas scene. | | `story` | A scene that steps through a story. | | `particles` | Text and shapes as flowing dots. | | `match-lines` | Draws what Remold paired. | Replace the name at the end of the command with any of these. The full list is at [`/r/registry.json`](/r/registry.json). ## Colours [#colours] The components use your theme's colours when it defines Fumadocs-style variables (`--color-fd-background`, `--color-fd-border` and so on), and fall back to neutral light colours when it doesn't. Change them in the copied CSS.