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
variables
or 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-part
attribute 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
getBoundaryEl
prop). -
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:
ids
Partial<{ trigger: string; positioner: string; content: string; title: string; header: string; }>
The ids of the elements in the floating panel. Useful for composition.translations
IntlTranslations
The translations for the floating panel.strategy
"absolute" | "fixed"
The strategy to use for positioningallowOverflow
boolean
Whether the panel should be strictly contained within the boundary when draggingopen
boolean
The controlled open state of the paneldefaultOpen
boolean
The initial open state of the panel when rendered. Use when you don't need to control the open state of the panel.draggable
boolean
Whether the panel is draggableresizable
boolean
Whether the panel is resizablesize
Size
The size of the paneldefaultSize
Size
The default size of the panelminSize
Size
The minimum size of the panelmaxSize
Size
The maximum size of the panelposition
Point
The controlled position of the paneldefaultPosition
Point
The initial position of the panel when rendered. Use when you don't need to control the position of the panel.getAnchorPosition
(details: AnchorPositionDetails) => Point
Function that returns the initial position of the panel when it is opened. If provided, will be used instead of the default position.lockAspectRatio
boolean
Whether the panel is locked to its aspect ratiocloseOnEscape
boolean
Whether the panel should close when the escape key is pressedgetBoundaryEl
() => HTMLElement
The boundary of the panel. Useful for recalculating the boundary rect when the it is resized.disabled
boolean
Whether the panel is disabledonPositionChange
(details: PositionChangeDetails) => void
Function called when the position of the panel changes via draggingonPositionChangeEnd
(details: PositionChangeDetails) => void
Function called when the position of the panel changes via dragging endsonOpenChange
(details: OpenChangeDetails) => void
Function called when the panel is opened or closedonSizeChange
(details: SizeChangeDetails) => void
Function called when the size of the panel changes via resizingonSizeChangeEnd
(details: SizeChangeDetails) => void
Function called when the size of the panel changes via resizing endspersistRect
boolean
Whether the panel size and position should be preserved when it is closedgridSize
number
The snap grid for the panelonStageChange
(details: StageChangeDetails) => void
Function called when the stage of the panel changesdir
"ltr" | "rtl"
The document's text/writing direction.id
string
The unique identifier of the machine.getRootNode
() => Node | ShadowRoot | Document
A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.
Machine API
The floating panel api
exposes the following methods:
open
boolean
Whether the panel is opensetOpen
(open: boolean) => void
Function to open or close the paneldragging
boolean
Whether the panel is being draggedresizing
boolean
Whether the panel is being resizedposition
Point
The position of the panelsetPosition
(position: Point) => void
Function to set the position of the panelsize
Size
The size of the panelsetSize
(size: Size) => void
Function to set the size of the panelminimize
() => void
Function to minimize the panelmaximize
() => void
Function to maximize the panelrestore
() => void
Function to restore the panel before it was minimized or maximizedresizable
boolean
Whether the panel is resizabledraggable
boolean
Whether the panel is draggable
Data Attributes
Edit this page on GitHub