Getting started
Install dashfoo and run a styled, resizable docking layout in Vite or Next.js.
Start with the default theme and a sized container. You can replace the theme or compose your own chrome once the layout is running.
Step 1: Install
In an existing React application:
npm install @dashfoo/core @dashfoo/react @dashfoo/theme
# or
pnpm add @dashfoo/core @dashfoo/react @dashfoo/themeStarting from scratch? Copy the Vite starter
or Next.js starter
outside the repository, then run npm install and npm run dev (or
pnpm install and pnpm dev). Both include a stateful notes panel and persistence.
The React layer supports React and React DOM 18.3.1 or 19. The Next.js
starter uses React 19. Packages are ESM-only; use a modern bundler and TypeScript
moduleResolution: "bundler" (or node16/nodenext). The drag, resize, and
state dependencies are installed transitively; you do not configure them.
Step 2: Render a complete layout
Replace your Vite src/App.tsx with:
import { model, row, tab, tabset } from "@dashfoo/core";
import { DashfooLayout } from "@dashfoo/react";
import "@dashfoo/react/styles.css";
import "@dashfoo/theme/dashfoo.css";
const layout = model(
row(
[
tabset([tab("welcome", "Welcome"), tab("notes", "Notes")], { id: "main" }),
tabset([tab("help", "Help")], { id: "side" }),
],
{ id: "root" },
),
);
const App = () => (
<div style={{ height: "100dvh", width: "100%" }}>
<DashfooLayout
defaultModel={layout}
factory={(node) => <p>{node.name} content</p>}
floatable
responsive={{ maxWidth: 720 }}
/>
</div>
);
export default App;Replace the generated src/index.css with:
html,
body,
#root {
height: 100%;
margin: 0;
}Remove the generated App.css import if it is still present. Run npm run dev
or pnpm dev. You can now select, close, rename, dock, resize, maximize, and
float panels. Below the responsive breakpoint, the layout stacks and disables
restructuring; tabs still work.
defaultModel is read once at mount. To reset a running layout, use
resetLayout() on its ref; to replace it from application state, use
controlled mode.
Step 3: Render your own content
Tabs contain a component key, not a React element. A registry maps those
keys to components receiving a TabNode:
import type { TabNode } from "@dashfoo/core";
const Welcome = ({ node }: { node: TabNode }) => <p>{node.name}</p>;
const Notes = () => <textarea aria-label="Notes" />;
const Help = () => <p>Drag a tab to rearrange the layout.</p>;
const components = { welcome: Welcome, notes: Notes, help: Help };
<DashfooLayout defaultModel={layout} components={components} keepMounted />;Use either components or factory; factory wins when both are supplied.
Unknown registry keys render an empty panel and warn. By default inactive panels
unmount. Set keepMounted to retain local state across tab switches. This does
not save widget content or guarantee state survives moving a panel to another
tabset; application state that must outlive remounts belongs outside the panel.
A tab's id defaults to its component key. When reusing a component, provide
unique ids: tab("notes", "Second note", { id: "notes-2" }). Builders warn on
duplicates. Use stable explicit row/tabset ids for SSR and any node you address
from application code.
Using Next.js?
Define the dashboard in a "use client" module: factories and component
registries cannot cross the server/client boundary as props. Import the theme
once in the root layout. The complete Next.js starter
shows both files.
// app/dashboard.tsx
"use client";
import { model, row, tab, tabset } from "@dashfoo/core";
import { DashfooLayout } from "@dashfoo/react";
const layout = model(row([tabset([tab("welcome", "Welcome")], { id: "main" })], { id: "root" }));
export const Dashboard = () => (
<div style={{ height: "100dvh" }}>
<DashfooLayout defaultModel={layout} factory={(node) => <p>{node.name}</p>} />
</div>
);Render <Dashboard /> from your page. The persist prop renders the seed on
the server and first client render, then restores saved data after hydration.
See persistence for storage adapters and failure behavior.
Styling and composition
The React layer applies structural positioning and sizing styles. It does not
impose a visual theme. The optional skin is light by default; set
data-dashfoo-theme="dark" on an ancestor for dark mode.
For Tailwind v4, import @dashfoo/theme/tailwind.css after tailwindcss
instead of the prebuilt CSS. To own the appearance, style data-dashfoo
attributes and size both splitter orientations. See theming.
To restructure the chrome itself, compose Layout and Tabset parts.
Troubleshooting
- Blank layout: its parent needs a height. Use the sized wrapper above.
- Unstyled controls or missing splitters: import the theme or supply your own skin.
- Empty panel: check its registry key and console warnings.
- Notes reset on tab switches: enable
keepMounted. - Saved data is invalid or storage is blocked: dashfoo warns and keeps a usable layout. Fix the adapter or clear the saved arrangement.
- Cannot drag on a narrow screen:
responsiveintentionally stacks and locks restructuring.
Docking is pointer-only. Tab selection and the exposed buttons have keyboard paths; this release does not offer keyboard docking or native browser-window popouts. Read the drag guide before choosing dashfoo for a workflow that requires keyboard rearrangement.
What to read next
Concepts · Model · Floating panels · Controlled state and history · Persistence · API reference
Introduction
dashfoo is a headless React docking-layout library: tiled, resizable, tabbed regions with a serializable model and zero imposed styling.
Concepts & terminology
The names dashfoo uses for tabs, panes, rows, splitters, and floating panels, with one annotated figure, the vocabulary, and the same layout as data.