Floating Panel
A floating panel is a detachable window that floats above the main interface, typically used for displaying and editing properties. The panel can be dragged, resized, and positioned anywhere on the screen for optimal workflow.
Think of the panel that pops up in Figma when you click
variablesor try set a color.
Features
- Allows interaction with the main content
- Supports dragging and resizing
- Support for minimizing and maximizing the panel
- Controlled and uncontrolled size and position
- Support for snapping to a grid
- Support for locking the aspect ratio
- Support for closing on escape key
- Support for persisting the size and position when closed
Installation
To use the hover card machine in your project, run the following command in your command line:
npm install @zag-js/floating-panel @zag-js/react # or yarn add @zag-js/floating-panel @zag-js/react
npm install @zag-js/floating-panel @zag-js/solid # or yarn add @zag-js/floating-panel @zag-js/solid
npm install @zag-js/floating-panel @zag-js/vue # or yarn add @zag-js/floating-panel @zag-js/vue
npm install @zag-js/floating-panel @zag-js/svelte # or yarn add @zag-js/floating-panel @zag-js/svelte
Anatomy
To set up the editable correctly, you'll need to understand its anatomy and how we name its parts.
Each part includes a
data-partattribute to help identify them in the DOM.
Usage
First, import the floating panel package into your project
import * as floatingPanel from "@zag-js/floating-panel"
The floating panel package exports two key functions:
machine— The state machine logic for the floating panel widget.connect— The function that translates the machine's state to JSX attributes and event handlers.
Next, import the required hooks and functions for your framework and use the floating panel machine in your project 🔥
import * as floatingPanel from "@zag-js/floating-panel" import { useMachine, normalizeProps } from "@zag-js/react" import { ArrowDownLeft, Maximize2, Minus, XIcon } from "lucide-react" import { useId } from "react" function FloatingPanel() { const service = useMachine(floatingPanel.machine, { id: useId() }) const api = floatingPanel.connect(service, normalizeProps) return ( <> <button {...api.getTriggerProps()}>Toggle Panel</button> <div {...api.getPositionerProps()}> <div {...api.getContentProps()}> <div {...api.getDragTriggerProps()}> <div {...api.getHeaderProps()}> <p {...api.getTitleProps()}>Floating Panel</p> <div {...api.getControlProps()}> <button {...api.getStageTriggerProps({ stage: "minimized" })}> <Minus /> </button> <button {...api.getStageTriggerProps({ stage: "maximized" })}> <Maximize2 /> </button> <button {...api.getStageTriggerProps({ stage: "default" })}> <ArrowDownLeft /> </button> <button {...api.getCloseTriggerProps()}> <XIcon /> </button> </div> </div> </div> <div {...api.getBodyProps()}> <p>Some content</p> </div> <div {...api.getResizeTriggerProps({ axis: "n" })} /> <div {...api.getResizeTriggerProps({ axis: "e" })} /> <div {...api.getResizeTriggerProps({ axis: "w" })} /> <div {...api.getResizeTriggerProps({ axis: "s" })} /> <div {...api.getResizeTriggerProps({ axis: "ne" })} /> <div {...api.getResizeTriggerProps({ axis: "se" })} /> <div {...api.getResizeTriggerProps({ axis: "sw" })} /> <div {...api.getResizeTriggerProps({ axis: "nw" })} /> </div> </div> </> ) }
import * as floatingPanel from "@zag-js/floating-panel" import { useMachine, normalizeProps } from "@zag-js/solid" import { createMemo, createUniqueId } from "solid-js" import { ArrowDownLeft, Maximize2, Minus, XIcon } from "lucide-solid" function FloatingPanel() { const service = useMachine(floatingPanel.machine, { id: createUniqueId() }) const api = createMemo(() => floatingPanel.connect(service, normalizeProps)) return ( <> <button {...api().getTriggerProps()}>Toggle Panel</button> <div {...api().getPositionerProps()}> <div {...api().getContentProps()}> <div {...api().getDragTriggerProps()}> <div {...api().getHeaderProps()}> <p {...api().getTitleProps()}>Floating Panel</p> <div {...api().getControlProps()}> <button {...api().getStageTriggerProps({ stage: "minimized" })}> <Minus /> </button> <button {...api().getStageTriggerProps({ stage: "maximized" })}> <Maximize2 /> </button> <button {...api().getStageTriggerProps({ stage: "default" })}> <ArrowDownLeft /> </button> <button {...api().getCloseTriggerProps()}> <XIcon /> </button> </div> </div> </div> <div {...api().getBodyProps()}> <p>Some content</p> </div> <div {...api().getResizeTriggerProps({ axis: "n" })} /> <div {...api().getResizeTriggerProps({ axis: "e" })} /> <div {...api().getResizeTriggerProps({ axis: "w" })} /> <div {...api().getResizeTriggerProps({ axis: "s" })} /> <div {...api().getResizeTriggerProps({ axis: "ne" })} /> <div {...api().getResizeTriggerProps({ axis: "se" })} /> <div {...api().getResizeTriggerProps({ axis: "sw" })} /> <div {...api().getResizeTriggerProps({ axis: "nw" })} /> </div> </div> </> ) }
<script setup> import * as floatingPanel from "@zag-js/floating-panel" import { useMachine, normalizeProps } from "@zag-js/vue" import { computed, useId } from "vue" import { Minus, Maximize2, ArrowDownLeft, XIcon } from "lucide-vue-next" const service = useMachine(floatingPanel.machine, { id: useId() }) const api = computed(() => floatingPanel.connect(service, normalizeProps)) </script> <template> <button v-bind="api.getTriggerProps()">Toggle Panel</button> <div v-bind="api.getPositionerProps()"> <div v-bind="api.getContentProps()"> <div v-bind="api.getDragTriggerProps()"> <div v-bind="api.getHeaderProps()"> <p v-bind="api.getTitleProps()">Floating Panel</p> <div v-bind="api.getControlProps()"> <button v-bind="api.getStageTriggerProps({ stage: 'minimized' })"> <Minus /> </button> <button v-bind="api.getStageTriggerProps({ stage: 'maximized' })"> <Maximize2 /> </button> <button v-bind="api.getStageTriggerProps({ stage: 'default' })"> <ArrowDownLeft /> </button> <button v-bind="api.getCloseTriggerProps()"> <XIcon /> </button> </div> </div> </div> <div v-bind="api.getBodyProps()"> <p>Some content</p> </div> <div v-bind="api.getResizeTriggerProps({ axis: 'n' })" /> <div v-bind="api.getResizeTriggerProps({ axis: 'e' })" /> <div v-bind="api.getResizeTriggerProps({ axis: 'w' })" /> <div v-bind="api.getResizeTriggerProps({ axis: 's' })" /> <div v-bind="api.getResizeTriggerProps({ axis: 'ne' })" /> <div v-bind="api.getResizeTriggerProps({ axis: 'se' })" /> <div v-bind="api.getResizeTriggerProps({ axis: 'sw' })" /> <div v-bind="api.getResizeTriggerProps({ axis: 'nw' })" /> </div> </div> </template>
<script lang="ts"> import * as floatingPanel from "@zag-js/floating-panel" import { normalizeProps, useMachine } from "@zag-js/svelte" import { ArrowDownLeft, Maximize2, Minus, XIcon } from "lucide-svelte" const id = $props.id() const service = useMachine(floatingPanel.machine, { id }) const api = $derived(floatingPanel.connect(service, normalizeProps)) </script> <button {...api.getTriggerProps()}>Toggle Panel</button> <div {...api.getPositionerProps()}> <div {...api.getContentProps()}> <div {...api.getDragTriggerProps()}> <div {...api.getHeaderProps()}> <p {...api.getTitleProps()}>Floating Panel</p> <div {...api.getControlProps()}> <button {...api.getStageTriggerProps({ stage: "minimized" })}> <Minus /> </button> <button {...api.getStageTriggerProps({ stage: "maximized" })}> <Maximize2 /> </button> <button {...api.getStageTriggerProps({ stage: "default" })}> <ArrowDownLeft /> </button> <button {...api.getCloseTriggerProps()}> <XIcon /> </button> </div> </div> </div> <div {...api.getBodyProps()}> <p>Some content</p> </div> <div {...api.getResizeTriggerProps({ axis: "n" })}></div> <div {...api.getResizeTriggerProps({ axis: "e" })}></div> <div {...api.getResizeTriggerProps({ axis: "w" })}></div> <div {...api.getResizeTriggerProps({ axis: "s" })}></div> <div {...api.getResizeTriggerProps({ axis: "ne" })}></div> <div {...api.getResizeTriggerProps({ axis: "se" })}></div> <div {...api.getResizeTriggerProps({ axis: "sw" })}></div> <div {...api.getResizeTriggerProps({ axis: "nw" })}></div> </div> </div>
Resizing
Setting the initial size
To set the initial size of the floating panel, you can pass the defaultSize
prop to the machine.
const service = useMachine(floatingPanel.machine, { defaultSize: { width: 300, height: 300 }, })
Controlling the size
To control the size of the floating panel programmatically, you can pass the
size onResize prop to the machine.
const service = useMachine(floatingPanel.machine, { size: { width: 300, height: 300 }, onSizeChange(details) { // details => { width: number, height: number } console.log("floating panel is:", details.width, details.height) }, })
Disable resizing
By default, the panel can be resized by dragging its edges (resize handles). To
disable this behavior, set the resizable prop to false.
const service = useMachine(floatingPanel.machine, { resizable: false, })
Setting size constraints
You can also control the minimum allowed dimensions of the panel by using the
minSize and maxSize props.
const service = useMachine(floatingPanel.machine, { minSize: { width: 100, height: 100 }, maxSize: { width: 500, height: 500 }, })
Aspect ratio
To lock the aspect ratio of the floating panel, set the lockAspectRatio prop.
This will ensure the panel maintains a consistent aspect ratio while being
resized.
const service = useMachine(floatingPanel.machine, { lockAspectRatio: true, })
Positioning
Setting the initial position
To specify the initial position of the floating panel, use the defaultPosition
prop. If defaultPosition is not provided, the floating panel will be initially
positioned at the center of the viewport.
const service = useMachine(floatingPanel.machine, { defaultPosition: { x: 500, y: 200 }, })
Anchor position
An alternative to setting the initial position is to provide a function that
returns the anchor position. This function is called when the panel is opened
and receives the triggerRect and boundaryRect.
const service = useMachine(floatingPanel.machine, { getAnchorPosition({ triggerRect, boundaryRect }) { return { x: boundaryRect.x + (boundaryRect.width - triggerRect.width) / 2, y: boundaryRect.y + (boundaryRect.height - triggerRect.height) / 2, } }, })
Controlling the position
To control the position of the floating panel programmatically, you can pass the
position and onPositionChange prop to the machine.
const service = useMachine(floatingPanel.machine, { position: { x: 500, y: 200 }, onPositionChange(details) { // details => { x: number, y: number } console.log("floating panel is:", details.x, details.y) }, })
Disable dragging
The floating panel enables you to set its position and move it by dragging. To
disable this behavior, set the draggable prop to false.
Events
The floating panel generates a variety of events that you can handle.
Open State
When the floating panel is opened or closed, the onOpenChange callback is
invoked.
const service = useMachine(floatingPanel.machine, { onOpenChange(details) { // details => { open: boolean } console.log("floating panel is:", details.open ? "opened" : "closed") }, })
Position Change
When the position of the floating panel changes, these callbacks are invoked:
onPositionChange— When the position of the floating panel changes.onPositionChangeEnd— When the position of the floating panel changes ends.
const service = useMachine(floatingPanel.machine, { onPositionChange(details) { // details => { position: { x: number, y: number } } console.log("floating panel is:", details.position.x, details.position.y) }, onPositionChangeEnd(details) { // details => { position: { x: number, y: number } } console.log("floating panel is:", details.position.x, details.position.y) }, })
Resize
When the size of the floating panel changes, these callbacks are invoked:
onResize— When the size of the floating panel changes.onResizeEnd— When the size of the floating panel changes ends.
const service = useMachine(floatingPanel.machine, { onSizeChange(details) { // details => { size: { width: number, height: number } } console.log("floating panel is:", details.size.width, details.size.height) }, onSizeChangeEnd(details) { // details => { size: { width: number, height: number } } console.log("floating panel is:", details.size.width, details.size.height) }, })
Minimizing and Maximizing
The floating panel can be minimized, default, and maximized by clicking the
respective buttons in the header. We refer to this as the panel's stage.
-
When the panel is minimized, the body is hidden and the panel is resized to a minimum size.
-
When the panel is maximized, the panel scales to the match the size of the defined boundary rect (via
getBoundaryElprop). -
When the panel is restored, the panel is resized back to the previously known size.
When the stage changes, the onStageChange callback is invoked.
const service = useMachine(floatingPanel.machine, { onStageChange(details) { // details => { stage: "minimized" | "maximized" | "default" } console.log("floating panel is:", details.stage) }, })
Styling guide
The floating panel component uses data attributes to style its various parts.
Each part has a data-scope="floating-panel" and data-part attribute that you
can use to target specific elements.
[data-scope="floating-panel"][data-part="content"] { /* Add styles for the main panel container */ } [data-scope="floating-panel"][data-part="body"] { /* Add styles for the panel's content area */ } [data-scope="floating-panel"][data-part="header"] { /* Add styles for the panel's header */ } [data-scope="floating-panel"][data-part="stage-trigger"] { /* Add styles for state buttons in the header */ } [data-scope="floating-panel"][data-part="resize-trigger"] { /* Add styles for resize handles */ } /* North and south resize handles */ [data-scope="floating-panel"][data-part="resize-trigger"][data-axis="n"], [data-scope="floating-panel"][data-part="resize-trigger"][data-axis="s"] { /* Add styles for north and south resize handles */ } /* East and west resize handles */ [data-scope="floating-panel"][data-part="resize-trigger"][data-axis="e"], [data-scope="floating-panel"][data-part="resize-trigger"][data-axis="w"] { /* Add styles for east and west resize handles */ } /* Corner resize handles */ [data-scope="floating-panel"][data-part="resize-trigger"][data-axis="ne"], [data-scope="floating-panel"][data-part="resize-trigger"][data-axis="nw"], [data-scope="floating-panel"][data-part="resize-trigger"][data-axis="se"], [data-scope="floating-panel"][data-part="resize-trigger"][data-axis="sw"] { /* Add styles for corner resize handles */ }
Dragging
When dragging the panel, the [data-dragging] attribute is applied to the
panel.
[data-scope="floating-panel"][data-part="content"][data-dragging] { /* Add styles for dragging state */ }
Stacking
The floating panel has several states that can be targeted using data attributes:
/* When the panel is the topmost element */ [data-scope="floating-panel"][data-part="content"][data-topmost] { /* Add styles for topmost state */ } /* When the panel is behind another panel */ [data-scope="floating-panel"][data-part="content"][data-behind] { /* Add styles for behind state */ }
Methods and Properties
Machine Context
The floating panel machine exposes the following context properties:
idsPartial<{ trigger: string; positioner: string; content: string; title: string; header: string; }>The ids of the elements in the floating panel. Useful for composition.translationsIntlTranslationsThe translations for the floating panel.strategy"absolute" | "fixed"The strategy to use for positioningallowOverflowbooleanWhether the panel should be strictly contained within the boundary when draggingopenbooleanThe controlled open state of the paneldefaultOpenbooleanThe initial open state of the panel when rendered. Use when you don't need to control the open state of the panel.draggablebooleanWhether the panel is draggableresizablebooleanWhether the panel is resizablesizeSizeThe size of the paneldefaultSizeSizeThe default size of the panelminSizeSizeThe minimum size of the panelmaxSizeSizeThe maximum size of the panelpositionPointThe controlled position of the paneldefaultPositionPointThe initial position of the panel when rendered. Use when you don't need to control the position of the panel.getAnchorPosition(details: AnchorPositionDetails) => PointFunction that returns the initial position of the panel when it is opened. If provided, will be used instead of the default position.lockAspectRatiobooleanWhether the panel is locked to its aspect ratiocloseOnEscapebooleanWhether the panel should close when the escape key is pressedgetBoundaryEl() => HTMLElementThe boundary of the panel. Useful for recalculating the boundary rect when the it is resized.disabledbooleanWhether the panel is disabledonPositionChange(details: PositionChangeDetails) => voidFunction called when the position of the panel changes via draggingonPositionChangeEnd(details: PositionChangeDetails) => voidFunction called when the position of the panel changes via dragging endsonOpenChange(details: OpenChangeDetails) => voidFunction called when the panel is opened or closedonSizeChange(details: SizeChangeDetails) => voidFunction called when the size of the panel changes via resizingonSizeChangeEnd(details: SizeChangeDetails) => voidFunction called when the size of the panel changes via resizing endspersistRectbooleanWhether the panel size and position should be preserved when it is closedgridSizenumberThe snap grid for the panelonStageChange(details: StageChangeDetails) => voidFunction called when the stage of the panel changesdir"ltr" | "rtl"The document's text/writing direction.idstringThe unique identifier of the machine.getRootNode() => Node | ShadowRoot | DocumentA root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.
Machine API
The floating panel api exposes the following methods:
openbooleanWhether the panel is opensetOpen(open: boolean) => voidFunction to open or close the paneldraggingbooleanWhether the panel is being draggedresizingbooleanWhether the panel is being resizedpositionPointThe position of the panelsetPosition(position: Point) => voidFunction to set the position of the panelsizeSizeThe size of the panelsetSize(size: Size) => voidFunction to set the size of the panelminimizeVoidFunctionFunction to minimize the panelmaximizeVoidFunctionFunction to maximize the panelrestoreVoidFunctionFunction to restore the panel before it was minimized or maximizedresizablebooleanWhether the panel is resizabledraggablebooleanWhether the panel is draggable
Data Attributes
Edit this page on GitHub