Skip to content

Early previewhamur is not on npm yet. The components are being built in the open, follow along on GitHub.

OverlaysReact

Floating Panel

A window inside the page: a non-modal dialog the person moves by its title bar, resizes from its edges and corners, maximizes, minimizes and closes, while the page behind stays usable.

Rendered by React 19.3.1

Loading React…
import { FloatingPanel } from '@hamur/react/floating-panel'

const layers = ['Header', 'Hero image', 'Headline', 'Body copy', 'Sign-up form', 'Footer']

export default function FloatingPanelExample() {
  return (
    <FloatingPanel.Root defaultSize={{ width: 320, height: 260 }}>
      <FloatingPanel.Trigger className="button">
        Open layers
      </FloatingPanel.Trigger>
      <FloatingPanel.Portal>
        <FloatingPanel.Popup className="floating-panel">
          <FloatingPanel.DragHandle className="floating-panel-bar">
            <FloatingPanel.Title className="floating-panel-title">
              Layers
            </FloatingPanel.Title>
            <FloatingPanel.Minimize className="floating-panel-button">
              <svg viewBox="0 0 24 24" aria-hidden="true">
                <path d="M5 12h14" />
              </svg>
            </FloatingPanel.Minimize>
            <FloatingPanel.Maximize className="floating-panel-button">
              <svg className="floating-panel-icon-maximize" viewBox="0 0 24 24" aria-hidden="true">
                <rect width="16" height="16" x="4" y="4" />
              </svg>
              <svg className="floating-panel-icon-restore" viewBox="0 0 24 24" aria-hidden="true">
                <path d="M8 8V4h12v12h-4M4 8h12v12H4z" />
              </svg>
            </FloatingPanel.Maximize>
            <FloatingPanel.Close
              className="floating-panel-button"
              aria-label="Close layers"
            >
              <svg viewBox="0 0 24 24" aria-hidden="true">
                <path d="M18 6 6 18M6 6l12 12" />
              </svg>
            </FloatingPanel.Close>
          </FloatingPanel.DragHandle>
          <FloatingPanel.Body className="floating-panel-body">
            <FloatingPanel.Description className="muted">
              Drag the title bar to move the panel. Focused, the arrow keys move it and Alt with the
              arrow keys resizes it.
            </FloatingPanel.Description>
            <ul className="floating-panel-list">
              {layers.map((layer) => (
                <li key={layer}>{layer}</li>
              ))}
            </ul>
          </FloatingPanel.Body>
          <FloatingPanel.ResizeHandle
            className="floating-panel-resize"
            position="bottom-right"
          >
            <svg viewBox="0 0 24 24" aria-hidden="true">
              <path d="M21 13 13 21M21 19l-2 2" />
            </svg>
          </FloatingPanel.ResizeHandle>
        </FloatingPanel.Popup>
      </FloatingPanel.Portal>
    </FloatingPanel.Root>
  )
}
Not measured yetTests passing in React
Not measured yetSize
Not measured yetMemory
Not measured yetSpeed
[N] commits

Installation

npm i @hamur/react

Not on npm yet. The package goes out under the chosen name.

Anatomy

Import the component and assemble its parts:

<FloatingPanel.Root>
  <FloatingPanel.Trigger />
  <FloatingPanel.Portal>
    <FloatingPanel.Popup>
      <FloatingPanel.DragHandle>
        <FloatingPanel.Title />
        <FloatingPanel.Minimize />
        <FloatingPanel.Maximize />
        <FloatingPanel.Close />
      </FloatingPanel.DragHandle>
      <FloatingPanel.Body>
        <FloatingPanel.Description />
      </FloatingPanel.Body>
      <FloatingPanel.ResizeHandle />
    </FloatingPanel.Popup>
  </FloatingPanel.Portal>
</FloatingPanel.Root>

Examples

Move

position and size are controlled; buttons in the Body move the panel to a corner or by 10 pixels, without a drag.

FloatingPanelMove.tsx
import * as React from 'react'
import { FloatingPanel } from '@hamur/react/floating-panel'
import type { FloatingPanelPosition, FloatingPanelSize } from '@hamur/react/floating-panel'

const MARGIN = 16
const NUDGE = 10

