WebberUI

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.

Loading preview…
npx shadcn@latest add https://webberui.com/r/widget-board.json

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

4
<WidgetBoard />

Installation

npx shadcn@latest add https://webberui.com/r/widget-board.json

Installing 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

PropTypeDefaultDescription
editingbooleanControlled edit-mode state; when omitted, the component manages it internally
onEditingChange(editing: boolean) => voidFires when edit mode changes (including when clicking empty grid space or pressing Esc ends editing)
childrenReact.ReactNodePut Widget / WidgetStack children here directly
classNamestringAppended to the board container's className (the container is also the container-query reference, so avoid adding padding here)

Widget

PropTypeDefaultDescription
idstring— (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
labelstringidDisplay name: used for the remove button's aria-label and for the "removed" tray chip
childrenReact.ReactNodeCard content: no padding by default, so you can go full-bleed with an image or add padding yourself
classNamestringAppended to a single widget's frame className

WidgetStack

PropTypeDefaultDescription
idstring— (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
intervalnumber4Interval between automatic page turns (seconds)
labelstringidDisplay name: used for the remove button's aria-label and for the "removed" tray chip
childrenReact.ReactNodeSeveral content nodes; each direct child is one page
classNamestringAppended 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-dense lets the smaller widgets that follow fill in the gaps that md/lg leave behind
  • Wiggle: in edit mode, CSS keyframes loop rotate ±1.2deg infinitely; each widget hashes its id into a negative animation-delay to stagger the phase, so the board does not wobble in unison. The phase comes from a hash rather than Math.random(), so SSR and client output match
  • Layer separation: the outer motion.div handles 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 button whose aria-label is "remove" plus the label); clicking it has AnimatePresence popLayout 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 interval seconds, 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 onEditingChange in 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-blur adapts to light and dark mode, and the effect looks best over a colored "wallpaper" background

On this page