Overlays
Modals, drawers, popovers, hover cards, tooltips, menus, and the command palette: every component that renders above the page.
Components in this category
Centered dialog overlay with backdrop.
ModalHeaderHeader area of a Modal.
ModalBodyScrollable body area of a Modal.
ModalFooterFooter area of a Modal, typically for actions.
DrawerSlide-in panel from any edge.
DrawerHeaderHeader area of a Drawer.
DrawerBodyScrollable body of a Drawer.
DrawerFooterFooter area of a Drawer.
AlertDialogModal confirmation dialog with tone and actions.
PopoverPanelViewport-aware positioned popover content.
PopoverHeaderHeader of a PopoverPanel.
PopoverBodyBody of a PopoverPanel.
PopoverFooterFooter of a PopoverPanel.
HoverCardContextual card shown on hover.
HoverCardHeaderHeader of a HoverCard.
HoverCardBodyBody content of a HoverCard.
HoverCardStatsStat row inside a HoverCard.
TooltipShort label shown on hover or focus.
DropdownMenuDropdown menu triggered by a button.
ContextMenuRight-click context menu.
ContextMenuAreaWrapper that enables right-click on its children.
MenuItemSingle item in a menu with icon, shortcut, submenu.
MenuDividerHorizontal divider between menu items.
CommandPaletteKeyboard-driven command palette (⌘K).
TourProduct tour with target highlights and step content.
SettingsModalNav-rail settings dialog with a titled, scrolling pane.
SettingsGroupTitled, framed block of settings rows inside a pane.
SettingsRowOne setting: label + description on the left, control on the right.
Modal
Modal is a centered dialog over a dimmed backdrop. It configures portal, backdrop, Escape key, scroll lock, and focus trap via the underlying Dialog primitive. Compose with ModalHeader, ModalBody, and ModalFooter.
Modal
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | Controls visibility. |
onClose | () => void | — | Escape / close button handler. |
width | number | 440 | Panel width in px. |
closeOnBackdrop | boolean | true | Set false for destructive confirms. |
ModalHeader
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | Modal title (the header renders its children as the title). |
sub | ReactNode | — | Dim sub-label after the title. |
onClose | () => void | — | Renders the close button. |
ModalBody
Scrollable body region. Accepts all standard div attributes.
ModalFooter
| Prop | Type | Default | Description |
|---|---|---|---|
left | ReactNode | — | Left-aligned content. |
children | ReactNode | — | Right-aligned action buttons. |
import {
Modal,
ModalHeader,
ModalBody,
ModalFooter,
Button,
} from "@solarius/onset";
import { useState } from "react";
function SettingsDialog() {
const [open, setOpen] = useState(false);
return (
<>
<Button onClick={() => setOpen(true)}>Open settings</Button>
<Modal open={open} onClose={() => setOpen(false)} width={480}>
<ModalHeader sub="Account preferences" onClose={() => setOpen(false)}>
Settings
</ModalHeader>
<ModalBody>
<p>Configure your account settings here.</p>
</ModalBody>
<ModalFooter>
<Button variant="ghost" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button variant="primary" onClick={() => setOpen(false)}>
Save
</Button>
</ModalFooter>
</Modal>
</>
);
}Drawer
Drawer is an edge-anchored slide-over for the master-detail pattern. It uses a lighter backdrop since the table behind a drawer stays part of the task. Compose with DrawerHeader, DrawerBody, and DrawerFooter.
Drawer
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | Controls visibility. |
onClose | () => void | — | Escape / close button handler. |
side | "right" | "left" | "right" | Which edge the panel slides from. |
width | number | 400 | Panel width in px. |
DrawerHeader / DrawerBody / DrawerFooter
Same pattern as Modal. DrawerHeader adds a sub prop (rendered as a mono identifier line below the title) and a right slot before the close button.
import {
Drawer,
DrawerHeader,
DrawerBody,
DrawerFooter,
Button,
} from "@solarius/onset";
import { useState } from "react";
function DetailPanel() {
const [open, setOpen] = useState(false);
return (
<>
<Button onClick={() => setOpen(true)}>View details</Button>
<Drawer
open={open}
onClose={() => setOpen(false)}
side="right"
width={420}
>
<DrawerHeader sub="node—i-0a1b2c3d" onClose={() => setOpen(false)}>
web-prod-01
</DrawerHeader>
<DrawerBody>
<p>Detailed resource information…</p>
</DrawerBody>
<DrawerFooter>
<Button variant="ghost" onClick={() => setOpen(false)}>
Close
</Button>
</DrawerFooter>
</Drawer>
</>
);
}AlertDialog
Confirmation dialog for destructive or irreversible actions. Unlike Modal, it does not close on backdrop click or Escape-to-confirm, and it focuses Cancel rather than the destructive button, so an accidental Enter does not confirm the action.
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | Controls visibility. |
onClose | () => void | — | Dismiss handler. |
onConfirm | () => void | Promise<void> | — | Confirmation handler. May return a promise. |
title | ReactNode | — | Dialog heading. |
description | ReactNode | — | The consequence, stated plainly. |
tone | AlertTone | "danger" | "danger" | "warning" | "info". |
confirmLabel | string | "Confirm" | Confirm button text. |
cancelLabel | string | "Cancel" | Cancel button text. |
confirmText | string | — | Requires typing this exact string before Confirm enables. |
loading | boolean | — | Shows pending state while onConfirm resolves. |
import { AlertDialog, Button } from "@solarius/onset";
import type { AlertTone } from "@solarius/onset";
import { useState } from "react";
function DeleteDialog() {
const [open, setOpen] = useState(false);
return (
<>
<Button variant="danger" onClick={() => setOpen(true)}>
Delete cluster
</Button>
<AlertDialog
open={open}
onClose={() => setOpen(false)}
onConfirm={() => deleteCluster()}
title="Delete cluster"
description="This action is irreversible. All data, backups, and logs will be permanently removed."
tone="danger"
confirmLabel="Delete"
confirmText="web-prod-01"
>
<p>Cluster web-prod-01 has 3 active nodes and 12 TB of data.</p>
</AlertDialog>
</>
);
}PopoverPanel
PopoverPanel displays content in an anchored panel. Outside clicks dismiss the panel.
| Prop | Type | Default | Description |
|---|---|---|---|
trigger | ReactElement | — | The element that toggles the panel. Must forward its ref. |
children | ReactNode | (api: { close: () => void }) => ReactNode | — | Panel content. Render prop receives a close function. |
open | boolean | — | Controlled open state. |
defaultOpen | boolean | — | Default open state when uncontrolled. |
onOpenChange | (open: boolean) => void | — | Open state change handler. |
placement | Placement | "bottom-start" | Position relative to the trigger. |
offset | number | 6 | Gap between trigger and panel in px. |
matchWidth | boolean | — | Panel takes at least the trigger's width. |
width | number | — | Fixed panel width in px. |
unstyled | boolean | — | Skip the surface styling. |
Works with PopoverHeader, PopoverBody, and PopoverFooter sub-components, matching the header/body/footer pattern used by every other overlay.
import {
PopoverPanel,
PopoverHeader,
PopoverBody,
PopoverFooter,
Button,
Input,
} from "@solarius/onset";
function FilterPopover() {
return (
<PopoverPanel
trigger={<Button variant="outline">Filters</Button>}
placement="bottom-start"
width={300}
>
{({ close }) => (
<>
<PopoverHeader right={<button onClick={close}>×</button>}>
Filters
</PopoverHeader>
<PopoverBody>
<Input placeholder="Search…" mono />
</PopoverBody>
<PopoverFooter>
<Button variant="ghost" size="sm" onClick={close}>
Clear
</Button>
<Button variant="primary" size="sm" onClick={close}>
Apply
</Button>
</PopoverFooter>
</>
)}
</PopoverPanel>
);
}HoverCard
HoverCard displays supplementary content on hover. Moving the pointer from the trigger to the card keeps the card open.
| Prop | Type | Default | Description |
|---|---|---|---|
trigger | ReactElement | — | The element that reveals the card. |
children | ReactNode | — | Card content. |
openDelay | number | 350 | Delay before showing, in ms. |
closeDelay | number | 180 | Grace period before hiding. |
placement | Placement | "bottom-start" | Position relative to the trigger. |
width | number | 260 | Card width in px. |
Sub-components: HoverCardHeader (media, title, subtitle, action), HoverCardBody (body text), and HoverCardStats (stat strip at the bottom).
import {
HoverCard,
HoverCardHeader,
HoverCardBody,
HoverCardStats,
Avatar,
Button,
} from "@solarius/onset";
import { IconUser } from "@tabler/icons-react";
function UserCard() {
return (
<HoverCard
trigger={<Button variant="ghost">@janedoe</Button>}
placement="bottom-start"
>
<HoverCardHeader
media={<Avatar initials="JD" size="sm" />}
title="Jane Doe"
subtitle="@janedoe"
action={
<Button variant="ghost" size="sm">
Follow
</Button>
}
/>
<HoverCardBody>
Senior infrastructure engineer. Works on the platform team.
</HoverCardBody>
<HoverCardStats
stats={[
{ label: "Repos", value: "24" },
{ label: "Commits", value: "1.2K" },
{ label: "Teams", value: "3" },
]}
/>
</HoverCard>
);
}Tooltip
A short hover label. Tooltip wraps a single focusable or hoverable child and shows a tooltip above or below it.
side is a preference, not a guarantee. Placement goes through the same positioner as every other overlay, so a tooltip on a trigger near an edge flips to the opposite side and is clamped into the viewport. It renders in a portal, so an ancestor with overflow: hidden does not clip it.
| Prop | Type | Default | Description |
|---|---|---|---|
content | ReactNode | — | The tooltip text. |
children | ReactElement | — | The trigger element. |
side | "top" | "bottom" | "top" | Which side of the child to appear on. |
disabled | boolean | — | Suppress the tooltip. |
import { Tooltip } from "@solarius/onset";
<Tooltip content="Delete this item" side="bottom">
<button>🗑</button>
</Tooltip>;Menus
DropdownMenu
DropdownMenu is a click-to-open menu, for the "More ▾" / kebab-menu pattern. It wraps the trigger in an inline anchor so any component can be used as a trigger without forwarding a ref.
| Prop | Type | Default | Description |
|---|---|---|---|
trigger | ReactNode | — | The clickable element that opens the menu. |
children | ReactNode | — | MenuItem components for the dropdown. |
placement | Placement | "bottom-start" | Position relative to the trigger. |
open | boolean | — | Controlled open state. |
defaultOpen | boolean | — | Default open state. |
onOpenChange | (open: boolean) => void | — | Open state change handler. |
closeOnSelect | boolean | true | Close the menu after clicking any item. |
fullWidth | boolean | — | Stretch to full width for block-level triggers. |
import { DropdownMenu, MenuItem, MenuDivider } from "@solarius/onset";
import { IconDownload, IconCopy, IconTrash } from "@tabler/icons-react";
function MoreMenu() {
return (
<DropdownMenu
trigger={
<Button variant="ghost" size="sm">
Actions ▾
</Button>
}
placement="bottom-end"
>
<MenuItem icon={<IconDownload size={13} />} shortcut="⌘D">
Download
</MenuItem>
<MenuItem icon={<IconCopy size={13} />} shortcut="⌘C">
Copy
</MenuItem>
<MenuDivider />
<MenuItem icon={<IconTrash size={13} />} danger shortcut="⌘⌫">
Delete
</MenuItem>
</DropdownMenu>
);
}ContextMenu / ContextMenuArea
ContextMenuArea wraps any content and intercepts right-click events, suppressing the browser's native menu and opening a positioned ContextMenu at the cursor. ContextMenu renders the menu shell itself, and can also be used standalone inside a DropdownMenu.
import {
ContextMenuArea,
ContextMenu,
MenuItem,
MenuDivider,
} from "@solarius/onset";
import { IconCopy, IconEdit, IconTrash } from "@tabler/icons-react";
function EditableRow() {
return (
<ContextMenuArea
menu={
<>
<MenuItem icon={<IconEdit size={13} />}>Edit</MenuItem>
<MenuItem icon={<IconCopy size={13} />}>Duplicate</MenuItem>
<MenuDivider />
<MenuItem icon={<IconTrash size={13} />} danger>
Delete
</MenuItem>
</>
}
>
<div>Right-click this row to see the context menu.</div>
</ContextMenuArea>
);
}MenuItem
A single menu item; renders as a button with role="menuitem".
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | Item label. |
icon | ReactNode | — | Leading icon. |
shortcut | ReactNode | — | Right-aligned shortcut hint. |
highlighted | boolean | — | Highlight the row (selected state). |
disabled | boolean | — | Prevents interaction. |
danger | boolean | — | Red text for destructive actions. |
onClick | () => void | — | Click handler. |
MenuDivider
A horizontal divider between MenuItem groups. No props beyond standard div attributes.
SettingsModal
SettingsModal is a settings dialog with a persistent nav rail on the left and a titled, scrolling pane on the right. The nav is data-driven from a SettingsSection[]; the active section's body is children, swapped by the caller on activeSection. Compose the body from SettingsGroup and SettingsRow.
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | Controls visibility. |
onClose | () => void | — | Escape / backdrop / close-button handler. |
sections | SettingsSection[] | — | Navigation groups, headings, icons, and badges. |
activeSection | string | — | Controlled active id (pair with onSectionChange). |
onSectionChange | (id: string) => void | — | Active-section change handler. |
title | ReactNode | "Settings" | Sidebar heading. |
search | boolean | — | Adds a filter box over the nav. |
footer | ReactNode | — | Footer content below the active pane. |
import {
SettingsModal,
SettingsGroup,
SettingsRow,
Switch,
} from "@solarius/onset";
import type { SettingsSection } from "@solarius/onset";
import { IconUserCircle, IconBell } from "@tabler/icons-react";
import { useState } from "react";
const sections: SettingsSection[] = [
{
id: "profile",
label: "Profile",
icon: <IconUserCircle size={15} />,
group: "Account",
},
{
id: "notifications",
label: "Notifications",
icon: <IconBell size={15} />,
group: "Account",
},
];
function Settings() {
const [open, setOpen] = useState(false);
const [section, setSection] = useState("profile");
return (
<SettingsModal
open={open}
onClose={() => setOpen(false)}
sections={sections}
activeSection={section}
onSectionChange={setSection}
search
>
{section === "notifications" && (
<SettingsGroup title="Alerts">
<SettingsRow
label="Disputes opened"
description="A cardholder opened a chargeback."
>
<Switch checked onChange={() => {}} />
</SettingsRow>
</SettingsGroup>
)}
</SettingsModal>
);
}CommandPalette
CommandPalette filters grouped commands and provides arrow-key navigation, Enter selection, and Escape dismissal. The application controls open. Use useCommandK to register Cmd+K and Ctrl+K.
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | Controls visibility. |
onClose | () => void | — | Dismiss handler. |
groups | CommandGroup[] | — | Grouped commands to search and select from. |
onRun | (command: Command) => void | — | Called when a command is executed. |
placeholder | string | "Type a command or search…" | Search input placeholder. |
Command shape: id (string), label (string), keywords (optional string for aliases), icon (optional ReactNode), shortcut (optional ReactNode), and hint (optional ReactNode for right-aligned annotation).
import { CommandPalette, useCommandK } from "@solarius/onset";
import type { Command, CommandGroup } from "@solarius/onset";
import {
IconDashboard,
IconSettings,
IconUser,
IconServer,
} from "@tabler/icons-react";
import { useState } from "react";
function App() {
const [open, setOpen] = useState(false);
useCommandK(() => setOpen(true));
const groups: CommandGroup[] = [
{
label: "Navigation",
commands: [
{
id: "dashboard",
label: "Dashboard",
icon: <IconDashboard size={13} />,
hint: "Navigation",
},
{
id: "settings",
label: "Settings",
icon: <IconSettings size={13} />,
hint: "Navigation",
},
],
},
{
label: "Actions",
commands: [
{
id: "new-user",
label: "New user",
icon: <IconUser size={13} />,
keywords: "add create invite",
},
{
id: "new-node",
label: "New node",
icon: <IconServer size={13} />,
keywords: "add create provision",
},
],
},
];
return (
<CommandPalette
open={open}
onClose={() => setOpen(false)}
groups={groups}
onRun={(cmd) => {
console.log("Running:", cmd.id);
// Navigate or dispatch based on cmd.id
}}
/>
);
}