Overlays
Components

Overlays

Modals, drawers, popovers, hover cards, tooltips, menus, and the command palette: every component that renders above the page.

Components in this category

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.

ModalProps props
PropTypeDefaultDescription
openboolean—Controls visibility.
onClose() => void—Escape / close button handler.
widthnumber440Panel width in px.
closeOnBackdropbooleantrueSet false for destructive confirms.

ModalHeader

ModalHeaderProps props
PropTypeDefaultDescription
childrenReactNode—Modal title (the header renders its children as the title).
subReactNode—Dim sub-label after the title.
onClose() => void—Renders the close button.

ModalBody

Scrollable body region. Accepts all standard div attributes.

ModalFooter

ModalFooterProps props
PropTypeDefaultDescription
leftReactNode—Left-aligned content.
childrenReactNode—Right-aligned action buttons.
ImportTSX
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

DrawerProps props
PropTypeDefaultDescription
openboolean—Controls visibility.
onClose() => void—Escape / close button handler.
side"right" | "left""right"Which edge the panel slides from.
widthnumber400Panel 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.

ImportTSX
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.

AlertDialogProps props
PropTypeDefaultDescription
openboolean—Controls visibility.
onClose() => void—Dismiss handler.
onConfirm() => void | Promise<void>—Confirmation handler. May return a promise.
titleReactNode—Dialog heading.
descriptionReactNode—The consequence, stated plainly.
toneAlertTone"danger""danger" | "warning" | "info".
confirmLabelstring"Confirm"Confirm button text.
cancelLabelstring"Cancel"Cancel button text.
confirmTextstring—Requires typing this exact string before Confirm enables.
loadingboolean—Shows pending state while onConfirm resolves.
ImportTSX
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.

PopoverPanelProps props
PropTypeDefaultDescription
triggerReactElement—The element that toggles the panel. Must forward its ref.
childrenReactNode | (api: { close: () => void }) => ReactNode—Panel content. Render prop receives a close function.
openboolean—Controlled open state.
defaultOpenboolean—Default open state when uncontrolled.
onOpenChange(open: boolean) => void—Open state change handler.
placementPlacement"bottom-start"Position relative to the trigger.
offsetnumber6Gap between trigger and panel in px.
matchWidthboolean—Panel takes at least the trigger's width.
widthnumber—Fixed panel width in px.
unstyledboolean—Skip the surface styling.

Works with PopoverHeader, PopoverBody, and PopoverFooter sub-components, matching the header/body/footer pattern used by every other overlay.

ImportTSX
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.

HoverCardProps props
PropTypeDefaultDescription
triggerReactElement—The element that reveals the card.
childrenReactNode—Card content.
openDelaynumber350Delay before showing, in ms.
closeDelaynumber180Grace period before hiding.
placementPlacement"bottom-start"Position relative to the trigger.
widthnumber260Card width in px.

Sub-components: HoverCardHeader (media, title, subtitle, action), HoverCardBody (body text), and HoverCardStats (stat strip at the bottom).

ImportTSX
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.

TooltipProps props
PropTypeDefaultDescription
contentReactNode—The tooltip text.
childrenReactElement—The trigger element.
side"top" | "bottom""top"Which side of the child to appear on.
disabledboolean—Suppress the tooltip.
Tooltip
ImportTSX
import { Tooltip } from "@solarius/onset";

<Tooltip content="Delete this item" side="bottom">
  <button>🗑</button>
</Tooltip>;

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.

DropdownMenuProps props
PropTypeDefaultDescription
triggerReactNode—The clickable element that opens the menu.
childrenReactNode—MenuItem components for the dropdown.
placementPlacement"bottom-start"Position relative to the trigger.
openboolean—Controlled open state.
defaultOpenboolean—Default open state.
onOpenChange(open: boolean) => void—Open state change handler.
closeOnSelectbooleantrueClose the menu after clicking any item.
fullWidthboolean—Stretch to full width for block-level triggers.
ImportTSX
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>
  );
}
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.

ImportTSX
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>
  );
}

A single menu item; renders as a button with role="menuitem".

MenuItemProps props
PropTypeDefaultDescription
childrenReactNode—Item label.
iconReactNode—Leading icon.
shortcutReactNode—Right-aligned shortcut hint.
highlightedboolean—Highlight the row (selected state).
disabledboolean—Prevents interaction.
dangerboolean—Red text for destructive actions.
onClick() => void—Click handler.

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.

SettingsModalProps props
PropTypeDefaultDescription
openboolean—Controls visibility.
onClose() => void—Escape / backdrop / close-button handler.
sectionsSettingsSection[]—Navigation groups, headings, icons, and badges.
activeSectionstring—Controlled active id (pair with onSectionChange).
onSectionChange(id: string) => void—Active-section change handler.
titleReactNode"Settings"Sidebar heading.
searchboolean—Adds a filter box over the nav.
footerReactNode—Footer content below the active pane.
ImportTSX
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.

CommandPaletteProps props
PropTypeDefaultDescription
openboolean—Controls visibility.
onClose() => void—Dismiss handler.
groupsCommandGroup[]—Grouped commands to search and select from.
onRun(command: Command) => void—Called when a command is executed.
placeholderstring"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).

ImportTSX
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
      }}
    />
  );
}