Popover
A floating panel for interactive content — filters, small forms and settings.
'use client';
import { Button, Input, Popover } from '@akhatmirzo/ui';
import { Funnel } from 'lucide-react';
import { useState } from 'react';
export default function PopoverFilter() {
const [open, setOpen] = useState(false);
const [range, setRange] = useState({ from: '', to: '' });
const [applied, setApplied] = useState<string | null>(null);
return (
<div className="flex flex-wrap items-center gap-3">
<Popover open={open} onOpenChange={setOpen}>
<Popover.Trigger asChild>
<Button leftIcon={<Funnel />}>Narx</Button>
</Popover.Trigger>
<Popover.Content align="start" aria-label="Narx filtri">
<form
className="flex flex-col gap-3"
onSubmit={(e) => {
e.preventDefault();
setApplied(`${range.from || '0'} – ${range.to || '∞'} so'm`);
setOpen(false);
}}
>
<p className="m-0 font-semibold">Narx oralig'i</p>
<Input
size="sm"
inputMode="numeric"
placeholder="dan"
suffix="so'm"
aria-label="Narx: dan"
value={range.from}
onChange={(e) => setRange((r) => ({ ...r, from: e.target.value }))}
/>
<Input
size="sm"
inputMode="numeric"
placeholder="gacha"
suffix="so'm"
aria-label="Narx: gacha"
value={range.to}
onChange={(e) => setRange((r) => ({ ...r, to: e.target.value }))}
/>
<div className="mt-1 flex justify-end gap-2">
<Popover.Close asChild>
<Button size="sm">Bekor</Button>
</Popover.Close>
<Button size="sm" variant="primary" type="submit">
Qo'llash
</Button>
</div>
</form>
</Popover.Content>
</Popover>
<span className="text-sm text-text-muted">{applied ?? "Filtr qo'llanmagan"}</span>
</div>
);
}
Import
import { Popover } from '@akhatmirzo/ui';When to use
- Small interactive content — a price filter, a color picker, a couple of settings. The page stays usable and the panel opens next to its trigger.
- Text-only hints are a job for
Tooltip. A Popover opens on click and can hold buttons and fields. - A list of commands ("Edit", "Delete") belongs in a
DropdownMenu, which comes with menu roles and keyboard navigation. - Large forms or tasks that need full attention — use a
Dialog.
Anatomy
| Part | What it does |
|---|---|
Popover | State: open, defaultOpen, onOpenChange |
Popover.Trigger | Opens the panel (use with asChild) |
Popover.Content | The panel: portal, positioning, animation, optional arrow |
Popover.Close | Closes the panel when clicked (use with asChild) |
Popover.Anchor | Positions the panel against another element than the trigger |
<Popover>
<Popover.Trigger asChild>
<Button>Price</Button>
</Popover.Trigger>
<Popover.Content align="start" aria-label="Price filter">
{/* form */}
</Popover.Content>
</Popover>The panel is 288px wide by default (w-72). Pass a className such as w-56 or w-auto to
change it.
Placement and arrow
side is top, right, bottom (default) or left; align is start, center (default) or
end. The panel sits 6px from the trigger and flips to the opposite side when it hits the edge
of the screen. arrow draws an arrow pointing at the trigger.
'use client';
import { Button, Popover } from '@akhatmirzo/ui';
export default function PopoverPlacement() {
return (
<div className="flex flex-wrap items-center gap-3">
{(['top', 'right', 'bottom', 'left'] as const).map((side) => (
<Popover key={side}>
<Popover.Trigger asChild>
<Button variant="outline">{side}</Button>
</Popover.Trigger>
<Popover.Content side={side} arrow className="w-56" aria-label={`Tomon: ${side}`}>
<p className="m-0 text-text-muted">
Panel <b className="text-text">{side}</b> tomonda, triggerdan 6px uzoqlikda.
</p>
</Popover.Content>
</Popover>
))}
<Popover>
<Popover.Trigger asChild>
<Button variant="outline">align=end</Button>
</Popover.Trigger>
<Popover.Content align="end" className="w-56" aria-label="O'ngga tekislangan">
<p className="m-0 text-text-muted">Panel triggerning o'ng chetiga tekislangan.</p>
</Popover.Content>
</Popover>
</div>
);
}
Settings panel
An icon button with a few Switch controls — a typical small settings panel. When it opens,
focus moves to the first element inside.
'use client';
import { Field, IconButton, Popover, Switch } from '@akhatmirzo/ui';
import { Bell } from 'lucide-react';
export default function PopoverSettings() {
return (
<Popover>
<Popover.Trigger asChild>
<IconButton variant="outline" aria-label="Bildirishnoma sozlamalari">
<Bell />
</IconButton>
</Popover.Trigger>
<Popover.Content side="right" align="start" arrow aria-label="Bildirishnomalar">
<div className="flex flex-col gap-3">
<p className="m-0 font-semibold">Bildirishnomalar</p>
<Field label="Email orqali" orientation="horizontal">
<Switch defaultChecked />
</Field>
<Field label="SMS orqali" orientation="horizontal">
<Switch />
</Field>
<Field label="Telegram bot" orientation="horizontal">
<Switch defaultChecked />
</Field>
</div>
</Popover.Content>
</Popover>
);
}
Close on pick
Anything wrapped in Popover.Close asChild closes the panel when clicked — handy for color or
option pickers. In controlled mode (open + onOpenChange) you can also close it from code,
which is how the form in the first example works.
'use client';
import { Button, Popover } from '@akhatmirzo/ui';
import { useState } from 'react';
const colors = [
{ name: 'Asosiy', className: 'bg-primary' },
{ name: 'Yashil', className: 'bg-success' },
{ name: 'Sariq', className: 'bg-warning' },
{ name: 'Qizil', className: 'bg-danger' },
{ name: "Ko'k", className: 'bg-info' },
];
export default function PopoverColorPicker() {
const [color, setColor] = useState(colors[0]!);
return (
<Popover>
<Popover.Trigger asChild>
<Button
variant="outline"
leftIcon={<span className={`size-3 rounded-full ${color.className}`} />}
>
Teg rangi
</Button>
</Popover.Trigger>
<Popover.Content className="w-auto" aria-label="Rang tanlash">
<div className="flex gap-2">
{colors.map((c) => (
<Popover.Close key={c.name} asChild>
<button
type="button"
aria-label={c.name}
aria-pressed={c.name === color.name}
onClick={() => setColor(c)}
className={`focus-ring size-7 cursor-pointer rounded-full border-2 border-surface-raised ${c.className} aria-pressed:ring-2 aria-pressed:ring-primary`}
/>
</Popover.Close>
))}
</div>
</Popover.Content>
</Popover>
);
}
Inside a dialog
A Popover inside a Dialog or Drawer moves to the modal layer and opens above it — no extra
setup.
Props
Popover
Accepts the props of the Radix Popover Root.
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | Whether it is open (controlled) |
defaultOpen | boolean | — | Open on first render |
onOpenChange | (open: boolean) => void | — | Called when it opens or closes |
modal | boolean | false | true — blocks interaction with the rest of the page |
Popover.Content
Also accepts the rest of the Radix Popover.Content props (onOpenAutoFocus,
onEscapeKeyDown, alignOffset, avoidCollisions ...), <div> attributes and ref.
| Prop | Type | Default | Description |
|---|---|---|---|
side | 'top' | 'right' | 'bottom' | 'left' | 'bottom' | Preferred side |
align | 'start' | 'center' | 'end' | 'center' | Alignment along the trigger |
sideOffset | number | 6 | Gap to the trigger, px |
collisionPadding | number | 8 | Minimum distance from the screen edge, px |
arrow | boolean | false | Arrow pointing at the trigger |
container | HTMLElement | null | — | Portal container (defaults to document.body) |
Popover.Trigger, Popover.Close, Popover.Anchor
All three accept asChild. With a Popover.Anchor, the panel is positioned against that
element instead of the trigger (a whole table row, for example).
Data attributes
| Attribute | On | Value |
|---|---|---|
data-state | trigger, panel | open · closed |
data-side | panel | top · right · bottom · left |
data-align | panel | start · center · end |
Keyboard
| Key | Action |
|---|---|
Enter / Space | Opens or closes the panel from the trigger |
Tab | Moves between elements inside the panel |
Esc | Closes the panel; focus returns to the trigger |
The panel also closes when focus leaves it or the user clicks outside.
Accessibility
- Wiring. The trigger gets
aria-haspopup="dialog",aria-expandedandaria-controls; the panel isrole="dialog". Give the panel anaria-label(for example "Price filter") so screen readers announce it by name. - Focus. On open, focus moves to the first element inside the panel; on close, it returns to the trigger.
- Icon-only triggers need an
aria-labelon theIconButton. - The arrow is purely decorative.