- Mirapushed to mainnow
Round totals after tax, not before
checkout/total.ts
−total = round(sub) * rate
+total = round(sub * rate)
538a478+12−4Queued Let coupons work on phones
Live4c2a9f0· KaiShow the delivery date up front
b71e3d8· AnaCache product photos for a day
0e95c47· Theo
Every push ships
Merge to main and it goes out on its own.
Last build11.8s
- Installed 412 packages3.1s
- Compiled 86 routes5.6s
- Optimized 24 images1.9s
- Uploaded the build0.8s
Builtb71e3d810.9s
Built0e95c4712.5s
Follow the build
Each step streams in with its time, so a slow one stands out.
- TypesNo errors
- Unit tests248 passed
- Browser tests36 passed
- Speed score98
Checked before launch
Types, tests and speed. One failure stops it.
4c2a9f0.northwind.dev
Linen shirt$64.00
Arrives Thursday
TotalSPRING10$128.40
Preview this change
Every deploy gets its own link to try first.
Live in 9 regions
Roll out worldwide
From the first region to the last in under a second.
Requests2,624a minute
30s agoNow
Watch traffic switch
Requests move over as the new version warms up.
"use client"
import * as React from "react"
import { RelayBento, RelayTile, useRelay, useRelayLoop } from "@/components/keyframery/relay-bento"
export function Features() {
return (
<RelayBento className="grid-cols-1 md:grid-cols-3 md:grid-rows-[220px_220px]">
<Story />
</RelayBento>
)
}
// Tiles can live anywhere inside the bento. useRelay and useRelayLoop work in any of them.
function Story() {
const relay = useRelay()
const button = React.useRef<HTMLButtonElement>(null)
const [step, setStep] = React.useState("ready")
// Plays while the bento is on screen, then rests and plays again. Leaving the screen aborts the signal.
useRelayLoop(async (signal) => {
setStep("uploading")
await relay.send(button, "scan", { signal }) // resolves when the light lands
setStep("scanning")
await relay.send("scan", ["share", "archive"], { tone: "success", signal })
setStep("done")
}, { rest: 1600 })
return (
<>
<RelayTile id="upload" title="Drop a file" description="It starts the moment it lands." className="md:row-span-2">
<button ref={button}>Upload</button>
</RelayTile>
<RelayTile id="scan" title="Scanned" description="Checked before anyone opens it." className="md:col-span-2" />
<RelayTile id="share" title="Shared" description="A link for everyone who needs it." />
<RelayTile id="archive" title="Kept" description="A copy goes to the archive." />
</>
)
}Installation
npx shadcn add @keyframery/relay-bento
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
| RelayBento | div props + ref | — | The grid. Lay the tiles out with ordinary grid classes (grid-cols-*, grid-rows-*, col-span-*): the light finds its way through whatever gaps there are, at any size, and reroutes when the layout changes. |
| ref | { send } | — | send from outside the bento, if the code that runs the story lives there. |
| RelayTile id | string | — | What send calls the tile. |
| RelayTile title | ReactNode | — | A short name, set under the tile's picture. It also names the tile for screen readers. |
| RelayTile description | ReactNode | — | A line or two under the title. |
| RelayTile side | boolean | false | Sets the title and description beside the picture, on the left, whenever the tile is wide (36rem or more). Good for a wide, short tile like a map. |
| --relay-tile | CSS color | — | The tiles' fill. By default it's mixed from your theme's muted and background colours; set it on the bento, or anything around it, to choose your own. |
| useRelay().send | (from, to, options?) => Promise<void> | — | Runs light from one tile to another, or to several at once, and resolves when it lands. from and to are tile ids, elements inside tiles or refs to them: the light leaves beside the thing that sent it and lands beside the thing that receives it. |
| options.tone | "accent" | "success" | "danger" | "accent" | The light's colour, from --kf-accent, --kf-success or --kf-danger in your theme. Unset, they're blue, green and red. |
| options.dock | boolean | false | The light carries on past the tile's edge into the element it was sent to, and settles there as a dot that rings once. Good for a status dot or an avatar near an edge. |
| options.signal | AbortSignal | — | Stops the light where it is. It fades out, and send resolves at once. |
| options.pace | number | 1 | How fast the light travels: 2 is twice as fast. |
| useRelayLoop | (story: (signal) => Promise<void>, { rest? }) => void | — | Plays a story while the bento is on screen: it runs, rests for rest ms (1600 by default), and runs again. Scrolling away or hiding the tab aborts signal, and the story starts over on return. |
Accessibility and motion
- The light only ever travels through the gaps, so it never covers a word of your content, and it finds its route from where the tiles actually are. Any grid layout works, down to one column on a phone.
- It's drawn on one canvas laid over the grid that ignores the pointer, so every button, link and selection inside a tile works as usual. Nothing is drawn while no light is moving, and nothing plays while the bento is off screen.
- Each tile is a group named by its title. The light is hidden from screen readers, so say what happened in the tile itself, as the demo does with its statuses.
- With reduced motion on, nothing travels: send resolves at once, the receiving tile's edge glows for a moment, and a loop plays its story once and stays where it ends.