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.

Layers preview
Header layerHero image layerFooter layer

Import#

typescript
import { DraggableContainer, DraggablePanel, PanelHeader } from "@reactberry/system/blocks";

Usage#

typescript
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.

Toolbar (kept clear)
Color settings
Fill: #F59E0BStroke: none

Viewport container#

DraggableContainer places a panel with default props in a fixed, viewport-sized layer, so it floats over the whole page.

Show inspector

API#

DraggableContainer#

PropTypeDefaultDescription
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#

PropTypeDefaultDescription
children*
ReactNode
—

Content rendered below the header.

label
string
Panel

Header text. Also used to build the localStorage key for the position.

parentRef*
RefObject<HTMLDivElement>
—

Element whose bounds constrain dragging.

topSafeZone
number
16

Declared but not used in any calculation. Use safeZone.top instead.

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 safeZone.left / safeZone.top.

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#

PropTypeDefaultDescription
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 pointerEvents: "auto".

onPointerDown
((e: PointerEvent<Element>) => void)
—

Pointer-down handler on the header. DraggablePanel uses it to start a drag.

style
CSSProperties
—

Inline styles for the header.

label is required in the type; the code falls back to "Panel Label".

Accessibility#

  • DraggablePanel always 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 (dragListener is false). 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.left and safeZone.top + initial.y.
  • The position is stored in localStorage under draggable-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 zIndex 99999 by default; pass zIndex to place it below other layers.
  • The DraggableContainer layer sets no top or left, so it is positioned where it appears in the document flow.
  • topSafeZone only appears in an effect dependency list; use safeZone.top to control the top inset.
  • The panel's initial prop is consumed by the component, and motion's own initial is set to false.
  • Drag movement follows the pointer directly; there is no reduced-motion handling.