building per-project workspaces with KDL layouts: panes, tabs, sizes, cwd, environments, and commands, plus ready-to-use developer, server admin, and floating pane layout case studies.

Welcome to episode 10 of the Learn Zellij series! In episode 9 you learned to arrange keys — personal, conflict-free keybindings. Now it's time to arrange the room: KDL layouts and project workspaces. If keybindings are how you press shortcuts, a layout is the blueprint of a workspace: who sits where, which directory is opened, which command runs, and how it all comes together in one shareable file.
There are five things you'll master in this episode: (1) the basic KDL layout syntax with layout, pane, and tab nodes, (2) setting size, split direction, cwd, environment, and command, (3) running layouts with zellij -l and setting a default layout, (4) three real case studies: a developer workspace, a server admin workspace, and a layout with commands plus a floating pane, and (5) patterns that avoid common mistakes. Zellij uses KDL as its configuration language — no YAML since 0.40.
Why do layouts matter? Without layouts, every time you start work you have to rearrange panes, open an editor, run a dev server, adjust sizes — fifteen minutes repeated every day. With layouts, the whole process becomes one command. This isn't just convenience; it's discipline: a deterministic workspace means every person on the team, and every machine, starts from the same point. If episodes 8 and 9 were the foundation, this episode is the building frame. Let's begin.
A layout is a text file that defines the arrangement of Zellij panes and tabs. Its main structure is the global layout node, and inside it live pane and tab nodes. A pane can be a plain shell, a specific command, a plugin, or a logical container holding other panes. The split_direction attribute sets the direction of the dividing line between children: "vertical" means child panes sit side by side left-right, while "horizontal" stacks them top-bottom.
layout {
pane split_direction="vertical" {
pane
pane
}
}The example above opens two shell panes side by side. Notice that split_direction is placed on the parent pane and applies to its children. Deeper layouts are just a matter of nesting nodes: pane inside pane inside pane. Every pane without a command runs your default shell — usually $SHELL — and each pane is its own PTY, just like panes you create manually with Alt n.
layout {
tab name="kod" {
pane split_direction="vertical" {
pane
pane split_direction="horizontal" {
pane
pane
}
}
}
tab name="ops" {
pane
}
}The tab node lets one layout open several tabs at once, each with its own name and cwd. This is exactly like defining "pages" in a single file. If you write no tab at all, all panes sit in one default tab. Node order determines visual order, and the focus=true attribute can point to which pane receives focus first.
An arrangement without sizes will be split evenly by Zellij, but in practice you almost always want some panes to be more dominant. The size attribute accepts two forms: an integer for fixed size in characters, or a percent string like "60%" for a proportion of the space.
layout {
pane size="60%" split_direction="vertical" {
pane focus=true
pane split_direction="horizontal" {
pane size=5
pane
}
}
}Here the left column takes 60% of the screen width, and inside the right column there's a small strip five lines tall on top and another pane below it. Combining size at different levels gives you granular control: percentages for dividing main space, fixed numbers for strips like status monitors or input bars.
| Attribute | Value | Function |
|---|---|---|
split_direction | "vertical" / "horizontal" | Direction of child separation |
size | 4 or "60%" | Fixed/percent size within the container |
focus | true | Pane receives focus when the layout loads |
name | string | Display name of the pane/tab |
borderless | true / false | Hide the pane frame |
start_suspended | true / false | Start the command in a suspended state |
One important rule: a size of zero is rejected by Zellij, and the value must be a positive number or a valid percent. Small errors here only produce a clear parsing error — just follow the message.
Note
The definition order in KDL matches the visual order. If a layout feels "backwards", check the split direction on the parent pane: "vertical" produces side-by-side children (left-right), while "horizontal" stacks them (top-bottom). A helpful mental model: imagine split_direction as the direction of the line that splits the parent, not the flow direction of children.
A pane that only opens a shell is often not enough — you want panes running specific commands in specific directories with specific environments. The command attribute points to an executable, args holds its arguments, and cwd sets the working directory. For per-pane environments, Zellij has supported the env_vars node since 0.42.
layout {
pane cwd="/home/arman/dev/belajar-zellij" split_direction="vertical" {
pane focus=true
pane {
command "npm"
args "run" "dev"
env_vars {
PORT "5173"
NODE_ENV "development"
}
}
}
}This example opens an editor in the left pane and runs npm run dev with an environment injected specifically for that pane. cwd behavior follows a composition rule: a relative cwd on a pane is combined with the cwd on the tab and layout, while an absolute cwd overrides everything. You can also put a global cwd on the layout node so all panes start from the same directory — very practical for per-project layouts.
There are two attributes often misunderstood. start_suspended true makes the command pane wait until you resume it — useful for making sure the environment is ready before the command runs. Meanwhile, a pane with command closes automatically when the command exits. For short commands like git status, the pane disappears instantly; for long commands like a dev server, the pane lives as long as the server runs. If you want the pane to stay open after the command finishes, wrap the command in a shell — for example command "bash" with args "-c" "npm run dev; exec $SHELL".
Tip
Don't run commands that finish immediately in a layout — the pane closes as soon as the command exits and you lose the space. Choose long-running commands (dev server, tail -f, watchers), or wrap short commands with an interactive shell so the pane stays alive. The command "bash" + args "-c" pattern is a trick used by many community dotfiles.
A written layout is stored as a .kdl file. Zellij looks for layouts in the ~/.config/zellij/layouts/ directory — and by default loads default.kdl from there every time a session starts. For other layouts, use the -l or --layout flag with the file name without the extension, or a full file path.
zellij -l devNote the behavior difference since 0.44: zellij -l dev inside a running session adds the layout as a new tab, while outside a session it starts a new session. If you always want a new session, use --new-session-with-layout. Zellij also provides --layout-string to write a layout directly on the command line without a file, and zellij setup --dump-layout compact to copy a built-in layout as a learning reference.
To set the layout loaded every time a session starts, set the default_layout option in config.kdl:
default_layout "dev"The value "dev" refers to ~/.config/zellij/layouts/dev.kdl. If the file doesn't exist, Zellij falls back to the built-in default layout. This is the fastest way to make every Zellij session always start with the same workspace.
Let's put it all together in the first case study: a layout for web application development. The goal: an editor on the left, a dev server and git on the right — a workspace that's immediately productive without a single click.
layout {
pane cwd="/home/arman/dev/belajar-zellij" split_direction="vertical" {
pane size="60%" focus=true
pane split_direction="horizontal" {
pane size="50%" {
command "npm"
args "run" "dev"
}
pane {
command "lazygit"
}
}
}
}With this layout, one command zellij -l dev produces three panes: a neovim editor (focused), npm run dev as the dev server, and lazygit for version control. Because cwd is placed on the parent pane, all children use the project directory. You can add a fourth pane for logs with tail -f or journalctl, and add it to this layout — Zellij will divide according to the specified sizes.
The second case study is a server admin layout — very useful for monitoring production or staging machines without opening a dozen tabs.
layout {
pane split_direction="vertical" {
pane focus=true
pane split_direction="horizontal" {
pane command="htop"
pane command="journalctl" args="-f" "-u" "nginx"
}
}
}The left pane is a shell for executing commands, while the top-right monitors resources with htop and the bottom-right follows nginx logs with journalctl -f. For remote machines, run this layout after SSH, or make the main pane a command "ssh" — remember that command panes automatically re-run when a session is resurrected, so after detach this monitoring layout can come back independently.
Important
When a layout is re-run through session resurrection, all command panes execute again from scratch. This is great for watchers and servers, but dangerous for commands whose effects aren't idempotent — for example database migrations or commands that write files. Put dangerous commands like those in a regular shell pane, not a command pane, so you decide when to run them.
The final case study shows two capabilities often combined: commands with close_on_exit and floating panes with percent coordinates.
layout {
tab name="main" {
pane
floating_panes {
pane x="25%" y="20%" width="50%" height="30%" {
command "npm"
args "run" "dev"
close_on_exit false
}
}
}
}The floating_panes node inside a tab defines panes that float above the tiled panes — following x, y, width, and height coordinates in screen percent. The close_on_exit false attribute keeps the pane open even after the command finishes, while without it the pane closes automatically when the command exits. For a floating pane that's always on top of the stack, add pinned true — the 0.42 feature we met in episode 7. Outside layouts, the same pattern is available at runtime via zellij run or a Run keybinding with a child block containing floating true.
| Pattern | Usage |
|---|---|
command + close_on_exit false | Worker panes that survive after the command finishes |
floating_panes + percent coordinates | HUDs, pickers, or floating editors |
pinned true | A floating pane always in front |
start_suspended true | Commands waiting for a manual trigger |
split_direction with a nonexistent value. Zellij only accepts "vertical" and "horizontal" — values like "right" or "left" will be rejected by the parser. Fix: remember this attribute directs the dividing line, not the direction of children.git status, ls, or date immediately close their pane. Fix: use long-running commands, or wrap with bash -c and exec $SHELL so the pane persists.cwd composition. A relative cwd is merged upward; if there's no cwd on the parent, panes start from the directory where Zellij was launched. Fix: set a global cwd on layout for every per-project layout.zellij -l dev only finds layouts in ~/.config/zellij/layouts/ or the given path. Fix: store without an extension in that directory, and check the exact location with zellij setup --check.close_on_exit false, start_suspended, or wrap with an interactive shell.Warning
One layout defines one whole session — including commands that immediately execute something. Before using a layout on a production machine or as a default layout, test it first in an empty directory and read the commands line by line. A wrong layout can have far bigger effects than just wrong pane display.
In this episode 10 you've turned Zellij from a manual terminal workspace into a scriptable workspace: KDL layouts with pane and tab, split_direction and size settings, commands with cwd and env_vars, running via zellij -l, and three real case studies — a developer workspace with an editor and dev server, a server admin with htop and journalctl, and a layout with commands and a floating pane.
Key takeaways:
layout, pane, tab, split_direction, size.cwd follows composition rules; set it globally on layout for per-project workspaces.zellij -l loads a layout; default_layout in the config sets the startup layout.pinned true for an always-front HUD.Your workspace can now be born with one command. In episode 11 next, we tidy up its appearance: Status Bar, Tab Bar, and Theme Customization — configuring the tab-bar, status-bar, compact-bar with tooltips, the latest theme spec, the built-in dracula and catppuccin themes, up to custom themes and dark-light auto-switching. See you in episode 11!