Popover Side Panel Hybrid
Contextual surface that floats as a popover or docks as a side panel, bridging Popover and Panel.
Overview
PopoverSidePanelHybrid is a compound primitive for contextual tool panels that can present as either a floating popover or a docked side panel.
It sits on the escalation ladder between Popover and Panel:
- lighter than a full
Paneldrawer - more persistent and spacious than a compact
Popover - ideal for builder tools, analytics sidecars, chat assistants, and compliance inspectors
On viewports below lg, the surface always presents as a floating panel regardless of the stored dock preference. On wider screens, users can toggle between docked and floating modes.
Compound API
| Part | Purpose |
|---|---|
PopoverSidePanelHybrid.Root | Root container with open, docked, and title state. |
PopoverSidePanelHybrid.Trigger | Clones a single child element to toggle open state. |
PopoverSidePanelHybrid.Dock | Docked side panel slot with animated width. |
PopoverSidePanelHybrid.Floating | Floating popover slot with backdrop. |
usePopoverSidePanelHybrid | Access open/docked state and actions inside the tree. |
Basic Usage
import { PopoverSidePanelHybrid } from '@fsai/shared-ui';
const [open, setOpen] = useState(false);
const [docked, setDocked] = useState(true);
<PopoverSidePanelHybrid.Root
open={open}
onOpenChange={setOpen}
docked={docked}
onDockedChange={setDocked}
title="Customize"
>
<div className="flex h-screen">
<main className="flex-1">{/* main workspace */}</main>
<PopoverSidePanelHybrid.Dock width={360} className="border-l border-default">
<PanelContent />
</PopoverSidePanelHybrid.Dock>
</div>
<PopoverSidePanelHybrid.Floating className="max-h-[calc(100vh-5rem)] w-[380px]">
<PanelContent />
</PopoverSidePanelHybrid.Floating>
</PopoverSidePanelHybrid.Root>Render the same panel content in both Dock and Floating. The hybrid switches presentation based on docked and viewport width.
With Toolbar Triggers
In builder-style UIs, triggers are often a row of IconButtons in the header rather than PopoverSidePanelHybrid.Trigger:
<IconButton
icon="Palette"
label="Customize"
size="sm"
variant="ghost"
isActive={activePanel === 'customize'}
onClick={() =>
setActivePanel((current) =>
current === 'customize' ? null : 'customize'
)
}
/>Drive open from whether activePanel is set.
Props
PopoverSidePanelHybrid.Root
| Prop | Type | Description |
|---|---|---|
open | boolean | Required. Whether the panel is open. |
onOpenChange | (open: boolean) => void | Required. Called when open state changes. |
docked | boolean | Required. Whether the panel prefers docked mode. |
onDockedChange | (docked: boolean) => void | Required. Called when dock preference changes. |
title | ReactNode | Required. Title shown in the panel header. |
showDockButton | boolean | Show the dock/pop-out toggle in the header. Defaults to true. |
children | ReactNode | Required. Dock and floating slots. |
PopoverSidePanelHybrid.Trigger
| Prop | Type | Description |
|---|---|---|
children | ReactElement | Required. Single trigger element. Receives onClick, aria-pressed, aria-expanded, and isActive. |
PopoverSidePanelHybrid.Dock
| Prop | Type | Description |
|---|---|---|
children | ReactNode | Required. Panel content. |
width | number | Required. Target dock width in pixels. |
className | string | Additional classes on the dock container. |
resizeHandle | ReactNode | Optional resize handle rendered on the dock edge. |
PopoverSidePanelHybrid.Floating
| Prop | Type | Description |
|---|---|---|
children | ReactNode | Required. Panel content. |
className | string | Additional classes on the floating dialog. |
backdropClassName | string | Classes for the transparent backdrop overlay. |
Surface Header
Both dock and floating modes share a built-in header with:
- the
titlepassed toRoot - a dock/pop-out toggle button (visible on
lg+viewports, unless disabled viashowDockButton={false}) - a close button
Users can switch between docked and floating without closing the panel.
Important Conventions
Rootmust wrap both the main layout (containingDock) and theFloatingslot.- Below
lg,dockedis ignored and the panel always floats. Dockanimates width open and closed; content opacity fades in with the expansion.Floatingrenders a backdrop below the header (top-[3.5rem]) so the main toolbar stays interactive.- Do not nest another major overlay (
ModalorPanel) on top of an open hybrid panel.
Brand Dashboard And Shared Usage
| Pattern | Path | Notes |
|---|---|---|
| Funnel builder panels | fsai/apps/brand-dashboard/src/pages/FunnelBuilder/FunnelBuilder.tsx | Canonical builder usage with header icon triggers and docked side panel. |
| Post composer sidecar | fsai/packages/shared-ui/src/modules/socials/components/PostSets/PostComposer/PostComposer.tsx | Analytics and tooling panel in social post editing. |
Guidelines
- Do use this when a contextual tool should stay near the workspace without forcing full page navigation
- Do render the same content in both
DockandFloatingslots - Do persist the user's dock preference with
docked/onDockedChange - Don't use this for simple confirmations or short forms — use
ModalorPopover - Don't use this when the task needs a full
Paneldrawer with topbar, breadcrumbs, and stack management