npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@aiquants/drag-drop-panels

v0.10.0

Published

Reusable drag-and-drop panel layout components and utilities

Readme

Drag & Drop Panel Package

Reusable Drag & Drop panel layout component package.

Package Info

  • Name: @aiquants/drag-drop-panels
  • Location: packages/drag-drop-panels in the monorepo

Features

  • Multi-column Drag & Drop layouts
  • Panel reordering and movement across columns, by drag or without one: each panel header has move buttons, and Alt + Shift + an arrow moves the panel that holds the focus; every such move the host carries out is announced to assistive technology
  • Touch and pen drag: a long-press on a panel header lifts the panel, and the layout runs it itself, so the host wires nothing for touch beyond the onColumnPanelsChange the move buttons already need
  • Column width resizing
  • Panel visibility toggle, close and maximize
  • Custom drag handler support
  • FLIP animations, left out for a reader who asks for reduced motion
  • Edge scrolling during a drag, which also reaches content below the fold when the layout sits in a scrolling page or container
  • Themable chrome (the panels, the hidden-panels bar and its restore buttons, the empty-column notice, a hidden panel's stand-in and the drop placeholders) through neutral --aqdd-* CSS custom properties, with light and .dark defaults that meet WCAG 2.2 AA
  • Replaceable labels, each built-in label marked with its own language
  • Debug overlay

Installation

// consumer package.json
{
  "dependencies": {
    "@aiquants/drag-drop-panels": "workspace:*"
  }
}
pnpm install

Usage

CSS Integration

Structural styles (layouts, resize handles, the panel chrome's colours and the panel header's layout) are required for all hosts. Ships plain CSS without :root theme variables, Tailwind utilities, or preflight:

  • @aiquants/drag-drop-panels/css (./css export, dist/drag-drop-panels.css) - Structural layout styles (import in JS or load per route via ?url).

Tailwind utilities in JSX depend on the host environment:

  • Tailwind v4 Host: Add the package src directory to your scan targets to generate utilities in your host build:

    /* app tailwind.css */
    @source "../node_modules/@aiquants/drag-drop-panels/src/**/*.{ts,tsx}";
    /* monorepo: @source "../../../../packages/drag-drop-panels/src/**/*.{ts,tsx}"; */
  • Non-Tailwind Host: Import the self-contained standalone CSS bundle (pnpm run build:css → dist/styles/drag-drop-panels.standalone.css):

    @import "@aiquants/drag-drop-panels/styles/drag-drop-panels.standalone.css";

    The dark defaults apply under an ancestor with the .dark class (such as <html class="dark">).

Basic Usage Example

useDragDropColumns owns the arrangement and the drag state; DragDropLayout renders it.

import { DEFAULT_DEBUG_CONFIG, DragDropLayout, type ColumnConfigType, type PanelConfig, useDragDropColumns } from "@aiquants/drag-drop-panels"
import "@aiquants/drag-drop-panels/css"

const columns: ColumnConfigType[] = [
  { id: "column-1", key: "column-1" },
  { id: "column-2", key: "column-2" },
]

const panels: PanelConfig[] = [
  { id: "panel-1", title: "Panel 1", component: MyPanel1 },
  { id: "panel-2", title: "Panel 2", component: MyPanel2 },
]

function MyApp() {
  const dnd = useDragDropColumns({
    initialColumnPanels: { "column-1": ["panel-1"], "column-2": ["panel-2"] },
    initialPanelVisibility: { "panel-1": true, "panel-2": true },
    debugConfig: DEFAULT_DEBUG_CONFIG,
  })

  return (
    <DragDropLayout
      columns={columns}
      panels={panels}
      columnPanels={dnd.columnPanels}
      onColumnPanelsChange={dnd.setColumnPanels}
      panelVisibility={dnd.panelVisibility}
      onPanelVisibilityChange={dnd.setPanelVisibility}
      dragState={dnd.dragState}
      dragOverPosition={dnd.dragOverPosition}
      originalPanelPosition={dnd.originalPanelPosition}
      dragModes={dnd.dragModes}
      debugConfig={DEFAULT_DEBUG_CONFIG}
      panelSizes={dnd.panelSizes}
      customDrag={dnd.customDrag}
      setPanelRef={dnd.setPanelRef}
      onDragStart={dnd.handleDragStart}
      onDragEnd={dnd.handleDragEnd}
      onPlaceholderDragEnter={dnd.handlePlaceholderDragEnter}
      onPlaceholderDragLeave={dnd.handlePlaceholderDragLeave}
      onDragOverWithInsert={dnd.handleDragOverWithInsert}
      onDropWithInsert={dnd.handleDropWithInsert}
      onColumnDragLeave={dnd.handleColumnDragLeave}
      onPanelMouseDown={dnd.handleMouseDown}
      onToggleDragMode={dnd.toggleDragMode}
    />
  )
}

dnd.setColumnPanels and dnd.setDragModes take the next value itself, as typed: the hook computes the next mouse drop and drag-mode switch on the last value they were given, so an updater function ((current) => next) throws a TypeError and changes nothing.

Observing Moves

Two callbacks report moves, and each move reaches exactly one of them:

| Move | Who changes the arrangement | Reported through | | --- | --- | --- | | A mouse drop (HTML5 drag or custom drag) | useDragDropColumns, in its own state | the hook's onPanelMove and onColumnPanelsChange options, each once per drop in one animation frame after the change (also under React's StrictMode) | | A header move button, Alt + Shift + an arrow, a touch or pen drop | you, answering the layout's request | DragDropLayout's onColumnPanelsChange prop |

