Resizable
PortedPanels with a handle between them that changes their sizes, for master–detail views: a list beside the record it opens, or a trace above its logs. The app sets the group's size with className; a vertical group needs a height.
List
Detail
import { type CSSProperties } from "react";
import { Text } from "@etched/monet/text";
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@etched/monet/resizable";
/** A panel edge around the group, like Orbit's cards. The app sets the size. */
const frame = (size: CSSProperties): CSSProperties => ({ ...size, borderRadius: "var(--et-card-radius)", boxShadow: "0 0 0 1px var(--et-line)", overflow: "clip" });
const pad: CSSProperties = { display: "grid", alignContent: "start", gap: "var(--et-space-1)", padding: "var(--et-space-3) var(--et-space-4)" };
export function Preview() {
return (
<ResizablePanelGroup style={frame({ inlineSize: "18rem", blockSize: "7rem" })}>
<ResizablePanel defaultSize={40} minSize={25} style={pad}>
<Text size="sm" variant="secondary">
List
</Text>
</ResizablePanel>
<ResizableHandle aria-label="List width" />
<ResizablePanel defaultSize={60} style={pad}>
<Text size="sm" variant="secondary">
Detail
</Text>
</ResizablePanel>
</ResizablePanelGroup>
);
}Installation
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from "@etched/monet/resizable";Examples
List and detail
Drag the line, or focus it and use Left and Right (16 px a step). The list stays between 25% and 60% of the width; Home and End move it to those limits, and a double-click puts it back to 35%. The detail panel takes the open candidate as its key, so each one opens fresh.
ASIC design engineer
import { useState, type CSSProperties } from "react";
import { SelectableList, type SelectableListItem } from "@etched/monet/selectable-list";
import { Text } from "@etched/monet/text";
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@etched/monet/resizable";
/** A panel edge around the group, like Orbit's cards. The app sets the size. */
const frame = (size: CSSProperties): CSSProperties => ({ ...size, borderRadius: "var(--et-card-radius)", boxShadow: "0 0 0 1px var(--et-line)", overflow: "clip" });
const pad: CSSProperties = { display: "grid", alignContent: "start", gap: "var(--et-space-1)", padding: "var(--et-space-3) var(--et-space-4)" };
const people: SelectableListItem[] = [
{ value: "ana", label: "Ana Ruiz", description: "ASIC design engineer", avatar: { name: "Ana Ruiz" } },
{ value: "jose", label: "José Núñez", description: "Firmware engineer", avatar: { name: "José Núñez" } },
{ value: "mira", label: "Mira Osei", description: "Physical design lead", avatar: { name: "Mira Osei" } },
];
export function ListAndDetail() {
const [open, setOpen] = useState<string | null>("ana");
const person = people.find((p) => p.value === open);
return (
<ResizablePanelGroup style={frame({ blockSize: "14rem", maxInlineSize: "40rem" })}>
<ResizablePanel defaultSize={35} minSize={25} maxSize={60}>
<SelectableList aria-label="Candidates" items={people} value={open} onValueChange={setOpen} />
</ResizablePanel>
<ResizableHandle aria-label="Candidate list width" />
<ResizablePanel key={open} defaultSize={65} style={pad}>
<Text variant="heading">{person?.label}</Text>
<Text variant="secondary">{person?.description}</Text>
</ResizablePanel>
</ResizablePanelGroup>
);
}One above the other
With orientation="vertical", the handle moves up and down (Up and Down keys). A vertical group needs a height.
Run 1,284 · 3 steps · 42 s
12 lines
import { type CSSProperties } from "react";
import { Text } from "@etched/monet/text";
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@etched/monet/resizable";
/** A panel edge around the group, like Orbit's cards. The app sets the size. */
const frame = (size: CSSProperties): CSSProperties => ({ ...size, borderRadius: "var(--et-card-radius)", boxShadow: "0 0 0 1px var(--et-line)", overflow: "clip" });
const pad: CSSProperties = { display: "grid", alignContent: "start", gap: "var(--et-space-1)", padding: "var(--et-space-3) var(--et-space-4)" };
export function Vertical() {
return (
<ResizablePanelGroup orientation="vertical" style={frame({ blockSize: "14rem", maxInlineSize: "40rem" })}>
<ResizablePanel defaultSize={60} minSize={30} style={pad}>
<Text variant="heading">Trace</Text>
<Text size="sm" variant="secondary">
Run 1,284 · 3 steps · 42 s
</Text>
</ResizablePanel>
<ResizableHandle aria-label="Trace height" />
<ResizablePanel defaultSize={40} minSize={20} style={pad}>
<Text variant="heading">Logs</Text>
<Text size="sm" variant="secondary">
12 lines
</Text>
</ResizablePanel>
</ResizablePanelGroup>
);
}Three panels
Each handle changes the two panels next to it, within both panels' limits.
Roles
Candidates
Profile
import { type CSSProperties } from "react";
import { Text } from "@etched/monet/text";
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@etched/monet/resizable";
/** A panel edge around the group, like Orbit's cards. The app sets the size. */
const frame = (size: CSSProperties): CSSProperties => ({ ...size, borderRadius: "var(--et-card-radius)", boxShadow: "0 0 0 1px var(--et-line)", overflow: "clip" });
const pad: CSSProperties = { display: "grid", alignContent: "start", gap: "var(--et-space-1)", padding: "var(--et-space-3) var(--et-space-4)" };
export function ThreePanels() {
return (
<ResizablePanelGroup style={frame({ blockSize: "8rem", maxInlineSize: "40rem" })}>
<ResizablePanel defaultSize={25} minSize={15} style={pad}>
<Text size="sm" variant="secondary">
Roles
</Text>
</ResizablePanel>
<ResizableHandle aria-label="Roles width" />
<ResizablePanel defaultSize={40} minSize={25} style={pad}>
<Text size="sm" variant="secondary">
Candidates
</Text>
</ResizablePanel>
<ResizableHandle aria-label="Candidates width" />
<ResizablePanel defaultSize={35} minSize={25} style={pad}>
<Text size="sm" variant="secondary">
Profile
</Text>
</ResizablePanel>
</ResizablePanelGroup>
);
}Accessibility
ArrowLeft/ArrowRightmove a focused handle 16 px a step (ArrowUp/ArrowDownin a vertical group).Home/Endmove it to its limits.- Each handle is a focusable
separator, named by its requiredaria-label. Itsaria-controlspoints at the panel before it. - Screen readers hear that panel's size in percent, from
aria-valuenow,aria-valueminandaria-valuemax. - Name each handle for what it sizes, such as "Candidate list width".
API reference
ResizableHandle
| Prop | Type | Default | Description |
|---|---|---|---|
| aria-label * | string | – | Names the handle, for example "Candidate list width". |
ResizablePanelGroup
| Prop | Type | Default | Description |
|---|---|---|---|
| orientation | "horizontal" | "vertical" | "horizontal" | |
| children * | ReactNode | – | ResizablePanels with a ResizableHandle between each two. |
ResizablePanel
| Prop | Type | Default | Description |
|---|---|---|---|
| defaultSize * | number | – | Its first size, in percent of the group. The sizes of a group's panels add up to 100. |
| minSize | number | 0 | The smallest size a handle can give it, in percent. |
| maxSize | number | 100 | The largest size a handle can give it, in percent. |
| children | ReactNode | – |