Control Center Panel
A grid of glass control modules; long-press any tile and it springs open in place into a fine-grained control card (slider, segmented toggle) while the background blurs and recedes, springing back on release.
npx shadcn@latest add https://webberui.com/r/control-center-panel.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<ControlCenterPanel />
Installation
npx shadcn@latest add https://webberui.com/r/control-center-panel.jsonOr, once registries are configured in components.json, install it as @webberui/control-center-panel.
Usage
import {
ControlCenterPanel,
ControlTile,
} from "@/components/ui/control-center-panel";
const [brightness, setBrightness] = React.useState(72);
const [appearance, setAppearance] = React.useState("auto");
<ControlCenterPanel columns={2}>
<ControlTile
id="brightness"
label="Brightness"
icon={<Sun />}
value={brightness}
onValueChange={setBrightness}
formatValue={(v) => `${v}%`}
/>
<ControlTile
id="appearance"
label="Appearance"
icon={<Palette />}
control="segmented"
value={appearance}
onValueChange={setAppearance}
options={[
{ label: "Light", value: "light" },
{ label: "Dark", value: "dark" },
{ label: "Auto", value: "auto" },
]}
/>
</ControlCenterPanel>;Props
ControlCenterPanel
| Prop | Type | Default | Description |
|---|---|---|---|
columns | number | 2 | Number of grid columns (2–4) |
value | string | null | — | Controlled mode: the id of the currently expanded tile (null means all collapsed) |
defaultValue | string | null | null | Initially expanded id in uncontrolled mode |
onValueChange | (id: string | null) => void | — | Fires when a tile expands or collapses |
className | string | — | Overrides the panel container's styles |
children | ReactNode | — | Put ControlTile children here |
ControlTile (shared)
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | — | Unique tile identifier; the panel uses it to coordinate "only one expanded at a time" |
label | string | — | Title (such as "Brightness") |
icon | ReactNode | — | Icon in the top-left corner; a lucide-react icon is recommended |
control | 'slider' | 'segmented' | 'slider' | Control type |
onValueChange | (value) => void | — | Fires when the value or selection changes |
className | string | — | Overrides the tile's styles |
ControlTile — control="slider"
| Prop | Type | Default | Description |
|---|---|---|---|
value | number | — | Current value (controlled) |
min | number | 0 | Minimum |
max | number | 100 | Maximum |
step | number | 1 | Step |
formatValue | (v: number) => string | (v) => `${v}` | Formatting for the compact readout and the expanded card |
ControlTile — control="segmented"
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | Currently selected value (controlled) |
options | { label: ReactNode; value: string }[] | — | Segment options, laid out at equal widths |
How it works
- Long-press to expand: the tile only expands after the pointer has been held for about 380ms; while held, the tile shrinks slightly to build tension, and releasing early springs it back into place (without expanding)
- Morphing in place: the tile and the expanded card share a
layoutId, so a decelerating spring grows the tile seamlessly from its grid cell into the fine-grained control card; only one tile is expanded at a time - Background recedes: while expanded, the whole grid is covered by a translucent overlay with
backdrop-blur, creating a layer where the background steps back and focus moves forward - Fine-grained slider: the slider inside the expanded card supports pointer dragging and the keyboard (arrow keys ±
step,PageUp/Down±10%,Home/Endto the ends), with the fill and thumb following on springs - Segmented toggle: equal-width options, with the highlight sliding to a position computed from the index (avoiding nested layout animations), and arrow keys cycling through them
- Collapse: click the background overlay or press
Escto collapse; the expanded card morphs back to its grid cell on the same spring
Accessibility
- Tiles are
role="button"witharia-haspopup="dialog"andaria-expanded;Enter/Spaceexpand directly (no long press needed for keyboard users) - The expanded card is a
role="dialog"witharia-modal; on open, focus moves into the card and the grid getsinertto leave the tab order, and on collapse focus returns to the original tile - The slider is a
role="slider"witharia-valuemin/max/nowandaria-valuetext; the segmented control is arole="radiogroup"/role="radio"marked witharia-checked - When the user has "reduce motion" enabled at the system level: the long-press expansion and every control feature are kept exactly as they are, and only the morph offsets and the tension scaling are removed, replaced by a brief fade in / fade out
Slide Over Panel
A non-modal narrow floating panel that can be dragged and magnetically snapped to either side, and flicked away to collapse into an edge handle — springs tracking the pointer throughout.
Now Playing Sheet
A persistent mini player bar at the bottom; tap it and the artwork scales up as a shared element while the whole layer springs open into a full-screen now-playing page, dismissed by a downward drag whose gesture velocity decides the outcome.