| title | React API |
|---|---|
| description | Every component, prop and hook in @danfessler/trellis-react. |
| section | Reference |
| order | 30 |
@danfessler/trellis-react wraps the core in React components and hooks. Import what you need:
import {
Workspace,
WorkspaceProvider,
ViewType,
Split,
Panel,
View,
Stage,
Floating,
useWorkspace,
useOptionalWorkspace,
useWorkspaceState,
useWorkspaceSelector,
useView,
useOptionalView,
useViewTitle,
useViewBadge,
useCloseGuard,
layout,
createDocument,
formatCombo,
} from "@danfessler/trellis-react";The package also re-exports every type from the core, such as WorkspaceHandle, LayoutDocument, ViewHandle and Placement. Import the stylesheet once with import "@danfessler/trellis/style.css".
Creates a workspace in a div that fills its parent (width: 100%; height: 100%).
<Workspace theme="dark" navigation="free" storageKey="app" version={1} ref={wsRef}>
{/* <ViewType>s, layout components, <Workspace.Empty>, <Workspace.Chrome>, … */}
</Workspace>| Prop | Type | Default | Description |
|---|---|---|---|
children |
ReactNode |
View types, layout components, and the slots Workspace.Empty, Workspace.Backdrop, Workspace.StageEmpty and Workspace.Chrome. |
|
theme |
"light" | "medium" | "dark" | "darker" | "system" |
"system" |
The built-in theme. |
tokens |
Record<string, string> |
CSS custom properties set on the workspace root. A new object replaces the previous set, so omitted tokens are removed, and an empty string removes a token. | |
floating |
false | "overlay" | "stage" |
"overlay" |
The layer floating panels live in. |
navigation |
false | "focus" | "free" |
"focus" |
The navigation mode. "free" adds gestures and a default 480 × 320 content minimum. See Navigation. |
motion |
"system" | "full" | "reduced" |
"system" |
The animation policy. |
keymap |
Keymap |
Changes key bindings with a partial map of command to combo. null disables a command. |
|
panelMenu |
boolean | (entries, context) => MenuEntry[] |
true |
Turns the built-in menu items on or off, or edits every panel's menu with a function. See Panel menus. |
detail |
false | { size?, outline? } |
{ size: 48, outline: 2 } |
Collapses a nested group into one tile when all its parts are smaller than size × size on screen. See Zoomable layouts. |
keepPushed |
boolean |
true |
Whether panels pushed by a divider stay pushed when it's dragged back. false makes each drag work from the layout it started with, so dragging back undoes the pushes. |
gestureKeys |
{ pan?, scale?, rect?, step? }, each a combo, a list or null |
defaultGestureKeys() |
Keys held to pan (drag), scale (drag), zoom to a rectangle (drag) and step (scroll) under free navigation. See Gesture keys. |
renderMenu |
(request: MenuRequest) => void |
Shows panel menus with your own component instead of the built-in one. See Panel menus. | |
tabs |
{ fill?: boolean; inset?: number } |
{ fill: false, inset: 4 } |
Sets the tab style. fill stretches tabs across the tab row, and inset is the space around them in pixels (0 is full-bleed). See Theming. |
hideToward |
(panelId: string) => Element | Rect | null | undefined |
Picks where the built-in menu's Hide animates to, such as your dock or tray button. See Hiding. | |
label |
string |
"Workspace" |
The accessible name of the workspace region. |
onMissingType |
(type, id) => "drop" | "placeholder" |
"placeholder" |
Decides what happens to a view whose type isn't registered. See Unknown view types. |
storageKey |
string |
Saves the layout to localStorage under this key and restores it from there. Read once, when the workspace mounts. |
|
version |
string | number |
Your layout version. A saved layout with a different version is discarded. Read once, when the workspace mounts. |
|
defaultLayout |
LayoutDocument | LayoutSpec | null |
The initial layout when nothing is persisted or passed as document. JSX layout children take precedence. Read once, when the workspace mounts. |
|
document |
LayoutDocument |
The controlled layout. It overrides persistence. Pair it with onDocumentChange. |
|
onDocumentChange |
(doc: LayoutDocument) => void |
Called with each committed layout change. | |
onOpen |
(view: ViewInfo) => void |
Called when a view is added. | |
onClose |
(view: ViewInfo) => void |
Called when a view is removed. | |
onFocus |
(viewId: string | null) => void |
Called when focus moves. | |
onNavigate |
(framed: string | null) => void |
Called when the camera frames a node or returns to the overview. | |
className, style |
Applied to the host div. |
||
ref |
Ref<WorkspaceHandle> |
The imperative handle. null until the workspace mounts. |
theme, tokens, tabs, detail, floating, navigation, motion, keymap, panelMenu, renderMenu and label can change at any time. Event props, onMissingType, hideToward, a panelMenu function and renderMenu always call the latest function.
Renders its children when the workspace has nothing in it:
<Workspace.Empty>
<button onClick={openDefault}>Open a document</button>
</Workspace.Empty>Render into the stage's slots. <Workspace.Backdrop> renders behind the stage's panels and floats, and <Workspace.StageEmpty> renders over the stage while it has no panels.
They do the same as <Stage backdrop> and <Stage empty>, and they also work when the layout comes from defaultLayout data or persistence. If both are given, the <Stage> props win.
<Workspace defaultLayout={L.stage()}>
<Workspace.Backdrop>
<Wallpaper />
</Workspace.Backdrop>
<Workspace.StageEmpty>
<button onClick={openDefault}>New document</button>
</Workspace.StageEmpty>
</Workspace>Renders a full-size layer above the workspace for your own overlays, such as a dock, minimap, HUD or toolbar. The layer itself ignores the pointer, but its direct children receive pointer events. Content here can use the workspace hooks.
The chrome layer lives inside the workspace and is clipped to it. For app-wide overlays, such as menus that extend past the workspace or a toolbar above it, render outside <Workspace> and use <WorkspaceProvider>.
<Workspace.Chrome>
<Dock />
</Workspace.Chrome>Makes the workspace hooks available to UI outside <Workspace>, such as app bars, menus, status bars and a dock. Wrap both the workspace and that UI:
import {
WorkspaceProvider,
Workspace,
useOptionalWorkspace,
useWorkspaceSelector,
} from "@danfessler/trellis-react";
function StatusBar() {
const views = useWorkspaceSelector((s) => s.views.length);
return <footer>{views} open</footer>;
}
function NewButton() {
const ws = useOptionalWorkspace(); // null until the workspace mounts
return (
<button disabled={!ws} onClick={() => ws?.open("doc")}>
New
</button>
);
}
<WorkspaceProvider>
<NewButton />
<Workspace>{/* … */}</Workspace>
<StatusBar />
</WorkspaceProvider>;Under a provider, useWorkspaceState() and useWorkspaceSelector() return an empty snapshot until the workspace mounts. useWorkspace() throws until then, so use useOptionalWorkspace() in UI that can render first. A provider serves one workspace.
Registers a kind of view. It renders nothing itself and must be a direct child of <Workspace>. Fragments are fine.
<ViewType id="doc" title={(v) => String(v.params.path)} placement="stage" icon={<FileIcon />}>
<Editor />
</ViewType>| Prop | Type | Default | Description |
|---|---|---|---|
id |
string |
Required. The type id that <View type> and open(type) refer to. |
|
title |
string | (view: ViewHandle) => string |
The type id | The tab label. |
icon |
ReactNode | string |
The tab icon: a React node, or trusted SVG/HTML markup. Without one, the empty icon slot takes no space. | |
children |
ReactNode |
The content, rendered once per view. Use useView() inside. |
|
render |
(view: ViewApi) => ReactNode |
Renders content from the view's state, as an alternative to children. |
|
accessory |
ReactNode | (view: ViewApi) => ReactNode |
Content for the tab bar while this view is selected. | |
iframe |
string | IframeOptions | (view: ViewHandle) => string | IframeOptions |
Renders an iframe instead: a URL, or IframeOptions. It reloads only when the computed options change. |
|
mount |
(element, view, parts) => void | (() => void) |
Renders vanilla content instead of React content. See ViewTypeDefinition. |
|
menu |
MenuEntry[] | (view: ViewHandle) => MenuEntry[] |
Items at the top of the panel menu while this view is selected. | |
placement |
Placement |
The default placement for open(). |
|
allow |
{ stage?, side?, floating? } |
All true |
The regions users may drop this view into. |
singleton |
boolean |
Allows at most one instance. open() focuses the existing one. |
|
closable |
boolean |
false removes the close button and ignores close commands. |
|
minSize |
{ width, height } |
480 × 320 with navigation="free", none otherwise |
The smallest size content lays out at. Below it, content scales down. |
scaling |
"interactive" | "inert" | false |
"interactive" |
Below minSize: scaled content takes input, ignores it ("inert"), or isn't scaled at all (false). See Small panels. |
tabbar |
"always" | "auto" | "never" | "overlay" |
"always" |
When the tab bar shows. See Tab bar modes. |
gestures |
"content" | "exclusive" | "workspace" |
"content" |
Who gets gestures over the content under free navigation. "exclusive" keeps pinch for the content; "workspace" lets a plain scroll step the workspace. See Gesture ownership. |
className |
string |
An extra class on the view's surface. |
When a type has more than one kind of content, iframe wins, then mount, then render, then children.
Children render through portals, so they see React context from above <Workspace>. Your stores, routers and themes work as usual.
Changing props doesn't remount content. Function props (title, iframe, mount and menu) go through stable wrappers that read the latest render, so inline arrow functions are fine. Content remounts only when an iframe's computed options change, or when you switch a type between iframe, mount and React content.
An item in a panel menu, used by menu, panelMenu and renderMenu:
type MenuEntry = MenuItem | "separator";
interface MenuItem {
id?: string;
label: string;
shortcut?: string; // a hint such as "⌘S", which doesn't bind the key
disabled?: boolean;
checked?: boolean;
danger?: boolean;
run?(): void;
items?: MenuItem[]; // a submenu
}See Panel menus for how view type items, the built-ins, panelMenu and renderMenu fit together.
Layout components describe the initial layout. Read once, when the workspace mounts. See Layout.
| Component | Props |
|---|---|
<Split> |
axis?: "x" | "y" (default "x"), weights?: number[], id?: string. Children: layout components. |
<Panel> |
selected?: number, id?: string. Children: <View>s. |
<View> |
type: string, params?: object, id?: string, title?: string. A <View> outside a <Panel> gets its own panel. |
<Stage> |
backdrop?: ReactNode, empty?: ReactNode, id?: string. Children: layout components. At most one per workspace. |
<Floating> |
rect: { x, y, w, h } (fractions 0 to 1 of the layer), layer?: "stage" | "overlay" (default "overlay"). Children: one <Panel> or <View>. |
<Stage backdrop> renders behind the stage's panels, such as a canvas, wallpaper or grid. <Stage empty> renders when the stage has no panels. Both can use the workspace hooks.
<Floating> declares an initial floating panel. Place it anywhere among the workspace's children:
<Workspace floating="stage">
<ViewType id="color" title="Color" />
<Stage />
<Floating rect={{ x: 0.65, y: 0.55, w: 0.3, h: 0.4 }} layer="stage">
<View type="color" />
</Floating>
</Workspace>function useView<P extends object = Params>(): ViewApi<P>;Returns the view whose content is rendering. The result merges the ViewHandle methods with the view's current state, and re-renders when that state changes. Presentation state, such as size and visibility, settles after motion, so the hook doesn't re-render on every animation frame. Throws outside view content.
function Canvas() {
const view = useView<{ file: string }>();
// view.id, view.type, view.params.file
// view.size.width, view.size.height, view.scale
// view.visible, view.focused, view.selected, view.interactive
// view.placement: "docked" | "stage" | "floating" | "hidden"
// view.title, view.badge, view.panelId, view.workspace
// view.setTitle(), view.setParams(), view.setBadge(), view.focus(), view.close(), view.hide()
// view.guardClose(), view.on("resize", …), view.subscribe(), view.getState()
return <canvas width={view.size.width} height={view.size.height} />;
}You can declare params as an interface. The generics accept any object type:
interface DocParams {
path: string;
readOnly?: boolean;
}
<ViewType<DocParams>
id="doc"
title={(v) => v.params.path}
render={(view) => <Editor path={view.params.path} />}
/>;
function Toolbar() {
const view = useView<DocParams>();
return <span>{view.params.readOnly ? "Read only" : view.params.path}</span>;
}function useOptionalView(): ViewHandle | null;Returns the raw view handle, or null outside view content. It doesn't subscribe to state. Use it in shared components that may or may not render inside a view.
function useViewTitle(title: string | null | undefined): void;Keeps the tab title in sync with a value. Empty values are ignored. Trellis stores the title in the document, like setTitle().
useViewTitle(dirty ? `${name} •` : name);function useViewBadge(badge: string | number | boolean | null | undefined): void;Shows a badge on the view's tab, and clears it on unmount. Pass true for a dot, the usual marker for unsaved changes. The dot swaps with the close button on hover.
useViewBadge(dirty); // a dot while dirty
useViewBadge(errors || null); // a count, or nothingfunction useCloseGuard(guard: () => boolean | Promise<boolean>): void;Adds a close guard. Return or resolve false to keep the view open. The guard runs for the close button, menus, shortcuts and close(), but not for close({ force: true }). The hook always calls the latest guard.
useCloseGuard(async () => !dirty || (await confirmDialog("Discard changes?")));function useWorkspace(): WorkspaceHandle;Returns the workspace's imperative handle. It works inside view content, stage slots, Workspace.Chrome and Workspace.Empty. It also works anywhere under a <WorkspaceProvider> after the workspace mounts. Elsewhere it throws, so use the ref prop instead.
function useOptionalWorkspace(): WorkspaceHandle | null;Returns the workspace's handle, or null where useWorkspace would throw.
function useWorkspaceState(): WorkspaceSnapshot;Returns the whole snapshot, which covers layout, focus, hidden panels and navigation. It re-renders on every change. For narrow reads, use useWorkspaceSelector.
function useWorkspaceSelector<T>(select: (s: WorkspaceSnapshot) => T, equal?: (a: T, b: T) => boolean): T;Returns part of the snapshot, and re-renders only when that part changes. Values are compared with Object.is, or with your equal function.
const framed = useWorkspaceSelector((s) => s.framed);
const docCount = useWorkspaceSelector((s) => s.views.filter((v) => v.type === "doc").length);
const hiddenIds = useWorkspaceSelector(
(s) => s.hidden.map((h) => h.panelId),
(a, b) => a.join() === b.join(),
);layout, createDocument and formatCombo are re-exported from the core, so React apps need only one import. See Core API.
<Workspace> renders an empty host div on the server and creates the workspace after hydration. View content renders on the client only.