In the frame of a mouse drop the hook calls its onColumnPanelsChange first and its onPanelMove second, the second even when the first throws. What they throw leaves that frame: one error as it is, both as one AggregateError whose errors hold the two in call order.

The layout's onColumnPanelsChange is a required prop. Wiring it to dnd.setColumnPanels (or to the state you pass back as columnPanels), as above, is all the move buttons and a touch screen need: the layout runs the touch and pen drag itself (see Touch and Pen Drag). The hook's options never see those moves, and the layout's prop never sees a mouse drop. To observe every change of the arrangement — to persist or log it — wire one handler to both, or observe dnd.columnPanels:

const dnd = useDragDropColumns({ ...options, onColumnPanelsChange: saveArrangement })

return (
  <DragDropLayout
    {...props}
    columnPanels={dnd.columnPanels}
    onColumnPanelsChange={(next) => {
      dnd.setColumnPanels(next)
      saveArrangement(next)
    }}
  />
)

Theming

The package stylesheet reads these neutral custom properties. The package never declares them itself, so an ordinary declaration on an ancestor of the layout (or on a panel) overrides the default whatever its cascade layer, with no !important. Bind them to your own design tokens in your layout:

.my-stage {
  --aqdd-stage-padding-block: 21px;
  --aqdd-stage-padding-inline: 13px;
  --aqdd-column-gap: 13px;
  --aqdd-panel-surface: var(--my-surface);
  --aqdd-panel-border: var(--my-border);
  --aqdd-panel-title-color: var(--my-text);
}

