Draggable
Float a draggable, translucent panel over the page that stays within its bounds and remembers its position.
The Draggable module exports three client components: DraggableContainer, DraggablePanel, and PanelHeader. The panel is dragged by its header using motion drag controls, is clamped inside a parent element, and persists its position in localStorage. Use it for floating tool panels such as inspectors or layer lists. There is no Draggable export.
Import#
import { DraggableContainer, DraggablePanel, PanelHeader } from "@reactberry/system/blocks";
Usage#
import { useRef } from "react";
import { DraggablePanel } from "@reactberry/system/blocks";
import { Box, Text } from "@reactberry/system/elements";
export default function Canvas() {
const areaRef = useRef<HTMLDivElement>(null!);
return (
<Box ref={areaRef} position="relative" width="100%" height="40rem">
<DraggablePanel parentRef={areaRef} label="Layers" initial={{ width: "18rem", x: 0, y: 0 }}>
<Box p="m"><Text>Layer list</Text></Box>
</DraggablePanel>
</Box>
);
}
Examples#
Safe zone#
Use safeZone to keep the panel clear of areas such as a toolbar along the parent's top edge.
Viewport container#
DraggableContainer places a panel with default props in a fixed, viewport-sized layer, so it floats over the whole page.
API#
DraggableContainer#
| Prop | Type | Default | Description |
|---|---|---|---|
children* | ReactNode | — | Panel content. |
DraggableContainer accepts no other props. It renders a fixed 100vw by 100vh layer with zIndex 9999 and pointerEvents: "none", and places a DraggablePanel with default props inside it.
DraggablePanel#
| Prop | Type | Default | Description |
|---|---|---|---|
children* | ReactNode | — | Content rendered below the header. |
label | string | Panel | Header text. Also used to build the |
parentRef* | RefObject<HTMLDivElement> | — | Element whose bounds constrain dragging. |
topSafeZone | number | 16 | Declared but not used in any calculation. Use |
safeZone | { top?: number; left?: number; bottom?: number | undefined; right?: number | undefined; } | undefined | { top: 16, left: 16, bot… | Inset in pixels kept between the panel and the parent's edges. |
initial | { width?: string; maxHeight?: string; x?: number | undefined; y?: number | undefined; } | undefined | {… | Panel width and max height, and the starting offset added to |
children (required) is rendered below the header. All remaining props are spread onto the panel's motion Box after its defaults, so they can override styling and drag settings.
PanelHeader#
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | Panel | Centred header title. |
leftControls | ReactNode | — | Content rendered on the left of the header. |
rightControls | ReactNode | — | Content rendered on the right of the header, with |
onPointerDown | ((e: PointerEvent<Element>) => void) | — | Pointer-down handler on the header. |
style | CSSProperties | — | Inline styles for the header. |
label is required in the type; the code falls back to "Panel Label".
Accessibility#
DraggablePanelalways renders a move icon button on the left and an empty icon button on the right of the header. These buttons have no accessible label.- Dragging has no keyboard alternative.
Notes#
- Dragging starts only from the header (
dragListenerisfalse). Momentum and elasticity are disabled. - Constraints are recalculated on mount, at drag start, on window resize, and when the parent resizes (via
ResizeObserver). The panel is clamped back inside the bounds when they shrink. - The starting position is
initial.x + safeZone.leftandsafeZone.top + initial.y. - The position is stored in
localStorageunderdraggable-pos-followed by the lowercased label with spaces replaced by hyphens. Panels sharing a label share a stored position. - During server rendering and before mount the panel uses the default position; the stored position is applied after mount.
- The panel has
zIndex99999 by default; passzIndexto place it below other layers. - The
DraggableContainerlayer sets notoporleft, so it is positioned where it appears in the document flow. topSafeZoneonly appears in an effect dependency list; usesafeZone.topto control the top inset.- The panel's
initialprop is consumed by the component, andmotion's owninitialis set tofalse. - Drag movement follows the pointer directly; there is no reduced-motion handling.