export default function FloatingPanelMoveExample() {
  const [position, setPosition] = React.useState<FloatingPanelPosition>({ x: MARGIN, y: MARGIN })
  const [size, setSize] = React.useState<FloatingPanelSize>({ width: 300, height: 240 })

  const room = () => {
    const root = document.documentElement
    return { x: root.clientWidth - size.width, y: root.clientHeight - size.height }
  }
  const corner = (right: boolean, bottom: boolean) => {
    const max = room()
    setPosition({ x: right ? max.x - MARGIN : MARGIN, y: bottom ? max.y - MARGIN : MARGIN })
  }
  const nudge = (dx: number, dy: number) => {
    const max = room()
    setPosition({
      x: Math.min(Math.max(position.x + dx, 0), max.x),
      y: Math.min(Math.max(position.y + dy, 0), max.y),
    })
  }

  return (
    <FloatingPanel.Root
      position={position}
      onPositionChange={setPosition}
      size={size}
      onSizeChange={setSize}
    >
      <FloatingPanel.Trigger className="button">
        Open panel
      </FloatingPanel.Trigger>
      <FloatingPanel.Portal>
        <FloatingPanel.Popup className="floating-panel">
          <FloatingPanel.DragHandle className="floating-panel-bar">
            <FloatingPanel.Title className="floating-panel-title">
              Position
            </FloatingPanel.Title>
            <FloatingPanel.Close
              className="floating-panel-button"
              aria-label="Close position"
            >
              <svg viewBox="0 0 24 24" aria-hidden="true">
                <path d="M18 6 6 18M6 6l12 12" />
              </svg>
            </FloatingPanel.Close>
          </FloatingPanel.DragHandle>
          <FloatingPanel.Body className="floating-panel-body">
            <output className="floating-panel-readout">
              {Math.round(position.x)}, {Math.round(position.y)}
            </output>
            <div className="floating-panel-grid" role="group" aria-label="Corners">
              <button type="button" className="button" onClick={() => corner(false, false)}>
                Top left
              </button>
              <button type="button" className="button" onClick={() => corner(true, false)}>
                Top right
              </button>
              <button type="button" className="button" onClick={() => corner(false, true)}>
                Bottom left
              </button>
              <button type="button" className="button" onClick={() => corner(true, true)}>
                Bottom right
              </button>
            </div>
            <div className="floating-panel-nudge" role="group" aria-label="Nudge by 10 pixels">
              <button type="button" className="button" onClick={() => nudge(-NUDGE, 0)}>
                Left
              </button>
              <button type="button" className="button" onClick={() => nudge(0, -NUDGE)}>
                Up
              </button>
              <button type="button" className="button" onClick={() => nudge(0, NUDGE)}>
                Down
              </button>
              <button type="button" className="button" onClick={() => nudge(NUDGE, 0)}>
                Right
              </button>
            </div>
          </FloatingPanel.Body>
          <FloatingPanel.ResizeHandle
            className="floating-panel-resize"
            position="bottom-right"
          >
            <svg viewBox="0 0 24 24" aria-hidden="true">
              <path d="M21 13 13 21M21 19l-2 2" />
            </svg>
          </FloatingPanel.ResizeHandle>
        </FloatingPanel.Popup>
      </FloatingPanel.Portal>
    </FloatingPanel.Root>
  )
}

API reference

40 props across 12 parts, in React syntax. className, style, render are listed once, under Shared props.

BodyFloatingPanel.Body

The panel's content, hidden while the panel is minimized. Renders a <div>.

CloseFloatingPanel.Close

A button that closes the floating panel. Renders a <button>.

Close props
PropTypeDefault
nativeButtonWhether the component renders a native <button> element when replacing itbooleantrue

DescriptionFloatingPanel.Description

A paragraph with additional information about the floating panel. Renders a <p>.

DragHandleFloatingPanel.DragHandle

The area that moves the panel with a pointer, usually its title bar. A double-click on it toggles the maximized stage. Renders a <div>.

DragHandle data attributes
Data attributeDescription
data-draggingPresent while the panel is being dragged.
data-disabledPresent when draggable is false or the panel is maximized.

MaximizeFloatingPanel.Maximize

A toggle button for the maximized stage, pressed while the panel is maximized. Named by its text, or by labels.maximize ("Maximize") when it has none; the name never changes with the stage. Renders a <button>.

Maximize props
PropTypeDefault
nativeButtonWhether the component renders a native <button> element when replacing itbooleantrue
Maximize data attributes
Data attributeDescription
data-pressedPresent while the panel is maximized.
data-disabledPresent when the button is disabled.

MinimizeFloatingPanel.Minimize

A toggle button for the minimized stage, pressed while the panel is minimized. Named by its text, or by labels.minimize ("Minimize") when it has none; the name never changes with the stage. Renders a <button>.

