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>
  );
}

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="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">
            Layers
          </FloatingPanelTrigger>
          <FloatingPanelSurface
            placement="bottom start"
            className="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"
          >
            <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="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">
            Properties
          </FloatingPanelTrigger>
          <FloatingPanelSurface
            placement="bottom end"
            className="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"
          >
            <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>
              )}
              {!resolved && (
                <form className="grid gap-2" onSubmit={submitReply}>
                  <TextField as="div" 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
asElementTypeRenders a wrapper element that carries the root's data attributes and DOM props; without it the root renders no DOM.
idstringBase for the generated trigger and content ids; also the wrapper id when as is set.
openbooleanControlled open state.
defaultOpenbooleanInitial uncontrolled open state.
onOpenChange(open: boolean) => voidReceives the next open state.
positionFloatingPanelPosition | nullControlled viewport coordinates, or local coordinates when the group renders an element. Non-finite coordinates warn in development and are ignored.
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. Non-finite or non-positive sizes warn in development and are ignored.
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
asElementType | FragmentElement or component rendered in place of the default; Fragment merges the part onto your own element child.
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.

PropTypeDescription
asElementType | FragmentElement or component rendered in place of the default; Fragment merges the part onto your own element child.

FloatingPanelDragHandle

DOM element

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

PropTypeDescription
asElementType | FragmentElement or component rendered in place of the default; Fragment merges the part onto your own element child.

FloatingPanelTitle

DOM element

Native h2 that labels the dialog surface.

PropTypeDescription
asElementType | FragmentElement or component rendered in place of the default; Fragment merges the part onto your own element child.

FloatingPanelClose

DOM element

Native button that closes this panel.

PropTypeDescription
asElementType | FragmentElement or component rendered in place of the default; Fragment merges the part onto your own element child.

FloatingPanelResizeHandle

DOM element

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

PropTypeDescription
asElementType | FragmentElement or component rendered in place of the default; Fragment merges the part onto your own element child.

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