Skip to content

Navigation

Floating Panel

A persistent non-modal inspector that can move, resize, overlap, and coexist with peer panels.

When to use it: Use it for inspectors, layers, properties, and other secondary tools people need while continuing to work in the application.

On this page

Example

Loading example…
Floating Panel.tsxtsx
import {
  FloatingPanel,
  FloatingPanelClose,
  FloatingPanelDragHandle,
  FloatingPanelGroup,
  FloatingPanelHeader,
  FloatingPanelResizeHandle,
  FloatingPanelSurface,
  FloatingPanelTitle,
  FloatingPanelTrigger,
} from "@comp0/react";

function MoveGrip() {
  return (
    <svg aria-hidden="true" className="size-4" fill="currentColor" viewBox="0 0 12 18">
      <circle cx="3" cy="3" r="1.25" />
      <circle cx="9" cy="3" r="1.25" />
      <circle cx="3" cy="9" r="1.25" />
      <circle cx="9" cy="9" r="1.25" />
      <circle cx="3" cy="15" r="1.25" />
      <circle cx="9" cy="15" r="1.25" />
    </svg>
  );
}

function ResizeGrip() {
  return (
    <svg aria-hidden="true" className="size-full" viewBox="0 0 24 24">
      <path d="M9 19h10V9M14 19l5-5" fill="none" stroke="currentColor" strokeWidth="1.5" />
    </svg>
  );
}

const triggerClassName =
  "rounded-lg border border-zinc-950/10 px-3 py-2 text-sm font-medium outline-teal-600 hover:bg-zinc-50 focus-visible:outline-2 dark:border-white/10 dark:outline-teal-400 dark:hover:bg-zinc-800";
const surfaceClassName =
  "w-72 min-w-56 min-h-44 overflow-auto rounded-xl border border-zinc-950/10 bg-white shadow-xl outline-teal-600 data-active:outline-2 data-active:outline-teal-500 data-moving:outline-dashed data-resizing:outline-dashed dark:border-white/10 dark:bg-zinc-900 dark:outline-teal-400";

