Sway School
A tree-first tutorial for sway beginners. Assumes the keybindings from this dotfiles config ($mod = Super).
Introduction
Sway is a tiling Wayland compositor. The default reaction to it is "I don't get how layouts work." This tutorial fixes that β not by listing keys, but by teaching you the tree model sway uses internally. Once you see the tree, every keystroke becomes obvious.
The lessons build on each other. Each one ends with a quiz that lets you check your understanding before moving on. Don't skip the quizzes β they're how you find out whether the model actually clicked.
The keys you'll use most
| Key | What it does |
|---|---|
| $mod+t | Open a terminal |
| $mod+h/j/k/l or $mod+β/β/β/β | Move focus left/down/up/right |
| $mod+Shift+h etc. or $mod+Shift+arrows | Move the focused window |
| $mod+[ / $mod+] | Next window goes below / right |
| $mod+c | Focus the parent container |
| $mod+Shift+space | Toggle floating |
| $mod+Shift+q | Close the focused window |
The full list is in the cheat sheet at the bottom.
L1A window is just a rectangle
Open one terminal: $mod+t. It fills the screen, minus a small gap.
One window takes all the space available to it.
That's the whole lesson. No tree, no containers β just one window filling its space.
Quick check
You open one terminal on an empty workspace. What do you expect?
L2A second window has to share the space
Open another terminal. Sway picks a direction (default: side-by-side) and shrinks the existing window to make room.
Sway never overlaps tiled windows. Space is always divided, never stacked.
This is the foundational difference between a tiling WM and a traditional one. There's no z-order to fight with for tiled windows.
Quick check
With two terminals open side-by-side, you open a third. What happens?
L3Sway remembers the arrangement
Sway didn't happen to put your three windows in a row. It decided "these go horizontally" and remembers that decision. Open a fourth terminal β it joins the row, four side-by-side.
Sway is keeping a note: "these windows are arranged horizontally." That note will eventually have a name: container (L8).
For now, just hold the idea: arrangements are remembered, not coincidental.
L4Focus: sway always knows which window is active
One window has a coloured border (blue in the Catppuccin theme). That's the focused window β the one keystrokes go to.
Move focus with either of these β they're bound identically:
- $mod+h / j / k / l (vim-style: left/down/up/right)
- $mod+β / β / β / β (arrow keys, exactly the same)
This config has focus_wrapping no, so focus stops at the edge. Press $mod+h (or $mod+β) on the leftmost window and nothing happens β you're already as far left as you can go.
Quick check
Which statement about focus is true in this config?
focus_follows_mouse yes is set, but it's not the only way.)L5New windows appear next to the focused one
The single most important rule:
A new window opens next to whichever window is currently focused, in whatever arrangement that window lives in.
Proof: with three terminals A, B, C in a row, focus the leftmost (A) and open a new terminal. The new window appears between A and B, not at the end of the row. Focus is the anchor.
Quick check
Three terminals in a row, side-by-side. You focus the middle one and open a fourth terminal. Where does it appear?
L6You choose the direction before opening the next window
So far sway has been picking the direction (horizontal). Now you take the wheel.
| Key | Whisper to sway |
|---|---|
| $mod+] | Next window opens to the right (horizontal) |
| $mod+[ | Next window opens below (vertical) |
These keys do nothing visible by themselves. They just set up the direction for the next window you open.
Recipe to build a column: focus a window β $mod+[ β $mod+t. The new terminal appears below.
L7Arrangements stick to their spot
Once you've established a direction in a slot (e.g. made a vertical column), new windows opened inside that slot keep going in that direction β without needing the bracket key again.
Proof: focus a window inside your vertical column, hit $mod+t, and the new terminal stacks into the column rather than popping out sideways.
The slot knows what it is. Sway carries the arrangement forward until you change direction again with [ or ].
This is the moment the mental model clicks for most people. Everything from here on is variations on this one idea.
Quick check
You built a 3-window column with $mod+[. Now you focus the middle window and press $mod+t (no bracket key first). What happens?
L8Containers β the tree, finally named
That "slot that knows what it is" has a name: a container.
A container = a remembered arrangement, with windows (or other containers) inside it.
Containers can hold:
- Windows (the things you actually see), or
- Other containers (which themselves hold windows or more containers).
That nesting is the tree. Russian dolls of remembered arrangements.
See your tree
Run this in any terminal:
swaymsg -t get_tree | grep -E '"(name|layout)"' | head -40
Read the output as nested boxes:
| Field | Means |
|---|---|
"layout": "splith" | horizontal container (children side-by-side) |
"layout": "splitv" | vertical container (children stacked) |
"layout": "tabbed" | tabbed container (children stacked as tabs) |
"layout": "none" | a leaf β a real window, not a container |
"name": "..." | a window's title (containers have null names) |
Worked example
A workspace with one tile on the left and a vertical column of two tiles on the right looks like this in the dump:
"name": "4", β workspace 4
"layout": "splith", β workspace is horizontal
"layout": "none",
"name": "Ο - dotfiles", β a window (left side)
"layout": "splitv", β a vertical container (the column!)
"layout": "none",
"name": "Chrome", β window in column
"layout": "none",
"name": "swaymsg β¦", β window in column
As a picture:
Quick check
In the worked example above, how many containers are there (not counting leaf windows)?
L8.5The correct mental model for splith / splitv
This trips up almost everyone. The naming convention:
splith= children laid out horizontally (a side-by-side row)splitv= children laid out vertically (a stacked column)
The name describes how the children are arranged, NOT the direction of the divider line between them.
Why it feels backwards
Most people's first instinct is to picture the divider:
- "vertical split" β a vertical line down the middle β windows side-by-side
- "horizontal split" β a horizontal line across the middle β windows stacked
That's the CSS / Photoshop / "split a board in half" intuition. It's perfectly reasonable β it's just the opposite of sway's. Sway names from the result's point of view: see a row β splith. See a column β splitv.
Cheat table
| You want⦠| Key | Container becomes | Tree dump shows |
|---|---|---|---|
| Next window to the right | $mod+] | horizontal row | "layout": "splith" |
| Next window below | $mod+[ | vertical column | "layout": "splitv" |
Quick check
You see "layout": "splitv" in a tree dump. What does that container look like on screen?
L8.75The insertion rule (the one rule to rule them all)
Everything you've learned so far is consequences of a single rule.
When you open a new window, sway:
1. Looks at the focused window
2. Finds the container that window lives in (its parent)
3. Inserts the new window into that container, right after the focused one
The new window is always a sibling of the focused window β same parent, immediately after.
The bracket-key twist
The bracket keys ($mod+[ and $mod+]) don't just "set a direction." They do something sneakier:
They ensure the focused window's parent is of the requested type, wrapping the focused window in a new container if it isn't.
So if you're focused on A inside splith [ A B C ] and hit $mod+[, sway silently rewraps A:
Now opening a new terminal places it inside the new splitv (next sibling of A):
Sway is smart: brackets are no-ops when the parent already matches
If the focused window's parent is already the requested layout, the bracket key does nothing. No new wrapping, no nested-container tower.
Hit $mod+[ ten times in a row inside a splitv β the tree is unchanged after the first one (which itself was a no-op if you were already in a splitv). This is why your tree never gets bloated from idle keypresses.
The full table
| Current parent layout | You press | What happens |
|---|---|---|
| splith | $mod+] | no-op (already horizontal) |
| splith | $mod+[ | wrap focused window in a new splitv |
| splitv | $mod+[ | no-op (already vertical) |
| splitv | $mod+] | wrap focused window in a new splith |
| tabbed | $mod+[ | wrap focused window in a new splitv |
| tabbed | $mod+] | wrap focused window in a new splith |
Quick check 1
You're focused on a window inside a splitv column. You press $mod+[ ten times in a row, then open a new terminal. What happens?
Quick check 2
You're focused on window A. The tree is splith [ A B C ]. You press $mod+[, then open a new terminal called NEW. What does the tree look like?
L9Focus parent / focus child
Until now, focus has always meant "a window is highlighted." But the tree has more than just windows in it β it has containers too. And sometimes you want to operate on a whole container instead of one window inside it.
| Key | Action |
|---|---|
| $mod+c | Focus parent (zoom out one level up the tree) |
| $mod+Ctrl+c | Focus child (zoom back in) |
What you'll see
Focus a window normally β the border wraps just that window. Press $mod+c: the border expands to wrap the entire parent container, possibly multiple windows. That's sway saying "focus is now on the container, not the window."
Press $mod+c again β expands further to the grandparent, and so on up to the workspace itself.
Press $mod+Ctrl+c to walk back down. Sway remembers which child you came from.
Why this matters
When focus is on a container, commands act on the whole container:
- Send a whole column to another workspace: focus a column container, hit $mod+Shift+3 β the entire subtree moves.
- Tab a whole group: focus a row, hit $mod+Ctrl+f β every direct child becomes a tab.
- Wrap a whole subtree in a new split: focus a row, hit $mod+[ β the entire row becomes the top child of a new splitv.
- Float a whole group: focus a container, hit $mod+Shift+space β the whole subtree floats together.
Quick check
You have a row of 5 windows, all siblings (no nesting). You focus one, press $mod+c, then press $mod+Shift+space (toggle floating). What floats?
L10Mutating the tree (changing what's already there)
You can build trees (L6, L7) and read trees (L8). Now: how to change them after the fact.
Changing a container's layout (the easy mutation)
Focus any window. The window's parent container is what these commands act on:
- $mod+Ctrl+f β set parent layout to tabbed
- $mod+period β toggle parent layout between tabbed and split
- $mod+] / $mod+[ β ensure parent is splith / splitv (wrapping if needed; see L8.75)
Changing the tree shape (the powerful mutation)
Combine $mod+c from L9 with bracket keys from L8.75 to wrap whole subtrees. Recipe to add a status-bar-style window across the bottom of an existing layout:
- Focus any window in the layout.
- $mod+c until the border wraps the entire workspace content (everything you want above the new bar).
- $mod+[ β wraps that whole subtree in a fresh splitv.
- $mod+t β new terminal appears below the entire layout.
Without the $mod+c step, the bracket key would only wrap one window. $mod+c is what makes mutations apply at the right scope.
Tabs that hold containers
A subtle but mind-bending consequence of L8.75 + L10: when you tab a row that contains a column, you get tabs where one of the tabs is the entire column. Click that tab and the screen fills with the column rendered as a splitv. You can build trees of arbitrary nesting, mixing splits and tabs, for any layout you want.
Quick check
You have a row of 3 windows. You focus the middle one and press $mod+Ctrl+f (tabbed). What happens?
L11Moving windows through the tree
You already know $mod+Shift+hjkl moves a window. Now we look at what it actually does.
$mod+Shift+<direction> walks the focused window through the tree in that direction β swapping with siblings, escaping outward, or entering inward as needed.
It's the inverse of $mod+<direction>: arrow keys read the tree, shift+arrows rewrite it.
The three cases
When you press $mod+Shift+l (move right), one of these happens:
Case A β swap with a sibling
If there's a sibling immediately to the right in the same parent, they swap places.
Case B β escape outward
If there's no sibling that way, but a parent further up has space, the window pops out of its container.
Case C β enter a sibling container
If the next sibling in that direction is itself a container, the window dives into it.
Bonus case β escape from a perpendicular layout
If you're inside a splitv and press "right," sway walks up the tree until it finds a parent that supports horizontal movement, then drops the window there.
Sway never gets stuck β it just keeps escaping until the direction makes sense.
Quick check
Tree is splith [ A B* splitv[ C D ] ] with B focused. You press $mod+Shift+l (move right). What happens?
L12Floating windows
A floating window lives outside the tree. It has no parent, no siblings, no layout. It's just a rectangle that sits on top of everything tiled.
That's the whole concept. Everything else is consequences.
Toggle and verify
- $mod+Shift+space β toggle the focused window between tiled and floating (i3/sway default).
Verify it actually floated by looking for "type": "floating_con" in the tree dump:
swaymsg -t get_tree | grep -B 1 '"type":' | grep -E '"(name|type)"'
Mouse: hold $mod and drag
- $mod + left-click drag anywhere on the window β move it
- $mod + right-click drag β resize it
Requires floating_modifier $mod normal in your config.
Two stages per workspace
$mod+hjkl only navigates within the layer you're in (tiled or floating). To cross between them:
- $mod+a β
focus mode_toggle(jump between tiled tree and floating layer) - Mouse β just hover, since
focus_follows_mouse yes - $mod+Tab β the fuzzel windows picker
Each workspace has two stages: tiled (back) and floating (front). $mod+a is the curtain between them.
Un-float placement gotcha
When you $mod+Shift+space a floater back to tiled, it's treated as a brand new window: inserted via the L8.75 rule as the next sibling of the most recently focused tile. Sway does not remember where the window came from before floating.
To control placement: $mod+a into the tree, walk to the desired neighbour, $mod+a back to the floater, then $mod+Shift+space.
Keyboard resize β use px, not ppt
ppt (percentage points) only works on tiled windows. resize β¦ 10 ppt errors out on a floater with "Floating containers cannot use ppt measurements". The robust pattern is to always use px β sway clamps tiled windows to layout constraints, so it does the right thing for both.
Auto-floating rules
Use for_window to auto-float specific apps. Find the app_id with:
swaymsg -t get_tree | jq -r '.. | select(.type?=="con" or .type?=="floating_con") | "\(.app_id // .window_properties.class // "?") :: \(.name)"' | grep -i <app-name>
Then add a rule like:
for_window [app_id="^pavucontrol$"] floating enable
The "summon and dismiss" pattern
For utility popups you summon, type into, and dismiss β audio mixers, calculators, password managers β use a richer rule that floats and sets a known size and position:
for_window [app_id="^wiremix-float$"] floating enable, resize set 720 480, move position center
for_window [app_id="^org\.gnome\.Calculator$"] floating enable, resize set 480 600, move position center
Every launch: float, resize to a known size, centre on the active output. The window appears in the same predictable spot every time, with no manual cleanup.
The for_window cookbook
| Pattern | Use case |
|---|---|
floating enable | Just float; sway picks size and position |
floating enable, resize set W H, move position center | Summon-and-dismiss popup at a fixed size |
floating enable, sticky enable | Always-visible overlay (PiP, etc.) |
move scratchpad | Auto-stash into the drawer (Cider) |
[app_id=β¦], [tiling], [floating] match against the whole tree, not the focused window. They're great for for_window rules and bulk swaymsg scripting, useless for "do X to the focused window only if it's of type Y."
Quick check
A window is floating. You press $mod+Shift+space to un-float it. Where in the tiled tree does it land?
L13Scratchpad
Scratchpad is a hideout for floating utility windows that aren't bound to any workspace. Stash with one key, summon with another.
Bindings
| Key | Action |
|---|---|
| $mod+Shift+n | move scratchpad β stash focused window (it disappears) |
| $mod+n | scratchpad show β summon/hide; cycles through stash |
The model: a flat list of floaters with no workspace
- Scratchpad windows are always floating. Stashing converts a tiled window to floating.
- They have no workspace β they appear on whichever workspace you're on when you summon.
- Each window remembers its size and position. Set once, summons always restore.
- You can stash any number of windows. $mod+n cycles through them; doesn't pop or consume.
Cycling: hidden β A β hidden β B β hidden β A β β¦.
Eviction
No dedicated "unstash" command. Both routes work because they violate the "floating + no workspace" property:
| Method | Why it evicts |
|---|---|
| $mod+Shift+space on a summoned window | Stops floating β must have a workspace β joins current |
| $mod+Shift+1..6 | Has a workspace β no longer no-workspace |
| Close the window | Window doesn't exist |
The killer pattern: auto-stash on launch
For a music player or chat app you always want one keystroke away:
for_window [app_id="^Cider$"] move scratchpad
Launch the app β it briefly flashes, then disappears into the scratchpad. $mod+n summons it whenever needed. Never wastes a workspace slot.
Quick check
You have two terminals stashed in the scratchpad. You press $mod+n three times. What's on screen at the end?
Bonus Β· Sticky
A sticky window stays visible on every workspace you switch to. Only applies to floaters.
This config already uses sticky for Picture-in-Picture videos:
for_window [title="^Picture in picture$"] floating enable, sticky enable, border pixel 2
Pop a YouTube video out into PiP β it floats AND sticks, so it follows you across workspaces. You've been using sticky daily without knowing the name.
Sticky vs scratchpad
| Scratchpad | Sticky | |
|---|---|---|
| Visible by default? | No (hidden) | Yes (always) |
| Per-workspace? | No (no workspace) | No (every workspace) |
| Summon needed? | Yes ($mod+n) | No (just there) |
| Use case | On-demand utility | Always-visible overlay |
Scratchpad = drawer. Pull it out when needed. Sticky = sticker. Always on the window.
If you ever want a manual sticky toggle, add: bindsym $mod+? sticky toggle to a free key.
Reading the matrix
Sway shows you the tree in tab titles when a container has only one child but sits inside wrapper containers. The shorthand:
| Symbol | Meaning |
|---|---|
H[ β¦ ] | splith container |
V[ β¦ ] | splitv container |
T[ β¦ ] | tabbed container |
S[ β¦ ] | stacked container (like tabbed but vertical title list) |
| bare name | a leaf window |
So H[T[Alacritty]] = an H container, holding a T container, holding one Alacritty window. Three nodes nested; only one is visible.
H[V[H[Alacritty]]], sway is literally telling you "this window has three orphan wrappers around it." Most users see noise. You now read it as a diagnostic: one window, three single-child wrappers, all collapsible if you want (use L11 escape moves). This is the moment you realise sway has been showing you the tree all along β you just couldn't read it before.
What to do about orphans
Usually nothing. They don't affect rendering, focus navigation, or resize. They only bite when:
- A bracket key surprises you (extra wrapper changes what "the parent" is)
- Move-window has more layers to escape than expected
- A layout toggle changes scope you didn't intend
The fix in all cases is the same one tool: $mod+c to navigate to the right scope first.
Cheat sheet
Open and close
| Key | Action |
|---|---|
| $mod+t | Open terminal |
| $mod+b | Open browser |
| $mod+i | Open VS Code |
| $mod+space | Run launcher (fuzzel) |
| $mod+Shift+q | Close focused window |
Focus & move
| $mod+h/j/k/l or $mod+β/β/β/β | Focus left/down/up/right |
| $mod+Shift+h/j/k/l or $mod+Shift+arrows | Move focused window |
| $mod+c | Focus parent container |
| $mod+Ctrl+c | Focus child container |
| $mod+a | Toggle focus between tiled tree and floating layer |
| $mod+Tab | Window picker (across all workspaces) |
Layout
| $mod+] | Next window opens to the right (splith) |
| $mod+[ | Next window opens below (splitv) |
| $mod+Ctrl+f | Layout: tabbed |
| $mod+period | Toggle tabbed β split |
| $mod+f | Fullscreen toggle |
| $mod+Shift+space | Floating toggle |
Resize
| $mod+r | Cycle width preset (33% β 50% β 67%) |
| $mod+Ctrl+r | Reset to even split |
| $mod+= / $mod+- | Grow / shrink width |
| $mod+Shift+= / $mod+Shift+- | Grow / shrink height |
Workspaces
| $mod+1..6 | Jump to workspace |
| $mod+Shift+1..6 | Send focused window/container to workspace |
| $mod+Page_Up/Down | Prev / next workspace |
| $mod+Ctrl+Page_Up/Down | Move container and follow |
| $mod+g | Back-and-forth (toggle to last workspace) |
| $mod+Shift+u/i | Move workspace to output left/right |
Scratchpad
| $mod+Shift+n | Stash focused window into scratchpad |
| $mod+n | Summon / hide / cycle scratchpad windows |
System
| $mod+Shift+r | Reload sway config |
| $mod+Shift+slash | Hotkeys cheatsheet (fuzzel) |
| $mod+F5 | Screenshot all |
| Ctrl+Alt+Delete | Session menu |
Mouse (with $mod held)
| $mod + left-drag floater | Move |
| $mod + right-drag floater | Resize (grab from a corner!) |
Inspect the tree
# Compact view
swaymsg -t get_tree | grep -E '"(name|layout)"'
# Find an app's app_id
swaymsg -t get_tree | jq -r '.. | select(.type?=="con" or .type?=="floating_con") | "\(.app_id // .window_properties.class // "?") :: \(.name)"'
# Send commands directly (bypass keybinds)
swaymsg "resize grow width 100 px"
swaymsg "[app_id=firefox] focus"
You've finished sway school. Now go build weird layouts. π³