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>
);
}
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.
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.
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 |
|---|---|---|
open | boolean | Controlled open state. |
defaultOpen | boolean | Initial uncontrolled open state. |
onToggle | (open: boolean) => void | Receives the next open state. |
position | FloatingPanelPosition | null | Controlled viewport coordinates, or local coordinates when the group renders an element. |
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. |
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 |
|---|---|---|
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.
FloatingPanelDragHandle
DOM elementNative button that moves the panel by pointer or an activated arrow-key interaction.
FloatingPanelTitle
DOM elementNative h2 that labels the dialog surface.
FloatingPanelClose
DOM elementNative button that closes this panel.
FloatingPanelResizeHandle
DOM elementNative 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 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. |