App Shells & Panels

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.en

New project or installation trouble? Start here →

Slide 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.

300
Active edited usageDefault props
<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.json

Or, 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

PropTypeDefaultDescription
childrenReact.ReactNode—Panel content
titleReact.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
collapsedboolean—Controlled collapsed state
defaultCollapsedbooleanfalseUncontrolled default collapsed state
onCollapsedChange(collapsed) => void—Collapsed-state change callback
panelWidthnumber300Width of the panel body (px, excluding the handle)
anchor"container" | "viewport""container"Positioning basis
labelstring—Accessible label; falls back to title when not provided
classNamestring—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 side and collapsed support controlled and uncontrolled modes, and each can be controlled independently.

Accessibility

  • The panel is a role="region" using title or label as its aria-label.
  • The edge handle is a native button: Enter / Space toggle 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 carries aria-hidden and interaction is disabled.
  • The panel is non-modal: it does not lock scrolling and does not intercept interaction with the content beneath (pointer-events only 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.