Skip to content

MasterDetail ​

A list beside the detail of what is picked in it, for a settings pane of many things of one kind: accounts, devices, shortcut groups. Each side scrolls on its own, and the detail takes the rest of the width.

tsx
import { DetailHeader, MasterDetail, MasterItem } from '@basmilius/react-ui/settings';
tsx
import { useState } from 'react';
import { Laptop, Smartphone, Trash } from 'lucide-react';
import { Button, Icon } from '@basmilius/react-ui';
import { DetailHeader, MasterDetail, MasterItem, SettingsRow, SettingsSection } from '@basmilius/react-ui/settings';

const DEVICES = [
    { id: 'laptop', name: 'Work laptop', kind: 'macOS', icon: Laptop },
    { id: 'phone', name: 'Phone', kind: 'iOS', icon: Smartphone }
];

export default function MasterDetailDemo() {
    const [picked, setPicked] = useState('laptop');
    const device = DEVICES.find((entry) => entry.id === picked)!;

    return (
        <div className="flex h-80 w-full flex-col overflow-hidden rounded-lg border border-border bg-surface">
            <MasterDetail
                listWidth={280}
                listLabel="Devices"
                list={DEVICES.map((entry) => (
                    <MasterItem key={entry.id} selected={picked === entry.id} onSelect={() => setPicked(entry.id)}>
                        <Icon icon={entry.icon} className="text-text-muted" />
                        {entry.name}
                    </MasterItem>
                ))}
                detail={
                    <>
                        <DetailHeader
                            mark={
                                <span className="grid size-10 place-items-center rounded-lg bg-surface-sunken text-text-muted">
                                    <Icon icon={device.icon} size={20} />
                                </span>
                            }
                            title={device.name}
                            subtitle={<>{device.kind}, signed in</>}
                            actions={
                                <Button size="sm" variant="danger-outline">
                                    <Icon icon={Trash} size={14} />
                                    Remove
                                </Button>
                            }
                        />
                        <SettingsSection>
                            <SettingsRow label="Last seen" control={<span className="text-xs text-text-muted">Today</span>} />
                        </SettingsSection>
                    </>
                }
            />
        </div>
    );
}

Inside a SettingsDialog, put it in a split section, so the dialog hands the pane its whole height instead of a padded scrolling column. Under 640 pixels the list stacks above the detail.

Parts ​

MasterDetail takes the list, its width (280, 320 or 340 pixels), a listLabel that names the list for a screen reader, and the detail.

MasterItem is one row of the list, a button that lifts a shade when selected and carries aria-current. It takes selected, onSelect and children.

DetailHeader is the head of a detail: a mark (a logo or an icon tile), the title, a subtitle line under it, and the actions of the thing on the right.

ComponentPropType
MasterDetaillistReactNode
listWidth280 | 320 | 340
listLabelstring
detailReactNode
MasterItemselectedboolean
onSelect() => void
childrenReactNode
DetailHeadermarkReactNode
titlestring
subtitleReactNode
actionsReactNode, optional

Every prop is required unless it says otherwise, and all three take className and ref. MasterDetailProps, MasterItemProps and DetailHeaderProps are exported types.