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.
Example
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.
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.
FloatingPanelGroup
Focus and stacking coordinator that optionally renders the panel boundary. Owns a DOM element.
FloatingPanel
Wrapper-free owner for one panel's open state and geometry. Does not add a DOM element.
FloatingPanelTrigger
Button that opens or closes its panel. Owns a DOM element.
FloatingPanelSurface
Non-modal dialog that is fixed to the viewport or positioned within its group boundary. Owns a DOM element.
FloatingPanelHeader
Pointer-draggable header whose interactive descendants keep their native gestures. Owns a DOM element.
FloatingPanelDragHandle
Native button that moves the panel by pointer or an activated arrow-key interaction. Owns a DOM element.
FloatingPanelTitle
Native h2 that labels the dialog surface. Owns a DOM element.
FloatingPanelClose
Native button that closes this panel. Owns a DOM element.
FloatingPanelResizeHandle
Native button that changes size by pointer or an activated arrow-key interaction. Owns a DOM element.
Step by step
- 1
Add the main part
Wrap related panels in FloatingPanelGroup, then give each FloatingPanel its own trigger and labelled surface.
- 2
Add the supporting parts
Build the draggable surface header from FloatingPanelTitle, FloatingPanelDragHandle, and FloatingPanelClose; add FloatingPanelResizeHandle at the resize corner.
- 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
import { FloatingPanel, FloatingPanelClose, FloatingPanelDragHandle, FloatingPanelGroup, FloatingPanelHeader, FloatingPanelResizeHandle, FloatingPanelSurface, FloatingPanelTitle, FloatingPanelTrigger } from "@comp0/react";FloatingPanelGroup
DOM elementFocus and stacking coordinator that optionally renders the panel boundary.
| Prop | Type | Description |
|---|---|---|
as | ElementType | Fragment | Renders a positioned local boundary; omit it to keep the group wrapper-free and viewport-based. |
FloatingPanel
Context onlyWrapper-free owner for one panel's open state and geometry.
| Prop | Type | Description |
|---|---|---|
as | ElementType | Renders a wrapper element that carries the root's data attributes and DOM props; without it the root renders no DOM. |
id | string | Base for the generated trigger and content ids; also the wrapper id when as is set. |
open | boolean | Controlled open state. |
defaultOpen | boolean | Initial uncontrolled open state. |
onOpenChange | (open: boolean) => void | Receives the next open state. |
position | FloatingPanelPosition | null | Controlled viewport coordinates, or local coordinates when the group renders an element. Non-finite coordinates warn in development and are ignored. |
defaultPosition | FloatingPanelPosition | null | Initial coordinates; null begins anchored to the trigger. |
onPositionChange | (position: FloatingPanelPosition) => void | Receives coordinates produced by moving the panel. |
size | FloatingPanelSize | null | Controlled width and height in pixels. Non-finite or non-positive sizes warn in development and are ignored. |
defaultSize | FloatingPanelSize | null | Initial uncontrolled width and height. |
onSizeChange | (size: FloatingPanelSize) => void | Receives geometry produced by resizing the panel. |
FloatingPanelTrigger
DOM elementButton that opens or closes its panel.
| Prop | Type | Description |
|---|---|---|
as | ElementType | Fragment | Fragment merges trigger behavior onto one child element. |
FloatingPanelSurface
DOM elementNon-modal dialog that is fixed to the viewport or positioned within its group boundary.
| Prop | Type | Description |
|---|---|---|
as | ElementType | Fragment | Element or component rendered in place of the default; Fragment merges the part onto your own element child. |
aria-label | string | Names the panel when FloatingPanelTitle is omitted. |
placement | PopoverPlacement | Initial side and alignment beside the trigger. |
offset | number | Initial pixel gap from the trigger; defaults to 8. |
portal | boolean | Renders viewport panels into document.body; locally contained panels remain inside their group element. |
FloatingPanelHeader
DOM elementPointer-draggable header whose interactive descendants keep their native gestures.
| Prop | Type | Description |
|---|---|---|
as | ElementType | Fragment | Element or component rendered in place of the default; Fragment merges the part onto your own element child. |
FloatingPanelDragHandle
DOM elementNative button that moves the panel by pointer or an activated arrow-key interaction.
| Prop | Type | Description |
|---|---|---|
as | ElementType | Fragment | Element or component rendered in place of the default; Fragment merges the part onto your own element child. |
FloatingPanelTitle
DOM elementNative h2 that labels the dialog surface.
| Prop | Type | Description |
|---|---|---|
as | ElementType | Fragment | Element or component rendered in place of the default; Fragment merges the part onto your own element child. |
FloatingPanelClose
DOM elementNative button that closes this panel.
| Prop | Type | Description |
|---|---|---|
as | ElementType | Fragment | Element or component rendered in place of the default; Fragment merges the part onto your own element child. |
FloatingPanelResizeHandle
DOM elementNative button that changes size by pointer or an activated arrow-key interaction.
| Prop | Type | Description |
|---|---|---|
as | ElementType | Fragment | Element 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 hook | Meaning |
|---|---|
[data-open] | Its panel is open. |
FloatingPanelSurface
| Style hook | Meaning |
|---|---|
[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 hook | Meaning |
|---|---|
[data-moving] | A keyboard or pointer move is active. |
FloatingPanelDragHandle
| Style hook | Meaning |
|---|---|
[data-moving] | A keyboard or pointer move is active. |
FloatingPanelResizeHandle
| Style hook | Meaning |
|---|---|
[data-resizing] | A keyboard or pointer resize is active. |