'use client'
import { Button, FloatingPanel, IconButton, Portal } from '@chakra-ui/react'
import {
LuGripHorizontal,
LuMaximize2,
LuMinus,
LuSquare,
LuX,
} from 'react-icons/lu'
export const FloatingPanelBasic = () => {
return (
<FloatingPanel.Root>
<FloatingPanel.Trigger asChild>
<Button variant="outline" size="sm">
Open Panel
</Button>
</FloatingPanel.Trigger>
<Portal>
<FloatingPanel.Positioner>
<FloatingPanel.Content>
<FloatingPanel.Header>
<FloatingPanel.DragTrigger>
<LuGripHorizontal />
<FloatingPanel.Title>Floating Panel</FloatingPanel.Title>
</FloatingPanel.DragTrigger>
<FloatingPanel.Control>
<FloatingPanel.StageTrigger stage="minimized" asChild>
<IconButton variant="ghost" size="2xs">
<LuMinus />
</IconButton>
</FloatingPanel.StageTrigger>
<FloatingPanel.StageTrigger stage="maximized" asChild>
<IconButton variant="ghost" size="2xs">
<LuSquare />
</IconButton>
</FloatingPanel.StageTrigger>
<FloatingPanel.StageTrigger stage="default" asChild>
<IconButton variant="ghost" size="2xs">
<LuMaximize2 />
</IconButton>
</FloatingPanel.StageTrigger>
<FloatingPanel.CloseTrigger asChild>
<IconButton variant="ghost" size="2xs">
<LuX />
</IconButton>
</FloatingPanel.CloseTrigger>
</FloatingPanel.Control>
</FloatingPanel.Header>
<FloatingPanel.Body>
<p>Drag the header to move this panel around.</p>
</FloatingPanel.Body>
<FloatingPanel.ResizeTriggers />
</FloatingPanel.Content>
</FloatingPanel.Positioner>
</Portal>
</FloatingPanel.Root>
)
}
Anatomy
import { FloatingPanel } from '@chakra-ui/react'<FloatingPanel.Root>
<FloatingPanel.Trigger />
<FloatingPanel.Positioner>
<FloatingPanel.Content>
<FloatingPanel.Header>
<FloatingPanel.DragTrigger>
<FloatingPanel.Title />
</FloatingPanel.DragTrigger>
<FloatingPanel.Control>
<FloatingPanel.StageTrigger />
<FloatingPanel.CloseTrigger />
</FloatingPanel.Control>
</FloatingPanel.Header>
<FloatingPanel.Body />
<FloatingPanel.ResizeTriggers />
</FloatingPanel.Content>
</FloatingPanel.Positioner>
</FloatingPanel.Root>FloatingPanel.ResizeTriggers renders a FloatingPanel.ResizeTrigger for each
resize axis (n, s, e, w, ne, nw, se, sw). Pass the axes prop
to render only specific handles.
Examples
Controlled Open
Use the open and onOpenChange props to control the visibility of the panel.
'use client'
import { useState } from 'react'
import {
Button,
FloatingPanel,
HStack,
IconButton,
Portal,
} from '@chakra-ui/react'
import { LuGripHorizontal, LuX } from 'react-icons/lu'
export const FloatingPanelControlledOpen = () => {
const [open, setOpen] = useState(false)
return (
<FloatingPanel.Root
open={open}
onOpenChange={(details) => setOpen(details.open)}
persistRect
defaultSize={{ width: 320, height: 200 }}
minSize={{ width: 320, height: 200 }}
>
<HStack>
<FloatingPanel.Trigger asChild>
<Button variant="outline" size="sm">
{open ? 'Close Panel' : 'Open Panel'}
</Button>
</FloatingPanel.Trigger>
</HStack>
<Portal>
<FloatingPanel.Positioner>
<FloatingPanel.Content>
<FloatingPanel.Header>
<FloatingPanel.DragTrigger>
<LuGripHorizontal />
<FloatingPanel.Title>Controlled Open</FloatingPanel.Title>
</FloatingPanel.DragTrigger>
<FloatingPanel.Control>
<FloatingPanel.CloseTrigger asChild>
<IconButton variant="ghost" size="2xs">
<LuX />
</IconButton>
</FloatingPanel.CloseTrigger>
</FloatingPanel.Control>
</FloatingPanel.Header>
<FloatingPanel.Body>
The open state is fully controlled externally. Closing via the
close button or the trigger both update the same state.
</FloatingPanel.Body>
<FloatingPanel.ResizeTriggers />
</FloatingPanel.Content>
</FloatingPanel.Positioner>
</Portal>
</FloatingPanel.Root>
)
}
Stages
Use the stage prop on FloatingPanel.StageTrigger to add buttons that
minimize, maximize and restore the panel. Here are the available stages:
default— restore the panel to its normal size and positionminimized— collapse the panel to a compact height (header only)maximized— expand the panel to fill the available boundary
<FloatingPanel.StageTrigger stage="minimized" />'use client'
import {
Button,
FloatingPanel,
IconButton,
Portal,
Text,
} from '@chakra-ui/react'
import {
LuGripHorizontal,
LuMaximize2,
LuMinus,
LuSquare,
LuX,
} from 'react-icons/lu'
export const FloatingPanelStages = () => {
return (
<FloatingPanel.Root
persistRect
defaultSize={{ width: 320, height: 220 }}
minSize={{ width: 280, height: 160 }}
>
<FloatingPanel.Trigger asChild>
<Button variant="outline" size="sm">
Open Panel
</Button>
</FloatingPanel.Trigger>
<Portal>
<FloatingPanel.Positioner>
<FloatingPanel.Content>
<FloatingPanel.Header>
<FloatingPanel.DragTrigger>
<LuGripHorizontal />
<FloatingPanel.Title>Stages</FloatingPanel.Title>
</FloatingPanel.DragTrigger>
<FloatingPanel.Control>
<FloatingPanel.StageTrigger stage="minimized" asChild>
<IconButton variant="ghost" size="2xs">
<LuMinus />
</IconButton>
</FloatingPanel.StageTrigger>
<FloatingPanel.StageTrigger stage="maximized" asChild>
<IconButton variant="ghost" size="2xs">
<LuSquare />
</IconButton>
</FloatingPanel.StageTrigger>
<FloatingPanel.StageTrigger stage="default" asChild>
<IconButton variant="ghost" size="2xs">
<LuMaximize2 />
</IconButton>
</FloatingPanel.StageTrigger>
<FloatingPanel.CloseTrigger asChild>
<IconButton variant="ghost" size="2xs">
<LuX />
</IconButton>
</FloatingPanel.CloseTrigger>
</FloatingPanel.Control>
</FloatingPanel.Header>
<FloatingPanel.Body>
<Text textStyle="sm">
Use the header controls to minimize, maximize, or restore the
panel.
</Text>
</FloatingPanel.Body>
<FloatingPanel.ResizeTriggers />
</FloatingPanel.Content>
</FloatingPanel.Positioner>
</Portal>
</FloatingPanel.Root>
)
}
Context
Use useFloatingPanelContext inside the panel to call minimize, maximize,
restore, setPosition, and setSize programmatically.
'use client'
import {
Button,
FloatingPanel,
HStack,
IconButton,
Portal,
Text,
useFloatingPanelContext,
} from '@chakra-ui/react'
import { LuGripHorizontal, LuX } from 'react-icons/lu'
const PanelActions = () => {
const api = useFloatingPanelContext()
return (
<HStack gap="2">
<Button size="sm" variant="outline" onClick={() => api.minimize()}>
Minimize
</Button>
<Button size="sm" variant="outline" onClick={() => api.maximize()}>
Maximize
</Button>
<Button size="sm" variant="outline" onClick={() => api.restore()}>
Restore
</Button>
</HStack>
)
}
export const FloatingPanelContext = () => {
return (
<FloatingPanel.Root
defaultSize={{ width: 320, height: 200 }}
minSize={{ width: 280, height: 160 }}
>
<FloatingPanel.Trigger asChild>
<Button variant="outline" size="sm">
Open Panel
</Button>
</FloatingPanel.Trigger>
<Portal>
<FloatingPanel.Positioner>
<FloatingPanel.Content>
<FloatingPanel.Header>
<FloatingPanel.DragTrigger>
<LuGripHorizontal />
<FloatingPanel.Title>Context</FloatingPanel.Title>
</FloatingPanel.DragTrigger>
<FloatingPanel.Control>
<FloatingPanel.CloseTrigger asChild>
<IconButton variant="ghost" size="2xs">
<LuX />
</IconButton>
</FloatingPanel.CloseTrigger>
</FloatingPanel.Control>
</FloatingPanel.Header>
<FloatingPanel.Body>
<Text textStyle="sm" mb="3">
Control the panel from inside the body using{' '}
<code>useFloatingPanelContext</code>.
</Text>
<PanelActions />
</FloatingPanel.Body>
<FloatingPanel.ResizeTriggers />
</FloatingPanel.Content>
</FloatingPanel.Positioner>
</Portal>
</FloatingPanel.Root>
)
}
Multiple
Render multiple FloatingPanel.Root instances to show several panels at once.
Each root manages its own open state, position, and size independently.
'use client'
import {
Button,
FloatingPanel,
HStack,
IconButton,
Portal,
Text,
} from '@chakra-ui/react'
import { LuGripHorizontal, LuX } from 'react-icons/lu'
const panels = [
{
id: 'notes',
label: 'Notes',
title: 'Notes',
defaultPosition: { x: 80, y: 80 },
description: 'Jot down quick ideas and reminders.',
},
{
id: 'tasks',
label: 'Tasks',
title: 'Tasks',
defaultPosition: { x: 260, y: 120 },
description: 'Track items you want to finish today.',
},
{
id: 'chat',
label: 'Chat',
title: 'Chat',
defaultPosition: { x: 440, y: 160 },
description: 'Keep a lightweight conversation panel open.',
},
] as const
export const FloatingPanelMultiple = () => {
return (
<HStack gap="2" flexWrap="wrap">
{panels.map((panel) => (
<FloatingPanel.Root
key={panel.id}
id={panel.id}
persistRect
defaultPosition={panel.defaultPosition}
defaultSize={{ width: 280, height: 200 }}
minSize={{ width: 240, height: 160 }}
>
<FloatingPanel.Trigger asChild>
<Button variant="outline" size="sm">
{panel.label}
</Button>
</FloatingPanel.Trigger>
<Portal>
<FloatingPanel.Positioner>
<FloatingPanel.Content>
<FloatingPanel.Header>
<FloatingPanel.DragTrigger>
<LuGripHorizontal />
<FloatingPanel.Title>{panel.title}</FloatingPanel.Title>
</FloatingPanel.DragTrigger>
<FloatingPanel.Control>
<FloatingPanel.CloseTrigger asChild>
<IconButton variant="ghost" size="2xs">
<LuX />
</IconButton>
</FloatingPanel.CloseTrigger>
</FloatingPanel.Control>
</FloatingPanel.Header>
<FloatingPanel.Body>
<Text textStyle="sm">{panel.description}</Text>
</FloatingPanel.Body>
<FloatingPanel.ResizeTriggers />
</FloatingPanel.Content>
</FloatingPanel.Positioner>
</Portal>
</FloatingPanel.Root>
))}
</HStack>
)
}
Resize Axes
Pass the axes prop to FloatingPanel.ResizeTriggers to limit which resize
handles are rendered.
'use client'
import {
Button,
FloatingPanel,
IconButton,
Portal,
Text,
} from '@chakra-ui/react'
import { LuGripHorizontal, LuX } from 'react-icons/lu'
export const FloatingPanelResizeAxes = () => {
return (
<FloatingPanel.Root
defaultSize={{ width: 320, height: 200 }}
minSize={{ width: 240, height: 160 }}
>
<FloatingPanel.Trigger asChild>
<Button variant="outline" size="sm">
Open Panel
</Button>
</FloatingPanel.Trigger>
<Portal>
<FloatingPanel.Positioner>
<FloatingPanel.Content>
<FloatingPanel.Header>
<FloatingPanel.DragTrigger>
<LuGripHorizontal />
<FloatingPanel.Title>Resize Axes</FloatingPanel.Title>
</FloatingPanel.DragTrigger>
<FloatingPanel.Control>
<FloatingPanel.CloseTrigger asChild>
<IconButton variant="ghost" size="2xs">
<LuX />
</IconButton>
</FloatingPanel.CloseTrigger>
</FloatingPanel.Control>
</FloatingPanel.Header>
<FloatingPanel.Body>
<Text textStyle="sm">
Only the south, east, and southeast handles are enabled.
</Text>
</FloatingPanel.Body>
<FloatingPanel.ResizeTriggers axes={['s', 'e', 'se']} />
</FloatingPanel.Content>
</FloatingPanel.Positioner>
</Portal>
</FloatingPanel.Root>
)
}
Min/Max
Use the minSize and maxSize props to constrain how small or large the panel
can be resized.
'use client'
import {
Button,
FloatingPanel,
HStack,
IconButton,
Portal,
Span,
Stack,
Text,
} from '@chakra-ui/react'
import {
LuGripHorizontal,
LuMouse,
LuMoveHorizontal,
LuX,
} from 'react-icons/lu'
export const FloatingPanelMinMax = () => {
return (
<Stack gap="4" align="flex-start">
<HStack textStyle="sm" gap="2">
<LuMouse />
<LuMoveHorizontal />
<Span>
Drag the resize handles — size is clamped between min and max.
</Span>
</HStack>
<FloatingPanel.Root
defaultSize={{ width: 320, height: 220 }}
minSize={{ width: 280, height: 160 }}
maxSize={{ width: 480, height: 320 }}
>
<FloatingPanel.Trigger asChild>
<Button variant="outline" size="sm">
Open Panel
</Button>
</FloatingPanel.Trigger>
<Portal>
<FloatingPanel.Positioner>
<FloatingPanel.Content>
<FloatingPanel.Header>
<FloatingPanel.DragTrigger>
<LuGripHorizontal />
<FloatingPanel.Title>Min/Max</FloatingPanel.Title>
</FloatingPanel.DragTrigger>
<FloatingPanel.Control>
<FloatingPanel.CloseTrigger asChild>
<IconButton variant="ghost" size="2xs">
<LuX />
</IconButton>
</FloatingPanel.CloseTrigger>
</FloatingPanel.Control>
</FloatingPanel.Header>
<FloatingPanel.Body>
<Text textStyle="sm">
Resize the panel with the resize handles. Size is constrained
between 280x160px (min) and 480x320px (max).
</Text>
</FloatingPanel.Body>
<FloatingPanel.ResizeTriggers />
</FloatingPanel.Content>
</FloatingPanel.Positioner>
</Portal>
</FloatingPanel.Root>
</Stack>
)
}
Boundary
Use getBoundaryEl with allowOverflow={false} to confine the panel within a
specific container.
Drag boundary
'use client'
import { useRef } from 'react'
import {
Box,
Button,
FloatingPanel,
IconButton,
Portal,
Text,
} from '@chakra-ui/react'
import { LuGripHorizontal, LuX } from 'react-icons/lu'
export const FloatingPanelBoundary = () => {
const boundaryRef = useRef<HTMLDivElement>(null)
return (
<Box
ref={boundaryRef}
position="relative"
borderWidth="2px"
borderStyle="dashed"
borderColor="border.emphasized"
borderRadius="l2"
w="full"
h="400px"
p="4"
>
<Text textStyle="xs" color="fg.muted" mb="2">
Drag boundary
</Text>
<FloatingPanel.Root
persistRect
allowOverflow={false}
getBoundaryEl={() => boundaryRef.current}
defaultSize={{ width: 280, height: 180 }}
minSize={{ width: 280, height: 180 }}
>
<FloatingPanel.Trigger asChild>
<Button variant="outline" size="sm">
Open Panel
</Button>
</FloatingPanel.Trigger>
<Portal>
<FloatingPanel.Positioner>
<FloatingPanel.Content>
<FloatingPanel.Header>
<FloatingPanel.DragTrigger>
<LuGripHorizontal />
<FloatingPanel.Title>Bounded Panel</FloatingPanel.Title>
</FloatingPanel.DragTrigger>
<FloatingPanel.Control>
<FloatingPanel.CloseTrigger asChild>
<IconButton variant="ghost" size="2xs">
<LuX />
</IconButton>
</FloatingPanel.CloseTrigger>
</FloatingPanel.Control>
</FloatingPanel.Header>
<FloatingPanel.Body>
<Text textStyle="sm">
This panel cannot be dragged outside the dashed boundary box.
</Text>
</FloatingPanel.Body>
<FloatingPanel.ResizeTriggers />
</FloatingPanel.Content>
</FloatingPanel.Positioner>
</Portal>
</FloatingPanel.Root>
</Box>
)
}
Props
Root
| Prop | Default | Type |
|---|---|---|
allowOverflow | true | booleanWhether the panel should be strictly contained within the boundary when dragging |
defaultOpen | false | booleanThe initial open state of the panel when rendered. Use when you don't need to control the open state of the panel. |
dir | '\'ltr\'' | 'ltr' | 'rtl'The document's text/writing direction. |
draggable | true | booleanWhether the panel is draggable |
gridSize | '1' | numberThe snap grid for the panel |
hideMode | ''display-none'' | HideModeHow to hide content when mounted but not present. - `'display-none'`: HTML `hidden` attribute. Effects stay alive. - `'activity'`: React 19 `<Activity mode="hidden">`. Effects pause. Requires React 19+. |
lazyMount | false | booleanWhether to enable lazy mounting |
resizable | true | booleanWhether the panel is resizable |
restoreFocus | true | booleanWhether to restore focus to the trigger when the panel is closed. |
skipAnimationOnMount | false | booleanWhether to allow the initial presence animation. |
strategy | '\'fixed\'' | 'absolute' | 'fixed'The strategy to use for positioning |
unmountOnExit | false | booleanWhether to unmount on exit. |
closeOnEscape | booleanWhether the panel should close when the escape key is pressed | |
defaultPosition | PointThe initial position of the panel when rendered. Use when you don't need to control the position of the panel. | |
defaultSize | SizeThe default size of the panel | |
disabled | booleanWhether the panel is disabled | |
finalFocusEl | () => HTMLElement | nullElement to receive focus when the panel is closed. By default, the trigger element is focused. | |
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. | |
getBoundaryEl | () => HTMLElement | nullThe boundary of the panel. Useful for recalculating the boundary rect when the it is resized. | |
id | stringThe unique identifier of the machine. | |
ids | Partial<{ trigger: string; positioner: string; content: string; title: string; header: string }>The ids of the elements in the floating panel. Useful for composition. | |
immediate | booleanWhether to synchronize the present change immediately or defer it to the next frame | |
initialFocusEl | () => HTMLElement | nullElement to receive focus when the panel is opened. By default, the first focusable element in the content is focused. | |
lockAspectRatio | booleanWhether the panel is locked to its aspect ratio | |
maxSize | SizeThe maximum size of the panel | |
minSize | SizeThe minimum size of the panel | |
onExitComplete | VoidFunctionFunction called when the animation ends in the closed state | |
onOpenChange | (details: OpenChangeDetails) => voidFunction called when the panel is opened or closed | |
onPositionChange | (details: PositionChangeDetails) => voidFunction called when the position of the panel changes via dragging | |
onPositionChangeEnd | (details: PositionChangeDetails) => voidFunction called when the position of the panel changes via dragging ends | |
onSizeChange | (details: SizeChangeDetails) => voidFunction called when the size of the panel changes via resizing | |
onSizeChangeEnd | (details: SizeChangeDetails) => voidFunction called when the size of the panel changes via resizing ends | |
onStageChange | (details: StageChangeDetails) => voidFunction called when the stage of the panel changes | |
open | booleanThe controlled open state of the panel | |
persistRect | booleanWhether the panel size and position should be preserved when it is closed | |
position | PointThe controlled position of the panel | |
present | booleanWhether the node is present (controlled by the user) | |
size | SizeThe size of the panel | |
translations | IntlTranslationsThe translations for the floating panel. |