Minimize props
PropTypeDefault
nativeButtonWhether the component renders a native <button> element when replacing itbooleantrue
Minimize data attributes
Data attributeDescription
data-pressedPresent while the panel is minimized.
data-disabledPresent when the button is disabled.

PopupFloatingPanel.Popup

The panel: a non-modal dialog placed and sized in its boundary. It takes focus when it opens, and the arrow keys move it (Alt and arrows resize it) while it is focused itself. Renders a <div>.

Popup props
PropTypeDefault
initialFocusDetermines the element to focus when the floating panel is opened.boolean | React.RefObject<HTMLElement | null> | ((openType: InteractionType) => boolean | void | HTMLElement | null)—
finalFocusDetermines the element to focus when the floating panel is closed.boolean | React.RefObject<HTMLElement | null> | ((closeType: InteractionType) => boolean | void | HTMLElement | null)—
Popup data attributes
Data attributeDescription
data-openPresent when the floating panel is open.
data-closedPresent when the floating panel is closed.
data-starting-stylePresent when the floating panel begins animating in.
data-ending-stylePresent when the floating panel is animating out.
data-nestedPresent when the floating panel is nested within a dialog.
data-nested-dialog-openPresent when the floating panel has open dialogs nested within it.
data-draggingPresent while the panel is being dragged.
data-resizingPresent while the panel is being resized.
data-maximizedPresent while the panel is maximized.
data-minimizedPresent while the panel is minimized.
data-topmostPresent when the panel is in front of every other open panel in the document.
Popup CSS variables
CSS variableDescription
--panel-xThe rendered left edge in px from the boundary.
--panel-yThe rendered top edge in px from the boundary.
--panel-widthThe rendered width in px, once measured or set.
--panel-heightThe rendered height in px, once set; absent while minimized.
--panel-indexThe panel's place in the document's stack of open panels, 0 at the back.

PortalFloatingPanel.Portal

A portal element that moves the popup to a different part of the DOM. By default, the portal element is appended to <body>. Renders a <div>.

Portal props
PropTypeDefault
keepMountedWhether to keep the portal mounted in the DOM while the popup is hidden.booleanfalse
containerA parent element to render the portal element into.HTMLElement | ShadowRoot | React.RefObject<HTMLElement | ShadowRoot | null> | null—

ResizeHandleFloatingPanel.ResizeHandle

An edge or a corner that resizes the panel with a pointer, the opposite side anchored. Hidden from assistive technology: Alt with the arrow keys resizes the focused Popup. Renders a <div>.

ResizeHandle props
PropTypeDefault
positionrequiredThe edge or corner the handle resizes from, physical.FloatingPanelResizeHandlePosition—
ResizeHandle data attributes
Data attributeDescription
data-positionThe edge or corner the handle resizes from.
data-resizingPresent while this handle is resizing the panel.
data-disabledPresent when resizable is false or the stage is not normal.

RootFloatingPanel.Root

Groups all parts of the floating panel: a non-modal dialog the user moves, resizes, maximizes and minimizes. Doesn’t render its own element.