| Property | Styles | Default (light) | Default (.dark descendant) | | --- | --- | --- | --- | | --aqdd-stage-padding-block | Scroll root block padding | 1.5rem | same | | --aqdd-stage-padding-inline | Scroll root inline padding (one or two values) | 1rem 2rem; 1.5rem 2.5rem from 40rem wide | same | | --aqdd-column-gap | Gap between columns | 1.5rem | same | | --aqdd-panel-surface | Background of the panels, the hidden-panels bar, the empty-column notice and a hidden panel's stand-in | rgb(255 255 255 / 0.8), 0.95 while maximized | rgb(15 23 42 / 0.8), 0.95 while maximized | | --aqdd-panel-border | Border colour of the same parts | #e2e8f0 | #334155 | | --aqdd-panel-title-color | Panel title, and the text of the bar, the notice and the stand-in | #334155 | #e2e8f0 | | --aqdd-header-button-color | Header button icons | #475569 | #94a3b8 | | --aqdd-header-button-hover-color | Hovered header button icon | #0f172a (close: #b91c1c) | #f1f5f9 (close: #fca5a5) | | --aqdd-header-button-hover-surface | Hovered header button background | #f1f5f9 | #334155 | | --aqdd-custom-badge-color | Custom drag mode badge, pressed toggle and the stand-in's hidden badge, text and icon | #9a3412 | #fdba74 | | --aqdd-custom-badge-surface | Same, background | #ffedd5 | #431407 | | --aqdd-restore-button-color | Restore buttons of the hidden-panels bar, text and icon | #ffffff | same | | --aqdd-restore-button-surface | Same, background | #0369a1 | same | | --aqdd-restore-button-hover-surface | Same, background while hovered | #075985 | same | | --aqdd-drop-placeholder-color | Drop placeholders during a drag, text, icon and dashed border | #075985 | #bae6fd | | --aqdd-drop-placeholder-surface | Same, background | rgb(240 249 255 / 0.9) | rgb(8 47 73 / 0.9) | | --aqdd-drop-placeholder-target-surface | Background of the placeholder a drop would land on | #bae6fd | #075985 | | --aqdd-maximize-top / -right / -bottom / -left | Insets of a maximized panel | 0px | same |

The hidden-panels bar, the empty-column notice and a hidden panel's stand-in during a drag take the panel's surface, border and title colours, and the stand-in's "hidden" badge takes the custom badge colours, so one binding themes them all; the restore buttons and the drop placeholders take their own properties.

The defaults sit on the layout's root and on each panel, and a drop placeholder carries its own, so the exported InsertPlaceholder keeps its default colours (light and .dark) when you render it outside DragDropLayout; a property you set on any of its ancestors still overrides them.

The default icon colours reach at least 3:1 against the panel surface, and the badge, title, notice, restore button and placeholder text at least 4.5:1, in both schemes and over any page background. The dark defaults apply under an ancestor with the .dark class; a host that switches schemes another way sets the properties itself.

A drag image the layout appends to <body> (the custom-drag clone, and the ghost of a scaled-down layout) carries the values these properties resolve to on its panel, so a binding on an ancestor of the layout and a .dark class below <html> reach it too.

Labels

Every user-visible label is replaceable through labels. A built-in label keeps its text and marks the element that bears it with the language it is written in (lang="ja" or lang="en"); a label you supply replaces it verbatim and carries no lang, so it is read in your page's language. A label that is not a string, or labels that is not an object, throws a TypeError; a key that names no label (a misspelling, or a shared dictionary that also holds other keys) throws a RangeError naming the key.

<DragDropLayout
  {...props}
  labels={{
    hidePanel: "Hide panel",
    maximizePanel: "Maximize panel",
    restorePanel: "Restore panel",
    closePanel: "Close panel",
    switchToCustomDragMode: "Switch to custom drag mode",
    switchToNormalDragMode: "Back to normal drag mode",
    movePanelToPreviousColumn: "Move to previous column",
    movePanelToNextColumn: "Move to next column",
    movePanelUp: "Move up",
    movePanelDown: "Move down",
    customDragModeBadge: "Custom",
    panelMoved: "{title} moved to column {column}, position {position}",
    panelMoveUnavailable: "{title} cannot {move}",
    panelDragStarted: "Lifted {title}",
    panelDragCancelled: "{title} stays where it was",
  }}
/>

