Quiz progress: 0 / 0 answered

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.

How to use this page Read each lesson, try the suggested experiments in a real sway session, then take the quiz. Click an answer to lock it in. Green = correct, red = wrong. Use the sidebar to jump around.

The keys you'll use most

KeyWhat it does
$mod+tOpen a terminal
$mod+h/j/k/l or $mod+←/↓/↑/β†’Move focus left/down/up/right
$mod+Shift+h etc. or $mod+Shift+arrowsMove the focused window
$mod+[ / $mod+]Next window goes below / right
$mod+cFocus the parent container
$mod+Shift+spaceToggle floating
$mod+Shift+qClose 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)
Throughout this tutorial we'll write $mod+hjkl for brevity. Mentally substitute arrows if you prefer them β€” they're bound to the same actions everywhere. Same for $mod+Shift+hjkl (move window) ↔ $mod+Shift+arrows.

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?

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.

Practical takeaway If you want a new window in a specific spot, focus its desired neighbour first, then open it.

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.

KeyWhisper 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.

The unlock This is how you build complex layouts. Pick a direction, open a window. Pick another direction, open another. You're in charge of every shape on screen.

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:

FieldMeans
"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:

Workspace 4 (splith β€” horizontal) β”‚ β”œβ”€β”€ Ο€ - dotfiles ← a window (left side) β”‚ └── (splitv β€” vertical column) ← a container (right side) β”‚ β”œβ”€β”€ Chrome ← a window (top of column) β”‚ └── swaymsg terminal ← a window (bottom of column)
Fun fact Workspaces are containers too β€” just the top-level ones. Everything is containers all the way down.

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.

What to actively forget Stop thinking about the divider line. It will lie to you every time. Re-anchor on "shape of the children" or "direction the next window goes."

Cheat table

You want…KeyContainer becomesTree 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:

splith [ A B C ] ← before ↓ $mod+[ splith [ splitv[ A ] B C ] ← after

Now opening a new terminal places it inside the new splitv (next sibling of A):

splith [ splitv[ A NEW ] B C ] ↑ stacked vertically because parent is splitv

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 layoutYou pressWhat 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.

KeyAction
$mod+cFocus parent (zoom out one level up the tree)
$mod+Ctrl+cFocus 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.
Mental model Focus is a cursor in the tree. $mod+c moves the cursor up; $mod+Ctrl+c moves it down. Whatever it points at β€” window or container β€” is what the next command operates on.

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)
Key insight The windows themselves don't move in the tree. Only the rendering changes. A splith with 4 children becomes a tabbed with 4 children β€” same children, same order, different presentation.

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:

  1. Focus any window in the layout.
  2. $mod+c until the border wraps the entire workspace content (everything you want above the new bar).
  3. $mod+[ β†’ wraps that whole subtree in a fresh splitv.
  4. $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.

splith [ A* B C ] β†’ splith [ B A* C ]

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.

splith [ splitv[ A* B ] C ] β†’ splith [ splitv[ B ] A* C ]

Case C β€” enter a sibling container

If the next sibling in that direction is itself a container, the window dives into it.

splith [ A* splitv[ B C ] ] β†’ splith [ splitv[ A* B C ] ]

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.

splith [ A splitv[ B* C ] ] $mod+Shift+l splith [ A splitv[ C ] B* ] ← B escaped to the outer splith

Sway never gets stuck β€” it just keeps escaping until the direction makes sense.

Mental model Moving a window doesn't move pixels β€” it edits the tree. Pixels are just the rendering of where the window ended up.

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.

⚠️ Mouse-resize rule of thumb Never start a $mod+right-drag from the middle of the window. Always position your cursor near the corner you want to grab first, then drag. The resize anchor follows the quadrant your cursor is in; near the centre it gets jittery.

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

Gotcha The unit 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

PatternUse case
floating enableJust float; sway picks size and position
floating enable, resize set W H, move position centerSummon-and-dismiss popup at a fixed size
floating enable, sticky enableAlways-visible overlay (PiP, etc.)
move scratchpadAuto-stash into the drawer (Cider)
Criteria selectors are tree-wide, not focus filters [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

KeyAction
$mod+Shift+nmove scratchpad β€” stash focused window (it disappears)
$mod+nscratchpad 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:

MethodWhy it evicts
$mod+Shift+space on a summoned windowStops floating β†’ must have a workspace β†’ joins current
$mod+Shift+1..6Has a workspace β†’ no longer no-workspace
Close the windowWindow 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

ScratchpadSticky
Visible by default?No (hidden)Yes (always)
Per-workspace?No (no workspace)No (every workspace)
Summon needed?Yes ($mod+n)No (just there)
Use caseOn-demand utilityAlways-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:

SymbolMeaning
H[ … ]splith container
V[ … ]splitv container
T[ … ]tabbed container
S[ … ]stacked container (like tabbed but vertical title list)
bare namea leaf window

So H[T[Alacritty]] = an H container, holding a T container, holding one Alacritty window. Three nodes nested; only one is visible.

✨ Pro tip β€” reading orphans from the title bar When you see something like 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

KeyAction
$mod+tOpen terminal
$mod+bOpen browser
$mod+iOpen VS Code
$mod+spaceRun launcher (fuzzel)
$mod+Shift+qClose 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+arrowsMove focused window
$mod+cFocus parent container
$mod+Ctrl+cFocus child container
$mod+aToggle focus between tiled tree and floating layer
$mod+TabWindow picker (across all workspaces)

Layout

$mod+]Next window opens to the right (splith)
$mod+[Next window opens below (splitv)
$mod+Ctrl+fLayout: tabbed
$mod+periodToggle tabbed ↔ split
$mod+fFullscreen toggle
$mod+Shift+spaceFloating toggle

Resize

$mod+rCycle width preset (33% β†’ 50% β†’ 67%)
$mod+Ctrl+rReset to even split
$mod+= / $mod+-Grow / shrink width
$mod+Shift+= / $mod+Shift+-Grow / shrink height

Workspaces

$mod+1..6Jump to workspace
$mod+Shift+1..6Send focused window/container to workspace
$mod+Page_Up/DownPrev / next workspace
$mod+Ctrl+Page_Up/DownMove container and follow
$mod+gBack-and-forth (toggle to last workspace)
$mod+Shift+u/iMove workspace to output left/right

Scratchpad

$mod+Shift+nStash focused window into scratchpad
$mod+nSummon / hide / cycle scratchpad windows

System

$mod+Shift+rReload sway config
$mod+Shift+slashHotkeys cheatsheet (fuzzel)
$mod+F5Screenshot all
Ctrl+Alt+DeleteSession menu

Mouse (with $mod held)

$mod + left-drag floaterMove
$mod + right-drag floaterResize (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. 🌳