Root props
PropTypeDefault
openWhether the floating panel is currently open.boolean—
defaultOpenWhether the floating panel is initially open.booleanfalse
onOpenChangeEvent handler called when the floating panel is opened or closed.((open: boolean, eventDetails: FloatingPanelRootChangeEventDetails) => void)—
onOpenChangeCompleteEvent handler called after any animations complete when the floating panel is opened or((open: boolean) => void)—
actionsRefA ref to imperative actions.React.RefObject<DialogRootActions | null>—
handleA handle to associate the floating panel with a trigger.FloatingPanelHandle<Payload>—
childrenThe content of the floating panel.React.ReactNode | PayloadChildRenderFunction<Payload>—
triggerIdID of the trigger that the floating panel is associated with.string | null—
defaultTriggerIdID of the trigger that the floating panel is associated with.string | null—
positionThe top-left corner of the normal stage in CSS px from the boundary's top-left corner.FloatingPanelPosition—
defaultPositionThe initial position, when uncontrolled.FloatingPanelPosition—
onPositionChangeEvent handler called when the position changes.((position: FloatingPanelPosition, eventDetails: FloatingPanelRootGeometryChangeEventDetails) => void)—
onPositionCommittedEvent handler called when a drag or a key that moved the panel ends.((position: FloatingPanelPosition, eventDetails: FloatingPanelRootGeometryChangeEventDetails) => void)—
sizeThe border box of the normal stage in CSS px.FloatingPanelSize—
defaultSizeThe initial size, when uncontrolled.FloatingPanelSize—
onSizeChangeEvent handler called when the size changes.((size: FloatingPanelSize, eventDetails: FloatingPanelRootGeometryChangeEventDetails) => void)—
onSizeCommittedEvent handler called when a resize or a key that resized the panel ends.((size: FloatingPanelSize, eventDetails: FloatingPanelRootGeometryChangeEventDetails) => void)—
stageThe stage: normal, maximized (the boundary's whole box) or minimized (the Body hidden).FloatingPanelStage—
defaultStageThe initial stage, when uncontrolled.FloatingPanelStage'normal'
onStageChangeEvent handler called when the stage changes.((stage: FloatingPanelStage, eventDetails: FloatingPanelRootStageChangeEventDetails) => void)—
minSizeThe smallest size a resize reaches, in CSS px.FloatingPanelSize{ width: 120, height: 100 }
maxSizeThe largest size a resize reaches, in CSS px.FloatingPanelSize—
boundaryThe box the panel stays inside: the viewport, or an element that is the Popup's containingElement | React.RefObject<Element | null> | "viewport"'viewport'
draggableWhether the panel moves with the DragHandle and the arrow keys.booleantrue
resizableWhether the panel resizes with the ResizeHandles and Alt with the arrow keys.booleantrue
stepCSS px per arrow key.number1
largeStepCSS px per arrow key with Shift.number10
labelsThe toggles' aria-labels when their content has no text, over the English defaults.Partial<FloatingPanelLabels>—

TitleFloatingPanel.Title

A heading that labels the floating panel. Renders a <h2>.

TriggerFloatingPanel.Trigger

A button that opens the floating panel. Renders a <button>.

Trigger props
PropTypeDefault
handleA handle to associate the trigger with a floating panel.FloatingPanelHandle<Payload>—
payloadA payload to pass to the floating panel when it is opened.Payload—
idID of the trigger.string—
nativeButtonWhether the component renders a native <button> element when replacing itbooleantrue

Shared props

Every part that renders an element takes these.

Shared props
PropType
classNameCSS class applied to the element, or a function that returns a class based on the component's state.string | ((state: State) => string | undefined)
styleStyle applied to the element, or a function that returns a style object based on the component's state.React.CSSProperties | ((state: State) => React.CSSProperties | undefined)
renderAllows you to replace the component's HTML element with a different tag, or compose it with another component.React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, State>

Keyboard and accessibility

Keyboard

Keyboard
KeysDoesPart
ArrowLeftArrowRightArrowUpArrowDownMoves the focused Popup by step (1 pixel), physicallyPopup
ShiftMoves it by largeStep (10 pixels)Popup
AltResizes it by step, the top-left corner anchoredPopup
AltShiftResizes it by largeStepPopup
EscapeCloses the panel, from anywhere inside itPopup
EscapeDuring a drag or a resize, puts the panel back where it was and keeps it openDragHandle
TabFrom the Trigger, moves to the Popup; from its last control, leaves the panelPopup
ShiftTabFrom the first control in the Popup, returns to the PopupPopup
EnterSpacePresses Maximize, Minimize or CloseMaximize

Accessibility

What the parts set
PartSetsSo that
Triggeraria-haspopup="dialog", aria-expanded, aria-controlsa screen reader says the button opens a dialog and whether it is open
Popuprole="dialog", no aria-modal, aria-labelledby to Title and aria-describedby to Descriptionthe panel is announced as a dialog with its heading, and the page behind stays reachable
Popuptabindex="0" while it can move or resize, -1 otherwisethe move and resize keys are reachable with Shift+Tab from the first control
Maximizea button with aria-pressed, named "Maximize" (labels) when its content has no textthe toggle says its state, and its name does not change with the stage
Minimizea button with aria-pressed, named "Minimize" (labels) when its content has no textthe same for the minimized stage
Bodyhidden while minimizeda minimized panel's content is neither shown nor read
DragHandleno role and no focus, touch-action: none and user-select: nonea drag neither scrolls the page nor selects text; the keys on the Popup do the same work
ResizeHandlearia-hidden="true", no focusa handle is not read; Alt with the arrow keys resizes the focused Popup

Focus order

  1. Opening moves focus to the Popup itself, so the arrow keys move it at once, or to initialFocus.
  2. Focus is not trapped: Tab goes from the Trigger to the Popup and from its last control to the element after the Trigger.
  3. A press on the DragHandle focuses the Popup only when focus is outside it; minimizing moves focus out of the hidden Body to the Popup.
  4. Closing returns focus to the Trigger, or to finalFocus.