export function Example() {
  return (
    <FloatingPanelGroup
      as="div"
      className="flex min-h-[30rem] flex-col items-start gap-4 rounded-xl border border-dashed border-zinc-950/15 bg-zinc-50 p-5 dark:border-white/15 dark:bg-zinc-950"
    >
      <div>
        <h2 className="font-semibold text-zinc-950 dark:text-white">Workspace</h2>
        <p className="mt-1 max-w-md text-sm/6 text-zinc-600 dark:text-zinc-400">
          Open both inspectors, drag a header or its grip, resize from a corner, and use Tab to
          explore each panel. F6 is an optional shortcut between regions.
        </p>
      </div>
      <div className="flex w-full max-w-xl justify-between gap-2">
        <FloatingPanel defaultOpen>
          <FloatingPanelTrigger className={triggerClassName}>Layers</FloatingPanelTrigger>
          <FloatingPanelSurface placement="bottom start" className={surfaceClassName}>
            <FloatingPanelHeader className="flex cursor-grab items-center gap-2 border-b border-zinc-950/10 px-3 py-2 data-moving:cursor-grabbing dark:border-white/10">
              <FloatingPanelDragHandle className="cursor-grab touch-none rounded p-1 text-zinc-400 outline-teal-600 hover:bg-zinc-100 active:cursor-grabbing focus-visible:outline-2 data-moving:cursor-grabbing dark:outline-teal-400 dark:hover:bg-zinc-800">
                <MoveGrip />
              </FloatingPanelDragHandle>
              <FloatingPanelTitle className="flex-1 text-sm font-semibold text-zinc-950 dark:text-white">
                Layers
              </FloatingPanelTitle>
              <FloatingPanelClose className="rounded px-1.5 py-0.5 text-zinc-500 outline-teal-600 hover:bg-zinc-100 hover:text-zinc-950 focus-visible:outline-2 dark:outline-teal-400 dark:hover:bg-zinc-800 dark:hover:text-white">
                ×
              </FloatingPanelClose>
            </FloatingPanelHeader>
            <div className="space-y-1 p-3 text-sm text-zinc-700 dark:text-zinc-300">
              <button className="block w-full rounded bg-teal-50 px-2 py-1.5 text-left outline-teal-600 focus-visible:outline-2 dark:bg-teal-950 dark:outline-teal-400">
                Header
              </button>
              <button className="block w-full rounded px-2 py-1.5 text-left outline-teal-600 hover:bg-zinc-50 focus-visible:outline-2 dark:outline-teal-400 dark:hover:bg-zinc-800">
                Product grid
              </button>
              <button className="block w-full rounded px-2 py-1.5 text-left outline-teal-600 hover:bg-zinc-50 focus-visible:outline-2 dark:outline-teal-400 dark:hover:bg-zinc-800">
                Footer
              </button>
            </div>
            <FloatingPanelResizeHandle className="absolute right-1 bottom-1 size-7 cursor-nwse-resize rounded text-zinc-400 outline-teal-600 hover:bg-zinc-100 hover:text-zinc-700 focus-visible:outline-2 dark:outline-teal-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-100">
              <ResizeGrip />
            </FloatingPanelResizeHandle>
          </FloatingPanelSurface>
        </FloatingPanel>

        <FloatingPanel>
          <FloatingPanelTrigger className={triggerClassName}>Properties</FloatingPanelTrigger>
          <FloatingPanelSurface placement="bottom end" className={surfaceClassName}>
            <FloatingPanelHeader className="flex cursor-grab items-center gap-2 border-b border-zinc-950/10 px-3 py-2 data-moving:cursor-grabbing dark:border-white/10">
              <FloatingPanelDragHandle className="cursor-grab touch-none rounded p-1 text-zinc-400 outline-teal-600 hover:bg-zinc-100 active:cursor-grabbing focus-visible:outline-2 data-moving:cursor-grabbing dark:outline-teal-400 dark:hover:bg-zinc-800">
                <MoveGrip />
              </FloatingPanelDragHandle>
              <FloatingPanelTitle className="flex-1 text-sm font-semibold text-zinc-950 dark:text-white">
                Properties
              </FloatingPanelTitle>
              <FloatingPanelClose className="rounded px-1.5 py-0.5 text-zinc-500 outline-teal-600 hover:bg-zinc-100 hover:text-zinc-950 focus-visible:outline-2 dark:outline-teal-400 dark:hover:bg-zinc-800 dark:hover:text-white">
                ×
              </FloatingPanelClose>
            </FloatingPanelHeader>
            <div className="grid grid-cols-[auto_1fr] items-center gap-3 p-3 text-sm text-zinc-700 dark:text-zinc-300">
              <label htmlFor="panel-opacity">Opacity</label>
              <input id="panel-opacity" className="w-full" type="range" defaultValue="80" />
              <label htmlFor="panel-name">Name</label>
              <input
                id="panel-name"
                className="min-w-0 rounded border border-zinc-950/10 px-2 py-1 outline-teal-600 focus-visible:outline-2 dark:border-white/10 dark:outline-teal-400"
                defaultValue="Header"
              />
            </div>
            <FloatingPanelResizeHandle className="absolute right-1 bottom-1 size-7 cursor-nwse-resize rounded text-zinc-400 outline-teal-600 hover:bg-zinc-100 hover:text-zinc-700 focus-visible:outline-2 dark:outline-teal-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-100">
              <ResizeGrip />
            </FloatingPanelResizeHandle>
          </FloatingPanelSurface>
        </FloatingPanel>
      </div>
    </FloatingPanelGroup>
  );
}

Anchored comment thread

Attach a movable, non-modal discussion to highlighted document content, with a chronological message log, reply composer, resolve state, and focusable numbered pin.

Loading example…
floating-panel.annotations.tsxtsx
import { useState, type FormEvent } from "react";
import {
  Button,
  FloatingPanel,
  FloatingPanelClose,
  FloatingPanelGroup,
  FloatingPanelHeader,
  FloatingPanelSurface,
  FloatingPanelTitle,
  FloatingPanelTrigger,
  Label,
  Messages,
  TextArea,
  TextField,
} from "@comp0/react";

type Comment = {
  id: number;
  author: string;
  body: string;
};

const initialComments: Comment[] = [
  { id: 1, author: "Mina", body: "Should this promise a specific response time?" },
  { id: 2, author: "You", body: "Good catch. I’ll make the wording measurable." },
];

