SettingsDialog
A settings window: sections on the left, one pane on the right, and an optional search that jumps to the row it found. It lives in its own entry point with the parts a pane is built from.
import { SettingsDialog } from '@basmilius/react-ui/settings';import { useState } from 'react';
import { Bell, Info, Keyboard, Palette, Settings } from 'lucide-react';
import { Button, Icon, Select, Switch } from '@basmilius/react-ui';
import { MasterDetail, MasterItem, SettingsDialog, SettingsRow, SettingsSection, type SettingsSearchResult } from '@basmilius/react-ui/settings';
function AppearancePane() {
const [theme, setTheme] = useState('system');
const [compact, setCompact] = useState(false);
return (
<SettingsSection title="Window" description="How the app looks on this computer.">
<SettingsRow
label="Theme"
searchId="theme"
control={
<Select
label="Theme"
value={theme}
onValueChange={setTheme}
items={[
{ value: 'system', label: 'Match the system' },
{ value: 'light', label: 'Light' },
{ value: 'dark', label: 'Dark' }
]}
/>
}
/>
<SettingsRow
label="Compact rows"
description="Fits more on a small screen."
searchId="compact"
control={<Switch label="Compact rows" checked={compact} onCheckedChange={setCompact} />}
/>
</SettingsSection>
);
}
function NotificationsPane() {
const [sounds, setSounds] = useState(true);
return (
<SettingsSection title="Alerts">
<SettingsRow label="Play a sound" searchId="sound" control={<Switch label="Play a sound" checked={sounds} onCheckedChange={setSounds} />} />
</SettingsSection>
);
}
const SHORTCUT_GROUPS = ['General', 'Editing', 'Navigation'];
function ShortcutsPane() {
const [picked, setPicked] = useState('General');
return (
<MasterDetail
listWidth={280}
listLabel="Shortcut groups"
list={SHORTCUT_GROUPS.map((group) => (
<MasterItem key={group} selected={picked === group} onSelect={() => setPicked(group)}>
{group}
</MasterItem>
))}
detail={<p className="text-sm text-text-muted">The shortcuts of {picked.toLowerCase()} go here.</p>}
/>
);
}
function AboutPane() {
return <p className="text-sm text-text-muted">Version 1.0.0</p>;
}
const GROUPS = [
{
label: null,
sections: [
{ id: 'appearance', icon: Palette, label: 'Appearance', description: 'Theme and density.', pane: AppearancePane },
{ id: 'notifications', icon: Bell, label: 'Notifications', description: 'What asks for your attention.', pane: NotificationsPane }
]
},
{
label: 'Advanced',
sections: [{ id: 'shortcuts', icon: Keyboard, label: 'Shortcuts', description: 'Every key the app knows.', pane: ShortcutsPane, split: true }]
}
];
const FOOTER = [{ id: 'about', icon: Info, label: 'About', description: 'The version you run.', pane: AboutPane }];
const SEARCHABLE: SettingsSearchResult[] = [
{ section: 'appearance', id: null, label: 'Appearance' },
{ section: 'appearance', id: 'theme', label: 'Theme' },
{ section: 'appearance', id: 'compact', label: 'Compact rows' },
{ section: 'notifications', id: 'sound', label: 'Play a sound' },
{ section: 'shortcuts', id: null, label: 'Shortcuts' }
];
const search = {
find: (query: string) => SEARCHABLE.filter((result) => result.label.toLowerCase().includes(query.trim().toLowerCase())),
hint: '⌘F'
};
export default function SettingsDialogDemo() {
const [open, setOpen] = useState(false);
const [section, setSection] = useState('appearance');
const [target, setTarget] = useState<string | null>(null);
return (
<>
<Button variant="secondary" onClick={() => setOpen(true)}>
<Icon icon={Settings} size={14} />
Open settings
</Button>
<SettingsDialog
open={open}
onOpenChange={setOpen}
section={section}
onNavigate={(next) => {
setSection(next.section);
setTarget(next.target ?? null);
}}
groups={GROUPS}
footer={FOOTER}
search={search}
target={target}
onTargetShown={() => setTarget(null)}
/>
</>
);
}Sections and panes
You describe the sections, the dialog draws them. groups is the navigation, in order, each with an optional label (the first group usually goes without one). footer sections stand at the foot of the navigation. A section names a pane, a component the dialog renders only while that section is open, inside a Suspense and an ErrorBoundary. A lazy pane loads when its section is first opened.
A pane is a padded column that scrolls, with a fade at its top once something scrolled under the header. A split section gets the whole height instead, for a MasterDetail whose two sides scroll on their own.
You keep which section is open. onNavigate tells you when a person picks another, with the arrow keys in the navigation or a click.
Search
search.find(query) answers the results for what a person typed. It is yours, so it can search your own words in any language. A result names a section and, optionally, the searchId of a SettingsRow in it. Picking one calls onNavigate with the section and the row as target. Pass that back in as target, and the row scrolls into view and lights up for a moment, then calls onTargetShown, where you clear it.
Escape in the search field clears the query first; only an empty field lets Escape close the dialog. Enter jumps to the first result. search.hint prints the shortcut that focuses the field, and search.focusAt focuses and selects it each time the number grows, so your shortcut handler can bump it.
An account at the foot
account puts one more section under the footer, past a hairline, whose tab you draw yourself, such as an avatar and a name. The tab is a Base UI Tabs.Tab with the section's id as its value.
Narrow windows
The dialog is 1200 by 760 pixels and steps down with the viewport. Under 960 pixels the navigation narrows, and under 640 it turns into a select of sections above the pane.
Props
| Prop | Type | |
|---|---|---|
open | boolean | Required. |
onOpenChange | (open: boolean) => void | Required. |
section | string | Required. The id of the open section. |
onNavigate | (next: { section: string; target?: string | null }) => void | Required. |
groups | readonly SettingsGroupEntry[] | Required. { label: string | null; sections } |
footer | readonly SettingsSectionEntry[] | |
account | { section: SettingsSectionEntry; tab: ReactNode } | |
search | SettingsSearch | { find(query), hint?, focusAt? } |
target | string | null | The row a search result jumped to. |
onTargetShown | () => void | |
className | string | On the popup. |
ref | Ref<HTMLDivElement> |
A SettingsSectionEntry is { id, icon, label, description, pane, split? }, where the description is the line under the pane's title. A SettingsSearchResult is { section, id, label }, with id: null for a result that is the pane itself.
SettingsDialogProps, SettingsGroupEntry, SettingsSectionEntry, SettingsSearch and SettingsSearchResult are exported types.