DEFAULT_DRAG_DROP_LAYOUT_LABELS lists every key and its built-in text: the header buttons, the badge, the move announcements (panelMoved, panelMoveUnavailable, and a touch or pen drag's panelDragStarted and panelDragCancelled), the hidden panels list (hiddenPanels, hiddenPanelBadge, hiddenPanelNotice), emptyColumn, dropZone, dropHere and resizeColumn. Six labels are templates (TEMPLATE_PLACEHOLDERS): panelMoved ({title}, {column}, {position}), panelMoveUnavailable ({title}, {move}), panelDragStarted ({title}), panelDragCancelled ({title}), dropZone ({column}) and resizeColumn ({column}); columns and positions count from 1, and a placeholder a key does not offer throws a RangeError.

An announcement keeps the language of every part: the template's own words and numbers are marked with the template's language, {title} (your panel title) is read in your page's language, and {move} keeps the language of the move button's label. Replacing only the template, or only a move name, therefore never reads one language in another's voice.

Moving Panels Without a Drag

  • Each panel header has four move buttons — to the previous or the next column, up or down in the column. A move the panel cannot make from where it sits is aria-disabled and stays focusable.
  • Alt + Shift + ← / → / ↑ / ↓ moves the panel that holds the focus (the shortcuts are on the buttons' aria-keyshortcuts). They are ignored inside text inputs and editable content, in content the panel renders outside its own DOM (a portal, such as a menu in <body>), on a keydown another handler already cancelled, with Ctrl or ⌘, while a panel is maximized and during a drag; a held chord moves once. A shortcut the panel cannot follow is announced as unavailable.
  • The layout asks for the move through onColumnPanelsChange(next) and announces it once you pass back, through columnPanels, an arrangement that moved that panel and nothing else — wherever you placed it, at once or later (wire onColumnPanelsChange to the state you pass in, such as useDragDropColumns' setColumnPanels). A request you decline is dropped without a word when the next arrangement also changes another panel or a drag starts; an arrangement that moves only that panel still counts as your answer.
  • When a move to another column remounts the panel, the focus returns to the control that held it; after any announced move, the control in the panel that holds the focus is scrolled into view by the least distance.
  • movePanelInColumns, availablePanelMoves, shownPanelPlace and panelMoveOfKey expose the same computation.

Touch and Pen Drag

  • A finger or a pen drags a panel by a long-press on its header (outside the header's buttons), whatever dragMode and dragHandle say — those choose how a mouse drags. The panel lifts once the pointer has rested there for longPressDelay ms (default 500) without moving more than slop CSS px (default 10). Moving further first, lifting the pointer sooner, a second finger or the browser taking the gesture over gives the press up, so scrolling and taps that start on a header keep working; until the panel lifts nothing is cancelled, no drag state shows and no drop placeholder appears.
  • Because the browser keeps the gesture until the panel lifts, it starts scrolling and sends pointercancel — which gives the press up — as soon as the pointer moves beyond the browser's own touch slop (it varies by platform). A slop larger than the browser's therefore has no effect; only a smaller one narrows the tolerance.
  • Once lifted, scrolling stops for that pointer (the header captures it and cancels its touch moves), a copy of the panel follows the pointer, and the drop placeholder nearest to it in the column under it is the target — over a layout nested in one of the panels, the column of this layout that holds it. The target is measured again whenever the stage, one of its ancestors or the page scrolls under a still finger, and edge scrolling runs as for a mouse drag.
  • Releasing over a target asks for the arrangement through the layout's onColumnPanelsChange(next) — the same arrangement a mouse drop on that placeholder produces (insertPanelAt) — and the layout announces the move once you pass it back, as it does for the move buttons; useDragDropColumns' onPanelMove and onColumnPanelsChange options are not called (see Observing Moves).
  • A pointercancel, Escape, leaving the layout, or releasing where the panel would stay where it is cancels without asking for anything. The lift and the cancel are announced (panelDragStarted, panelDragCancelled).
  • A browser drag that a touch or a pen press starts (anywhere in a panel) is cancelled and never reaches your onDragStart: iPadOS, Android, ChromeOS and Windows start the browser's own drag from a touch or pen long-press, which would compete with this one, so touch and pen drag a panel only by a long-press on its header. A mouse's drag is unaffected, also from a pen the system reports as a mouse. Nothing starts from a maximized panel, during another drag or from a mouse press.
  • Tune the timing with touchDragConfig (DEFAULT_TOUCH_DRAG_CONFIG holds the defaults); a value that is not a finite number of 0 or more, or an unknown key, throws.
<DragDropLayout {...props} touchDragConfig={{ ...DEFAULT_TOUCH_DRAG_CONFIG, longPressDelay: 350 }} />

Edge Scrolling

During any drag — a mouse's HTML5 or custom drag, or a touch or pen drag — a pointer within 80 px of an edge of the visible part of the layout's scroll root ([data-aqdd-scroll-root], its box clipped to the part of the page you see — the visual viewport under a pinch zoom) scrolls toward that edge, faster the nearer it is, following the pointer as it moves within the band.

  • The scroll root itself scrolls while it can move that way.
  • Where it cannot — at its end, or not scrolling at all because the layout sits in a container of no definite height — the nearest ancestor you can scroll that way (overflow auto or scroll) that hides part of the scroll root on that side scrolls instead, else the page while the viewport hides part of it. Each scrolls by no more than the hidden length, so the scroll stops once the scroll root's edge is in view; a scroll root that is entirely in view never scrolls the page.
  • Every step is instant, so a scroll-behavior: smooth on your page does not slow it.
  • The bands stay 80 CSS px wide under a pinch zoom: like the speeds and every other target of a drag, they grow with the content they lie over.

Column Resizing

With enableResize, each column whose resizable is not false gets a resize handle on its right edge (role="separator") that a mouse, a finger and a pen drag alike. Handles render only while onResizeStart, onResize and onResizeEnd are all passed — leave out any one of them and no column gets a handle, without a warning — and none render while a shown panel is maximized (see Maximize).

  • The primary button of a primary pointer starts a drag and reports onResizeStart(columnId, x); every move of that pointer reports onResize(x); its release, a pointercancel or the loss of the pointer capture reports onResizeEnd() exactly once (x is the pointer's viewport x in CSS px).
  • Other pointers and buttons start nothing. The handle has touch-action: none, so a touch or a pen on it never scrolls or zooms the page.
  • Turning isResizing off during a drag, or unmounting the handle, stops the drag without onResizeEnd.

Panel Header

The title comes first; the custom drag-mode badge and the buttons follow it and wrap onto the next line, aligned to the end, when the title would get less than 6em (the title then truncates with an ellipsis), so at narrow widths and large text sizes every button stays inside the panel. The buttons show while the panel is hovered, while one of them holds the focus, and always on devices that cannot hover. A held Enter acts once on a header button.

Maximize

With onPanelMaximizeChange, the header shows a maximize button and the panel named by maximizedPanel covers the viewport inside --aqdd-maximize-*. A double-click on the header outside its buttons toggles it, and an Escape keydown restores it (the layout cancels the Escape it uses) unless:

  • a handler already called preventDefault() on that keydown;
  • it belongs to an input method composition (isComposing, or Safari's composition-ending keydown with keyCode 229);
  • it was pressed inside an open <dialog>, or a popover the browser hides on Escape (popover / popover="auto" / popover="hint") is open — the browser closes those layers itself, which a cancelled Escape would prevent.

While a panel is maximized, everything else the layout renders is inert (also in the server render) and the scale is held at 1. All of this applies only while the named panel is shown: an id of a hidden or unknown panel covers nothing, keeps the scale and leaves Escape alone. Your own chrome is yours: keep it uncovered with --aqdd-maximize-*, or make it inert while a panel carries data-maximized.

Reduced Motion

For a reader who asks for reduced motion (prefers-reduced-motion: reduce), the layout leaves out the hover lift of the panels, every transition it renders (its utilities sit under motion-safe:, and its stylesheets stop their transitions) and the reorder animation of moved and new panels. A drag image still follows the pointer.

Scroll Root and Landmarks

The columns scroll inside a div marked data-aqdd-scroll-root, so the layout adds no landmark and can sit inside your <main>. Pass scrollRootElement="main" when your page renders no <main> of its own.

Host Scroll Areas and Bars

Two optional seams let a host draw its own scroll bars (for example one that hides the native vertical bar and shows a bar with a tap-scroll control). Without them the layout renders exactly what it renders today.

A panel's scroll area (renderPanelScrollArea). The layout calls it for each shown panel with { panelId, props, children } and puts what it returns in a scroll slot (data-aqdd-scroll-slot): a one-cell grid that fills the panel below its header, maximized or not, whose cell can shrink below its content. The node's outermost element is stretched to the cell, and a height: 100% inside it resolves against the cell.

  • Spread props (PanelScrollAreaProps: data-aqdd-scroll-area and the classes panel-scroll-area min-h-0 flex-1 overflow-y-auto) on the element that actually scrolls, and render children inside it exactly once. The panel's content finds its scroller with closest(SCROLL_AREA_SELECTOR), and panel-scroll-area keeps the thin native scrollbar; a wrapper that only clips (overflow: clip) must not carry them.
  • Mark your own controls inside the panel (the bar, a tap-scroll circle) with NO_PANEL_DRAG_ATTRIBUTE (data-aqdd-no-panel-drag): a mouse press on them, or inside them, never starts or arms a panel drag (see DOM Hooks). A plain element without it starts the panel's HTML5 drag when dragHandle is "panel".
  • Edge scrolling moves the scroll root and its ancestors only, never a panel's scroll area, so a host's scroll area does not change it. Maximizing keeps the host's elements and the content mounted.
  • Adding or removing the prop remounts every panel's content; keep it stable.
import { DragDropLayout, NO_PANEL_DRAG_ATTRIBUTE, type PanelScrollArea } from "@aiquants/drag-drop-panels"

const renderPanelScrollArea = ({ props, children }: PanelScrollArea) => (
    <div style={{ display: "flex", blockSize: "100%" }}>
        <div {...props}>{children}</div>
        <MyScrollBar {...{ [NO_PANEL_DRAG_ATTRIBUTE]: "" }} />
    </div>
)

<DragDropLayout {...layoutProps} renderPanelScrollArea={renderPanelScrollArea} />

The stage's scroll bar (renderStageScrollBar). The scroll root keeps its native bars hidden. With this prop the layout calls it with the scroll root once that is mounted (again after scrollRootElement changes) and puts the result in a slot (data-aqdd-stage-scroll-bar) laid over the inline-end edge of the stage and as tall as it: below the hidden-panels bar, outside the scale zoom. The slot takes no room, so widen the inline-end value of --aqdd-stage-padding-inline by your bar's width. While a panel is maximized the slot is not displayed.

DOM Hooks

Select the layout's elements by these attributes, never by class names:

  • The layout: data-aqdd-layout, data-aqdd-scroll-root, data-aqdd-columns, data-aqdd-hidden-panels with data-aqdd-restore-panel (the panel id each restore button shows again), data-aqdd-empty-column, data-aqdd-hidden-panel-stand-in with data-aqdd-hidden-panel-badge, data-aqdd-drop-placeholder (hidden, shown, or target for the placeholder a drop would land on), and data-aqdd-announcer.
  • A panel: data-panel-id with data-aqdd-panel, data-aqdd-drag-mode and data-maximized, data-aqdd-panel-header, data-aqdd-panel-title, data-aqdd-panel-controls, data-aqdd-panel-buttons, data-aqdd-custom-badge and data-aqdd-scroll-area (SCROLL_AREA_SELECTOR; with renderPanelScrollArea, on the host's scroller inside data-aqdd-scroll-slot).
  • The stage's host bar slot: data-aqdd-stage-scroll-bar (only with renderStageScrollBar).
  • Set by the host: data-aqdd-no-panel-drag (NO_PANEL_DRAG_ATTRIBUTE, any value) on its own controls inside a panel. A mouse press that starts on such an element or inside it reaches neither onPanelMouseDown (the custom drag mode's entry) nor onDragStart (the browser's drag it would start is cancelled). Clicks (onPanelClick), keys and the header's touch long-press are unaffected.
  • A header button: data-aqdd-panel-action (move-previous-column, move-next-column, move-up, move-down, drag-mode, hide, maximize, restore, close).

Demo App

You can view the interactive demo in the web application.

Running the Demo

  1. Start the web application:

    cd <your-app>
    pnpm dev
  2. Open the following URL in your browser:

    http://localhost:5173/drag-drop-demo

Demo Features

  • 3-column layout
  • 4 sample panels
  • Drag & Drop panel movement
  • Column width resizing
  • Panel visibility toggles

API Reference

DragDropLayout Props

| Property | Type | Required | Description | | --- | --- | --- | --- | | columns | ColumnConfig[] | ✓ | Column configurations, in display order | | panels | PanelConfig[] | ✓ | Panel configurations | | columnPanels | Record<string, string[]> | ✓ | Panel IDs per column key | | onColumnPanelsChange | (next: Record<string, string[]>) => void | ✓ | Receives the next arrangement of every move the layout asks for itself: a move button, Alt + Shift + an arrow, a touch or pen drop (a mouse drop goes through useDragDropColumns' options instead; see Observing Moves) | | panelVisibility | Record<string, boolean> | ✓ | Panel visibility map | | onPanelVisibilityChange | (visibility: Record<string, boolean>) => void | ✓ | Visibility change callback (hide button, hidden panels list) | | dragState, dragOverPosition, originalPanelPosition, dragModes, panelSizes, customDrag | hook state | ✓ (customDrag optional) | Drag state from useDragDropColumns | | onDragStart, onDragEnd, onPlaceholderDragEnter, onPlaceholderDragLeave, onDragOverWithInsert, onDropWithInsert, onColumnDragLeave | handlers | ✓ | Drag handlers from useDragDropColumns | | debugConfig | DebugConfig | ✓ | Debug overlay settings | | onToggleDragMode | (panelId: string) => void | - | Drag-mode toggle | | onPanelClose | (panelId: string) => void | - | Shows a close button that removes the panel | | onPanelMaximizeChange / maximizedPanel | (panelId: string \| null) => void / string \| null | - | Controlled maximize | | dragHandle | "panel" \| "header" | - | Where a mouse's HTML5 drag starts (default "panel"); touch and pen always drag by a long-press on the header | | touchDragConfig | TouchDragConfig | - | { longPressDelay, slop } of the touch and pen drag (default DEFAULT_TOUCH_DRAG_CONFIG) | | scrollRootElement | "div" \| "main" | - | Element of the scroll root (default "div") | | renderPanelScrollArea | (area: PanelScrollArea) => ReactNode | - | Renders each panel's scroll area in the host's place (see Host Scroll Areas and Bars) | | renderStageScrollBar | (scroller: HTMLElement) => ReactNode | - | Renders a host bar over the stage's inline-end edge for the scroll root | | labels | DragDropLayoutLabels | - | Replacement labels | | panelClassName | string | - | Classes appended to every panel frame | | scale / autoScale / onScaleChange | number / { requiredWidth, minScale? } / (scale) => void | - | Zoom out the layout | | enableResize, columnWidths, onResizeStart, onResize, onResizeEnd, isResizing, resizingColumn | resize config | - | Column width resizing |

ColumnConfig

interface ColumnConfig {
  id: string              // Unique column ID
  key: string             // Column key (referenced in columnPanels)
  initialWidth?: number   // Initial width in px
  minWidth?: number       // Minimum width in px
  maxWidth?: number       // Maximum width in px
  resizable?: boolean     // Whether column is resizable
  className?: string      // Custom CSS class
}

PanelConfig

interface PanelConfig {
  id: string                       // Panel ID (referenced in columnPanels)
  component: ComponentType         // Panel component reference
  title: string                    // Panel title
  props?: Record<string, unknown>  // Props passed to component
  minHeight?: string               // Minimum panel height (default 200px)
  maxHeight?: string               // Maximum panel height (default 500px)
}

Build

cd packages/drag-drop-panels
pnpm build

Build outputs:

  • dist/index.es.js - ES module
  • dist/index.umd.js - UMD module
  • dist/index.d.ts - TypeScript type definitions
  • dist/drag-drop-panels.css - Structural stylesheet (./css export)
  • dist/styles/drag-drop-panels.standalone.css - Standalone CSS for non-Tailwind hosts

Documentation & Specifications

Detailed architectural specifications, API additions, and testing guides are available in the package docs/ folder:

Testing

Run static type checking and unit tests before publishing or committing changes:

cd packages/drag-drop-panels

# Run TypeScript type check
pnpm run typecheck

# Run Vitest test suite
pnpm run test

Development

Modifying Package

  1. Edit source files in packages/drag-drop-panels/src
  2. Run type check and unit tests: pnpm run typecheck && pnpm run test
  3. Build: pnpm build
  4. Test in application

License

MIT