export function Example() {
  const [comments, setComments] = useState(initialComments);
  const [reply, setReply] = useState("");
  const [resolved, setResolved] = useState(false);

  const submitReply = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const body = reply.trim();
    if (!body) return;
    setComments((current) => [...current, { id: current.length + 1, author: "You", body }]);
    setReply("");
  };

  return (
    <FloatingPanelGroup
      as="div"
      className="relative min-h-[40rem] overflow-hidden rounded-xl border border-zinc-950/10 bg-zinc-100 p-5 dark:border-white/10 dark:bg-zinc-950"
    >
      <article className="mx-auto max-w-lg rounded-lg bg-white p-6 shadow-sm dark:bg-zinc-900">
        <p className="text-xs font-medium tracking-wide text-teal-700 uppercase dark:text-teal-300">
          Launch brief
        </p>
        <h2 className="mt-2 text-xl font-semibold text-zinc-950 dark:text-white">
          Support that stays ahead of demand
        </h2>
        <p className="mt-3 text-sm/6 text-zinc-600 dark:text-zinc-300">
          Every customer gets a clear answer quickly, with specialists available when the problem
          needs deeper investigation.
        </p>
        <FloatingPanel defaultOpen>
          <div className="relative mt-5 rounded-lg border-l-4 border-amber-400 bg-amber-50 p-4 text-sm/6 text-amber-950 dark:bg-amber-950/40 dark:text-amber-100">
            Most requests receive a complete response in under two hours.
            <FloatingPanelTrigger className="absolute -top-3 -right-3 grid size-7 place-items-center rounded-full bg-amber-500 text-xs font-bold text-amber-950 shadow outline-amber-600 focus-visible:outline-2 focus-visible:outline-offset-2 dark:bg-amber-300 dark:outline-amber-300">
              <span aria-hidden="true">{comments.length}</span>
              <span className="sr-only">Open comments about response time</span>
            </FloatingPanelTrigger>
          </div>
          <FloatingPanelSurface
            placement="bottom start"
            className="flex max-h-96 w-72 flex-col overflow-hidden rounded-xl border border-zinc-950/10 bg-white shadow-xl outline-teal-600 data-active:outline-2 dark:border-white/10 dark:bg-zinc-900 dark:outline-teal-400"
          >
            <FloatingPanelHeader className="flex cursor-grab items-center gap-2 border-b border-zinc-950/10 px-3 py-2 dark:border-white/10">
              <FloatingPanelTitle className="flex-1 text-sm font-semibold text-zinc-950 dark:text-white">
                Response-time wording
              </FloatingPanelTitle>
              <FloatingPanelClose
                aria-label="Close comments"
                className="rounded px-1.5 py-0.5 text-zinc-500 outline-teal-600 hover:bg-zinc-100 focus-visible:outline-2 dark:outline-teal-400 dark:hover:bg-zinc-800"
              >
                <span aria-hidden="true">×</span>
              </FloatingPanelClose>
            </FloatingPanelHeader>
            <Messages
              aria-label="Comments about response time"
              tabIndex={0}
              className="flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto p-3 outline-teal-600 focus-visible:outline-2 dark:outline-teal-400"
            >
              {comments.map((comment) => (
                <article key={comment.id} className="text-sm text-zinc-700 dark:text-zinc-300">
                  <strong className="font-medium text-zinc-950 dark:text-white">
                    {comment.author}
                  </strong>
                  <p className="mt-0.5">{comment.body}</p>
                </article>
              ))}
            </Messages>
            <div className="border-t border-zinc-950/10 p-3 dark:border-white/10">
              {resolved ? (
                <Button
                  className="rounded-lg border border-zinc-950/15 px-3 py-2 text-sm font-medium outline-teal-600 focus-visible:outline-2 dark:border-white/15 dark:outline-teal-400"
                  onClick={() => setResolved(false)}
                >
                  Reopen thread
                </Button>
              ) : (
                <form className="grid gap-2" onSubmit={submitReply}>
                  <TextField value={reply} onChange={setReply} className="grid gap-1">
                    <Label className="text-sm font-medium text-zinc-950 dark:text-white">
                      Reply
                    </Label>
                    <TextArea
                      rows={2}
                      className="resize-none rounded-lg border border-zinc-950/15 bg-white px-2.5 py-2 text-sm text-zinc-950 outline-teal-600 focus-visible:outline-2 dark:border-white/15 dark:bg-zinc-950 dark:text-white dark:outline-teal-400"
                    />
                  </TextField>
                  <div className="flex gap-2">
                    <Button
                      type="submit"
                      disabled={!reply.trim()}
                      className="rounded-lg bg-teal-700 px-3 py-2 text-sm font-medium text-white outline-teal-600 focus-visible:outline-2 disabled:opacity-50 dark:bg-teal-400 dark:text-zinc-950"
                    >
                      Reply
                    </Button>
                    <Button
                      className="rounded-lg px-3 py-2 text-sm font-medium text-zinc-600 outline-teal-600 hover:bg-zinc-100 focus-visible:outline-2 dark:text-zinc-300 dark:outline-teal-400 dark:hover:bg-zinc-800"
                      onClick={() => setResolved(true)}
                    >
                      Resolve
                    </Button>
                  </div>
                </form>
              )}
            </div>
          </FloatingPanelSurface>
        </FloatingPanel>
      </article>
    </FloatingPanelGroup>
  );
}

