Widget Board
An iOS home-screen widget grid — frosted glass cards in sm/md/lg size tiers, an edit mode where everything wiggles and can be removed, and a Smart Stack that pages on a timer.
npx shadcn@latest add https://webberui.com/r/widget-board.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<WidgetBoard />
Installation
npx shadcn@latest add https://webberui.com/r/widget-board.jsonInstalling also writes the --wb-duration-fast and --wb-ease-out CSS variables into your global stylesheet.
Usage
import { WidgetBoard, Widget, WidgetStack } from "@/components/ui/widget-board";
const [editing, setEditing] = React.useState(false);
<WidgetBoard editing={editing} onEditingChange={setEditing}>
<Widget id="clock" size="sm" label="Clock">
<Clock />
</Widget>
<Widget id="weather" size="md" label="Weather">
<Weather />
</Widget>
<Widget id="music" size="lg" label="Music">
<MusicPlayer />
</Widget>
<WidgetStack id="glance" size="sm" label="Smart Stack" interval={4}>
<CalendarGlance />
<Reminders />
</WidgetStack>
</WidgetBoard>Props
WidgetBoard
| Prop | Type | Default | Description |
|---|---|---|---|
editing | boolean | — | Controlled edit-mode state; when omitted, the component manages it internally |
onEditingChange | (editing: boolean) => void | — | Fires when edit mode changes (including when clicking empty grid space or pressing Esc ends editing) |
children | React.ReactNode | — | Put Widget / WidgetStack children here directly |
className | string | — | Appended to the board container's className (the container is also the container-query reference, so avoid adding padding here) |
Widget
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | — (required) | Unique key: used to pair removal / re-adding and to compute the wiggle phase |
size | 'sm' | 'md' | 'lg' | 'sm' | Size tier: sm 1×1, md 2×1, lg 2×2 |
label | string | id | Display name: used for the remove button's aria-label and for the "removed" tray chip |
children | React.ReactNode | — | Card content: no padding by default, so you can go full-bleed with an image or add padding yourself |
className | string | — | Appended to a single widget's frame className |
WidgetStack
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | — (required) | Unique key: used to pair removal / re-adding and to compute the wiggle phase |
size | 'sm' | 'md' | 'lg' | 'sm' | Size tier: sm 1×1, md 2×1, lg 2×2 |
interval | number | 4 | Interval between automatic page turns (seconds) |
label | string | id | Display name: used for the remove button's aria-label and for the "removed" tray chip |
children | React.ReactNode | — | Several content nodes; each direct child is one page |
className | string | — | Appended to the stack frame's className |
How it works
- Square cells: 4 columns when the container is ≥ 24rem wide and 2 otherwise; the row height is derived from the column width with a container query (
100cqw), so the cells are always 1:1 squares and md and lg map naturally onto iOS's 2×1 and 2×2 proportions.grid-flow-row-denselets the smaller widgets that follow fill in the gaps that md/lg leave behind - Wiggle: in edit mode, CSS keyframes loop
rotate ±1.2deginfinitely; each widget hashes itsidinto a negativeanimation-delayto stagger the phase, so the board does not wobble in unison. The phase comes from a hash rather thanMath.random(), so SSR and client output match - Layer separation: the outer
motion.divhandles the FLIP reflow and the shrink-out exit, the middle layer handles the fly-back animation on re-adding, and the inner layer runs the CSS wiggle, so the three transforms never interfere with each other - Removal and reflow: in edit mode a "−" button floats up in the top-left corner (a native
buttonwhosearia-labelis "remove" plus the label); clicking it hasAnimatePresencepopLayout shrink and fade that widget out in place while the rest reflow into position with a spring FLIP - Removed tray: removed widgets are listed along the bottom of the board and clicking a chip adds one back; at the moment of the click the center distance between the chip and the new cell is measured, and the widget scales up and flies from the tray back into the grid
- Smart Stack: pages slide vertically every
intervalseconds, with small dots along the right edge marking the current page; the timer is cleaned up on unmount, and the rotation pauses automatically in edit mode - Ending edit mode: clicking empty grid space or pressing Esc ends edit mode, notifying the outside through
onEditingChangein controlled mode - Reduce motion: when the user has "reduce motion" enabled at the system level, the wiggle is disabled (belt and braces, through both JS and the CSS
prefers-reduced-motion), the FLIP and the flight are disabled, and the Smart Stack's rotation switches instantly — the functionality is entirely unaffected - Frosted glass cards:
backdrop-bluradapts to light and dark mode, and the effect looks best over a colored "wallpaper" background
Shove Reflow Grid
A shove reflow grid — switching category makes incoming cards squeeze into the lineup, shoving their neighbors aside before they spring back, while outgoing cards get pushed past the edge and tumble away.
Agenda Tracks Grid
A time × track conference agenda grid — the timeline column freezes to the left, session cards reflow into place with FLIP when tracks or speakers are filtered, and clicking a session expands the speaker and summary in place.