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.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add slide-over-panel.enSlide Over Panel is a non-modal floating side panel for React, built with Tailwind CSS and Motion. You can drag it to the left or right edge and it snaps into place, flick it to collapse into an edge handle, and it never locks scrolling or blocks the content underneath.
Playground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<SlideOverPanel/>
The complete example includes the preview layout and data. Install the component first, then paste this file into your project.
When to use
- Property or layer panels in an editor or design tool, where users work on the canvas while the panel is open.
- Activity, notification, or filter sidebars on a dashboard that users can tuck away at the edge.
- Legends and notes on map or data visualization pages, where users pick which side the panel sits on.
- Supporting content on docs or admin pages, such as an AI assistant chat or a to-do list.
- Not a good fit: flows that need a backdrop and must be finished before the user continues; use Drawer instead.
Installation
npx webberui@latest add https://webberui.com/r/slide-over-panel.en.jsonOr, once registries are configured in components.json, install it as @webberui/slide-over-panel.
Usage
The panel is absolute-positioned, and with anchor="container" (the default) it hugs the nearest positioned ancestor, so make sure the outer container is position: relative:
import { SlideOverPanel } from "@/components/ui/slide-over-panel";
<div className="relative h-[480px] overflow-hidden">
{/* your main content */}
<SlideOverPanel title="Recent activity">
<p>Panel content…</p>
</SlideOverPanel>
</div>To float the panel over the whole viewport, use anchor="viewport" instead (which uses fixed positioning and needs no positioned ancestor).
Examples
Controlled collapse from a toolbar button
Pass collapsed and onCollapsedChange to own the state; the callback also fires when the user drags or flicks the handle, so the state stays in sync.
"use client";
import * as React from "react";
import { SlideOverPanel } from "@/components/ui/slide-over-panel";
export function EditorWithPanel() {
const [collapsed, setCollapsed] = React.useState(false);
return (
<div className="relative h-[560px] overflow-hidden rounded-xl border">
<div className="p-4">
<button onClick={() => setCollapsed((c) => !c)} className="rounded-md border px-3 py-1.5 text-sm">
{collapsed ? "Show properties" : "Hide properties"}
</button>
</div>
<SlideOverPanel
title="Properties"
collapsed={collapsed}
onCollapsedChange={setCollapsed}
>
<dl className="space-y-2 text-sm">
<div className="flex justify-between"><dt>Width</dt><dd>320 px</dd></div>
<div className="flex justify-between"><dt>Height</dt><dd>180 px</dd></div>
</dl>
</SlideOverPanel>
</div>
);
}Remember the side the user picked
In uncontrolled mode, give the initial value with defaultSide and save the user's choice from onSideChange so you can restore it on the next load.
"use client";
import { SlideOverPanel } from "@/components/ui/slide-over-panel";
export function FiltersPanel({ initialSide }: { initialSide: "left" | "right" }) {
return (
<SlideOverPanel
title="Filters"
defaultSide={initialSide}
onSideChange={(side) => localStorage.setItem("filters-panel-side", side)}
panelWidth={260}
>
{/* filter form */}
</SlideOverPanel>
);
}Float over the whole viewport, collapsed at first
anchor="viewport" switches to fixed positioning, so no positioned ancestor is needed; defaultCollapsed shows only the handle on page load.
<SlideOverPanel
anchor="viewport"
defaultCollapsed
label="Help panel"
title="Help"
>
<p className="text-sm">Drag the handle to move the panel to the other side.</p>
</SlideOverPanel>Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | — | Panel content |
title | React.ReactNode | — | Panel heading, which also serves as the source of the accessible label |
side | "left" | "right" | — | Controlled snap side |
defaultSide | "left" | "right" | "right" | Uncontrolled default snap side |
onSideChange | (side) => void | — | Snap-side change callback |
collapsed | boolean | — | Controlled collapsed state |
defaultCollapsed | boolean | false | Uncontrolled default collapsed state |
onCollapsedChange | (collapsed) => void | — | Collapsed-state change callback |
panelWidth | number | 300 | Width of the panel body (px, excluding the handle) |
anchor | "container" | "viewport" | "container" | Positioning basis |
label | string | — | Accessible label; falls back to title when not provided |
className | string | — | Appended to the panel body's className |
How it works
- Magnetic snapping: after a drop, whichever half of the container the panel's center lands in is the side it springs to and expands on.
- Flick to collapse: if the horizontal velocity on release exceeds the threshold (600 px/s), it collapses into an edge handle in the direction of the flick; pulling the handle back out expands it again.
- Tracks the pointer throughout: dragging, spring back, snapping, and collapsing all run on the same spring set, so it settles crisply rather than stiffly.
- Two controlled axes: both
sideandcollapsedsupport controlled and uncontrolled modes, and each can be controlled independently.
Accessibility
- The panel is a
role="region"usingtitleorlabelas itsaria-label. - The edge handle is a native
button:Enter/Spacetoggle collapse, and←/→snap it to the left or right side and expand it, filling the gap where dragging cannot be done from the keyboard. - The handle reflects the expanded state through
aria-expanded; when collapsed, the panel body carriesaria-hiddenand interaction is disabled. - The panel is non-modal: it does not lock scrolling and does not intercept interaction with the content beneath (
pointer-eventsonly apply to the panel itself). - When the user has "reduce motion" enabled at the system level, dragging is disabled and the spring offsets are replaced with instant positioning.
FAQ
Does it work with the Next.js App Router?
Yes. The source already has "use client", so import it from a Server Component page directly. The panel stays transparent in the server render and only appears and slides in once the container width has been measured.
Why isn't the panel where I expect it?
With the default anchor="container", the panel fills the nearest positioned ancestor with absolute inset-0, so the parent needs position: relative and a defined height. Use anchor="viewport" to pin it to the whole window.
Can users still interact with the page while the panel is open?
Yes. The outer stage is pointer-events-none, so only the panel itself receives pointer events, and page scrolling is never locked.
Is dragging the only way to move it?
No. With the edge handle focused, Enter or Space toggles collapse, and ← / → snap the panel to the left or right side and expand it. When reduced motion is on, dragging is disabled and the panel jumps into place instead.
How do I change the width and look?
panelWidth sets the panel body width in px (default 300, not counting the 32px handle), and className is applied to the panel body so you can override its background, border, or shadow.
Zen Mode Shell
A focus-mode shell: one click and the sidebar, top bar, and auxiliary panel fade out and slide away in timeline order while the main content eases back to center and scales up; exiting reverses the choreography and restores the whole shell.
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.