Content
Outline
Browse a hierarchy in rows that expand, like Finder's list view.
"use client"
import {
Outline,
OutlineGroup,
OutlineItem,
OutlineItemDetail,
OutlineItemIcon,
OutlineItemLabel,
OutlineItemText,
} from "macbetter/outline"
import { FileTextIcon, FolderIcon, ImageIcon, PresentationIcon, SheetIcon } from "lucide-react"
type Node = { value: string; name: string; detail: string; kind: "folder" | "doc" | "image" | "sheet" | "deck"; children?: Node[] }
const files: Node[] = [
{
value: "projects",
name: "Projects",
detail: "12 items",
kind: "folder",
children: [
{
value: "projects/macbetter",
name: "macbetter",
detail: "4 items",
kind: "folder",
children: [
{ value: "projects/macbetter/spec", name: "Design Spec.pdf", detail: "2.4 MB", kind: "doc" },
{ value: "projects/macbetter/icons", name: "App Icons.png", detail: "860 KB", kind: "image" },
{ value: "projects/macbetter/launch", name: "Launch Plan.key", detail: "18 MB", kind: "deck" },
],
},
{ value: "projects/budget", name: "Budget 2027.numbers", detail: "212 KB", kind: "sheet" },
],
},
{
value: "photos",
name: "Photos",
detail: "3 items",
kind: "folder",
children: [
{ value: "photos/offsite", name: "Team Offsite.heic", detail: "3.2 MB", kind: "image" },
{ value: "photos/headshot", name: "Headshot.jpg", detail: "1.1 MB", kind: "image" },
],
},
{ value: "notes", name: "Meeting Notes.md", detail: "4 KB", kind: "doc" },
{ value: "contract", name: "Contract — Signed.pdf", detail: "640 KB", kind: "doc" },
]
const icons = { folder: FolderIcon, doc: FileTextIcon, image: ImageIcon, sheet: SheetIcon, deck: PresentationIcon }
function Items({ items }: { items: Node[] }) {
return items.map((item) => {
const Icon = icons[item.kind]
return (
<OutlineItem key={item.value} value={item.value} label={item.name}>
<OutlineItemLabel>
<OutlineItemIcon>
<Icon aria-hidden="true" />
</OutlineItemIcon>
<OutlineItemText>{item.name}</OutlineItemText>
<OutlineItemDetail>{item.detail}</OutlineItemDetail>
</OutlineItemLabel>
{item.children ? (
<OutlineGroup>
<Items items={item.children} />
</OutlineGroup>
) : null}
</OutlineItem>
)
})
}
export default function Example() {
return (
<Outline
aria-label="Documents"
defaultExpanded={["projects", "projects/macbetter"]}
defaultValue="projects/macbetter/spec"
style={{ width: "100%", maxWidth: 520 }}
>
<Items items={files} />
</Outline>
)
}When to use it
Use an outline when items nest: folders and files, groups and their members, sections of a document. Each branch has a disclosure triangle that turns to show its children, and the rows stay in one column so people can scan them.
- Keep a leading icon for each row so folders and files are told apart at a glance.
- Put a short detail, like a size or an item count, at the trailing edge.
- Use a Table when people compare several attributes of flat items, and a Disclosure to hide a few sections of content.
Usage
import {
Outline,
OutlineGroup,
OutlineItem,
OutlineItemDetail,
OutlineItemIcon,
OutlineItemLabel,
OutlineItemText,
} from "macbetter/outline"An OutlineItem with an OutlineGroup inside it is a branch. OutlineItemLabel is the row, and draws the disclosure triangle for you; leaves keep its space so their icons line up under their parent's.
Indentation guides
Guides are off by default, as in Finder. Turn them on with guides to draw a hairline down each open branch, which helps in a deep tree.
"use client"
import {
Outline,
OutlineGroup,
OutlineItem,
OutlineItemIcon,
OutlineItemLabel,
OutlineItemText,
} from "macbetter/outline"
import { FileIcon, FolderIcon } from "lucide-react"
export default function Example() {
return (
<Outline aria-label="Source" guides mode="multiple" defaultExpanded={["src", "src/components"]} style={{ width: "100%", maxWidth: 360 }}>
<OutlineItem value="src">
<OutlineItemLabel>
<OutlineItemIcon>
<FolderIcon aria-hidden="true" />
</OutlineItemIcon>
<OutlineItemText>src</OutlineItemText>
</OutlineItemLabel>
<OutlineGroup>
<OutlineItem value="src/components">
<OutlineItemLabel>
<OutlineItemIcon>
<FolderIcon aria-hidden="true" />
</OutlineItemIcon>
<OutlineItemText>components</OutlineItemText>
</OutlineItemLabel>
<OutlineGroup>
<OutlineItem value="src/components/list.tsx">
<OutlineItemLabel>
<OutlineItemIcon>
<FileIcon aria-hidden="true" />
</OutlineItemIcon>
<OutlineItemText>list.tsx</OutlineItemText>
</OutlineItemLabel>
</OutlineItem>
<OutlineItem value="src/components/table.tsx">
<OutlineItemLabel>
<OutlineItemIcon>
<FileIcon aria-hidden="true" />
</OutlineItemIcon>
<OutlineItemText>table.tsx</OutlineItemText>
</OutlineItemLabel>
</OutlineItem>
</OutlineGroup>
</OutlineItem>
<OutlineItem value="src/index.ts">
<OutlineItemLabel>
<OutlineItemIcon>
<FileIcon aria-hidden="true" />
</OutlineItemIcon>
<OutlineItemText>index.ts</OutlineItemText>
</OutlineItemLabel>
</OutlineItem>
</OutlineGroup>
</OutlineItem>
<OutlineItem value="package.json">
<OutlineItemLabel>
<OutlineItemIcon>
<FileIcon aria-hidden="true" />
</OutlineItemIcon>
<OutlineItemText>package.json</OutlineItemText>
</OutlineItemLabel>
</OutlineItem>
</Outline>
)
}Selection and actions
Selection is single by default, and it follows the arrow keys as in Finder; pass activation="manual" to move focus without selecting until Enter or Space. mode="multiple" allows Command-click and Shift-click ranges. onAction runs when a row is pressed or Enter is pressed on it, like opening a file. Set expandOnPress={false} so a click selects a folder without opening it, leaving that to the triangle and the arrows.
Platform behavior
- macOS. 24-point rows with 13-point text, a 16-point indent per level and a small chevron for the triangle. The selection is the accent while the outline has focus and turns gray when focus leaves, as in Finder. Adjacent selected rows join into one block.
- iOS and iPadOS. 44-point rows with 17-point text, a 20-point indent, and a pressed state on rows. The disclosure chevron sits at the trailing edge in the accent color and turns down when open, as UIKit's outline disclosure does. Consider a List that navigates into each folder on iPhone, where nesting is hard to read.
- In right-to-left layouts the rows, indent and triangle mirror: a closed triangle points left and turns down when open.
- The triangle turns with the system's snappy spring, and a branch's rows slide open as its height animates. Reduced motion skips the slide.
Accessibility
The outline is a WAI-ARIA tree, from ui-sdk. It has one tab stop.
| Key | Action |
|---|---|
| Down / Up Arrow | Move to the next or previous visible row and select it. |
| Right Arrow | Expand a branch, or move into its first child. |
| Left Arrow | Collapse a branch, or move to its parent. |
| Option+Right / Option+Left | Expand or collapse a branch and everything in it. |
| Home / End | Move to the first or last visible row. |
| Enter | Run onAction on the row. |
| Typing | Move to the next row whose label starts with the text. |
Give the outline a name with aria-label. Icons are aria-hidden, so a row is named by its text and detail. Pass label to an item when its text isn't plain, so typing finds it.
API
Outline
Takes every prop of ui-sdk's Tree, including mode, value, defaultValue, expanded, defaultExpanded, onAction and expandOnPress, plus:
| Prop | Type | Default | Description |
|---|---|---|---|
guides | boolean | false | Draws a hairline down each open branch. |
activation | "automatic" | "manual" | "automatic" | Whether the arrow keys select as they move, in single selection. |
OutlineItem
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | Identifies the item. Unique in the outline. |
label | string | — | The text typing matches, when the row's text isn't plain. |
branch | boolean | — | Makes it a branch whose children load later. Set for you when it holds an OutlineGroup. |
disabled | boolean | false | Stops it from being selected or expanded. |
OutlineItemLabel, OutlineGroup, OutlineItemIcon, OutlineItemText and OutlineItemDetail take the props of the element they render.
Data attributes and CSS variables
| Attribute or variable | On | Meaning |
|---|---|---|
data-guides | .mb-outline | Indentation guides are on. |
data-selected, data-expanded, data-branch, data-disabled | .mb-outline-row | The row's state, from ui-sdk. |
--mb-outline-row-height, --mb-outline-indent | platform | Row height and the indent per level. |
--mb-outline-icon-color | .mb-outline | The icons' color. Defaults to the accent. |
--mb-outline-indicator-order, --mb-outline-indicator-color | platform | 0 leads the row with the triangle (macOS), 1 trails it (iOS); and its color. |