Anatomy

Dashed frames are invisible state providers; shaded shapes own real DOM. Numbered pins match the list below.

A wireframe sketch of the assembled component. Each numbered marker matches a part in the list that follows.

  1. FloatingPanelGroup

    Focus and stacking coordinator that optionally renders the panel boundary. Owns a DOM element.

  2. FloatingPanel

    Wrapper-free owner for one panel's open state and geometry. Does not add a DOM element.

  3. FloatingPanelTrigger

    Button that opens or closes its panel. Owns a DOM element.

  4. FloatingPanelSurface

    Non-modal dialog that is fixed to the viewport or positioned within its group boundary. Owns a DOM element.

  5. FloatingPanelHeader

    Pointer-draggable header whose interactive descendants keep their native gestures. Owns a DOM element.

  6. FloatingPanelDragHandle

    Native button that moves the panel by pointer or an activated arrow-key interaction. Owns a DOM element.

  7. FloatingPanelTitle

    Native h2 that labels the dialog surface. Owns a DOM element.

  8. FloatingPanelClose

    Native button that closes this panel. Owns a DOM element.

  9. FloatingPanelResizeHandle

    Native button that changes size by pointer or an activated arrow-key interaction. Owns a DOM element.

Step by step

  1. 1

    Add the main part

    Wrap related panels in FloatingPanelGroup, then give each FloatingPanel its own trigger and labelled surface.

  2. 2

    Add the supporting parts

    Build the draggable surface header from FloatingPanelTitle, FloatingPanelDragHandle, and FloatingPanelClose; add FloatingPanelResizeHandle at the resize corner.

  3. 3

    Make the behavior clear

    Use position and size when geometry must persist. Render FloatingPanelGroup as an element for workspace-local coordinates, or leave it wrapper-free for viewport coordinates. Tab enters each panel normally; F6 is an optional shortcut between open panels and the application.

    Exampletsx
    <FloatingPanelGroup as="div">
      <FloatingPanel>
        <FloatingPanelTrigger>Layers</FloatingPanelTrigger>
        <FloatingPanelSurface>
          <FloatingPanelHeader>
            <FloatingPanelDragHandle />
            <FloatingPanelTitle>Layers</FloatingPanelTitle>
            <FloatingPanelClose />
          </FloatingPanelHeader>
          <FloatingPanelResizeHandle />
        </FloatingPanelSurface>
      </FloatingPanel>
    </FloatingPanelGroup>;

Keyboard

F6
Moves from the application through each open panel, then back to the application.
F6
Cycles through the same regions in reverse.
Space
Starts or commits a keyboard move or resize. · on a move or resize handle
Moves the panel by 16 pixels while keyboard moving is active. · on FloatingPanelDragHandle
Decreases or increases panel width by 16 pixels while resizing is active. · on FloatingPanelResizeHandle
Decreases or increases panel height by 16 pixels while resizing is active. · on FloatingPanelResizeHandle
Esc
Cancels an active move or resize; otherwise closes the focused panel.
Follows the normal document order through panel and application controls.

Forms and accessibility

No native form behavior; form controls inside a panel keep their own behavior.

Accessibility checklist

  • Give every FloatingPanelSurface a visible FloatingPanelTitle or an explicit aria-label. The surface is a non-modal dialog and must not use aria-modal.
  • Keep the surface and its controls in ordinary Tab order inside and outside panels. F6 and Shift+F6 are optional region-level shortcuts, never the only way to reach a panel.
  • Make drag and resize handles visibly discoverable and large enough to operate. Enter or Space activates one, arrows adjust it, and Enter or Space commits the change.
  • Let pointer users drag the header, but keep its buttons and form controls interactive. Add data-floating-panel-no-drag to any other header descendant that must own its pointer gesture.
  • Do not put essential workflow content only inside a floating panel that can be closed or moved off the main reading path.
  • Closing restores focus to that panel's trigger. Escape closes only the panel containing focus, not every panel in the group.

API reference

Importtsx
import { FloatingPanel, FloatingPanelClose, FloatingPanelDragHandle, FloatingPanelGroup, FloatingPanelHeader, FloatingPanelResizeHandle, FloatingPanelSurface, FloatingPanelTitle, FloatingPanelTrigger } from "@comp0/react";

FloatingPanelGroup

DOM element

Focus and stacking coordinator that optionally renders the panel boundary.

PropTypeDescription
asElementType | FragmentRenders a positioned local boundary; omit it to keep the group wrapper-free and viewport-based.

FloatingPanel

Context only

Wrapper-free owner for one panel's open state and geometry.

PropTypeDescription
openbooleanControlled open state.
defaultOpenbooleanInitial uncontrolled open state.
onToggle(open: boolean) => voidReceives the next open state.
positionFloatingPanelPosition | nullControlled viewport coordinates, or local coordinates when the group renders an element.
defaultPositionFloatingPanelPosition | nullInitial coordinates; null begins anchored to the trigger.
onPositionChange(position: FloatingPanelPosition) => voidReceives coordinates produced by moving the panel.
sizeFloatingPanelSize | nullControlled width and height in pixels.
defaultSizeFloatingPanelSize | nullInitial uncontrolled width and height.
onSizeChange(size: FloatingPanelSize) => voidReceives geometry produced by resizing the panel.

FloatingPanelTrigger

DOM element

Button that opens or closes its panel.

PropTypeDescription
asElementType | FragmentFragment merges trigger behavior onto one child element.

FloatingPanelSurface

DOM element

Non-modal dialog that is fixed to the viewport or positioned within its group boundary.

PropTypeDescription
aria-labelstringNames the panel when FloatingPanelTitle is omitted.
placementPopoverPlacementInitial side and alignment beside the trigger.
offsetnumberInitial pixel gap from the trigger; defaults to 8.
portalbooleanRenders viewport panels into document.body; locally contained panels remain inside their group element.

FloatingPanelHeader

DOM element

Pointer-draggable header whose interactive descendants keep their native gestures.

FloatingPanelDragHandle

DOM element

Native button that moves the panel by pointer or an activated arrow-key interaction.

FloatingPanelTitle

DOM element

Native h2 that labels the dialog surface.

FloatingPanelClose

DOM element

Native button that closes this panel.

FloatingPanelResizeHandle

DOM element

Native button that changes size by pointer or an activated arrow-key interaction.

Style hooks

Attributes that appear while a state is true. Target them with Tailwind data variants such as data-open:bg-zinc-100, or with any CSS selector.

FloatingPanelTrigger

Style hookMeaning
[data-open]Its panel is open.

FloatingPanelSurface

Style hookMeaning
[data-open]The panel is open.
[data-active]The panel is topmost in its group.
[data-moving]A keyboard or pointer move is active.
[data-resizing]A keyboard or pointer resize is active.

FloatingPanelHeader

Style hookMeaning
[data-moving]A keyboard or pointer move is active.

FloatingPanelDragHandle

Style hookMeaning
[data-moving]A keyboard or pointer move is active.

FloatingPanelResizeHandle

Style hookMeaning
[data-resizing]A keyboard or pointer resize is active.

Keep exploring