diff --git a/src/cli/list_keybinds.zig b/src/cli/list_keybinds.zig
index 13c69d9709..6cd989201c 100644
--- a/src/cli/list_keybinds.zig
+++ b/src/cli/list_keybinds.zig
@@ -68,7 +68,9 @@ pub fn run(alloc: Allocator) !u8 {
 
     // Despite being under the posix namespace, this also works on Windows as of zig 0.13.0
     if (tui.can_pretty_print and !opts.plain and std.posix.isatty(stdout.handle)) {
-        return prettyPrint(alloc, config.keybind);
+        var arena = std.heap.ArenaAllocator.init(alloc);
+        defer arena.deinit();
+        return prettyPrint(arena.allocator(), config.keybind);
     } else {
         try config.keybind.formatEntryDocs(
             configpkg.entryFormatter("keybind", stdout.writer()),
@@ -79,6 +81,111 @@ pub fn run(alloc: Allocator) !u8 {
     return 0;
 }
 
+const TriggerList = std.SinglyLinkedList(Binding.Trigger);
+
+const ChordBinding = struct {
+    triggers: TriggerList,
+    action: Binding.Action,
+
+    // Order keybinds based on various properties
+    //    1. Longest chord sequence
+    //    2. Most active modifiers
+    //    3. Alphabetically by active modifiers
+    //    4. Trigger key order
+    // These properties propagate through chorded keypresses
+    //
+    // Adapted from Binding.lessThan
+    pub fn lessThan(_: void, lhs: ChordBinding, rhs: ChordBinding) bool {
+        const lhs_len = lhs.triggers.len();
+        const rhs_len = rhs.triggers.len();
+
+        std.debug.assert(lhs_len != 0);
+        std.debug.assert(rhs_len != 0);
+
+        if (lhs_len != rhs_len) {
+            return lhs_len > rhs_len;
+        }
+
+        const lhs_count: usize = blk: {
+            var count: usize = 0;
+            var maybe_trigger = lhs.triggers.first;
+            while (maybe_trigger) |trigger| : (maybe_trigger = trigger.next) {
+                if (trigger.data.mods.super) count += 1;
+                if (trigger.data.mods.ctrl) count += 1;
+                if (trigger.data.mods.shift) count += 1;
+                if (trigger.data.mods.alt) count += 1;
+            }
+            break :blk count;
+        };
+        const rhs_count: usize = blk: {
+            var count: usize = 0;
+            var maybe_trigger = rhs.triggers.first;
+            while (maybe_trigger) |trigger| : (maybe_trigger = trigger.next) {
+                if (trigger.data.mods.super) count += 1;
+                if (trigger.data.mods.ctrl) count += 1;
+                if (trigger.data.mods.shift) count += 1;
+                if (trigger.data.mods.alt) count += 1;
+            }
+
+            break :blk count;
+        };
+
+        if (lhs_count != rhs_count)
+            return lhs_count > rhs_count;
+
+        {
+            var l_trigger = lhs.triggers.first;
+            var r_trigger = rhs.triggers.first;
+            while (l_trigger != null and r_trigger != null) {
+                const l_int = l_trigger.?.data.mods.int();
+                const r_int = r_trigger.?.data.mods.int();
+
+                if (l_int != r_int) {
+                    return l_int > r_int;
+                }
+
+                l_trigger = l_trigger.?.next;
+                r_trigger = r_trigger.?.next;
+            }
+        }
+
+        var l_trigger = lhs.triggers.first;
+        var r_trigger = rhs.triggers.first;
+
+        while (l_trigger != null and r_trigger != null) {
+            const lhs_key: c_int = blk: {
+                switch (l_trigger.?.data.key) {
+                    .translated => |key| break :blk @intFromEnum(key),
+                    .physical => |key| break :blk @intFromEnum(key),
+                    .unicode => |key| break :blk @intCast(key),
+                }
+            };
+            const rhs_key: c_int = blk: {
+                switch (r_trigger.?.data.key) {
+                    .translated => |key| break :blk @intFromEnum(key),
+                    .physical => |key| break :blk @intFromEnum(key),
+                    .unicode => |key| break :blk @intCast(key),
+                }
+            };
+
+            l_trigger = l_trigger.?.next;
+            r_trigger = r_trigger.?.next;
+
+            if (l_trigger == null or r_trigger == null) {
+                return lhs_key < rhs_key;
+            }
+
+            if (lhs_key != rhs_key) {
+                return lhs_key < rhs_key;
+            }
+        }
+
+        // The previous loop will always return something on its final iteration so we cannot
+        // reach this point
+        unreachable;
+    }
+};
+
 fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 {
     // Set up vaxis
     var tty = try vaxis.Tty.init();
@@ -111,26 +218,11 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 {
 
     const win = vx.window();
 
-    // Get all of our keybinds into a list. We also search for the longest printed keyname so we can
-    // align things nicely
+    // Generate a list of bindings, recursively traversing chorded keybindings
     var iter = keybinds.set.bindings.iterator();
-    var bindings = std.ArrayList(Binding).init(alloc);
-    var widest_key: u16 = 0;
-    var buf: [64]u8 = undefined;
-    while (iter.next()) |bind| {
-        const action = switch (bind.value_ptr.*) {
-            .leader => continue, // TODO: support this
-            .leaf => |leaf| leaf.action,
-        };
-        const key = switch (bind.key_ptr.key) {
-            .translated => |k| try std.fmt.bufPrint(&buf, "{s}", .{@tagName(k)}),
-            .physical => |k| try std.fmt.bufPrint(&buf, "physical:{s}", .{@tagName(k)}),
-            .unicode => |c| try std.fmt.bufPrint(&buf, "{u}", .{c}),
-        };
-        widest_key = @max(widest_key, win.gwidth(key));
-        try bindings.append(.{ .trigger = bind.key_ptr.*, .action = action });
-    }
-    std.mem.sort(Binding, bindings.items, {}, Binding.lessThan);
+    const bindings, const widest_chord = try iterateBindings(alloc, &iter, &win);
+
+    std.mem.sort(ChordBinding, bindings, {}, ChordBinding.lessThan);
 
     // Set up styles for each modifier
     const super_style: vaxis.Style = .{ .fg = .{ .index = 1 } };
@@ -138,41 +230,41 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 {
     const alt_style: vaxis.Style = .{ .fg = .{ .index = 3 } };
     const shift_style: vaxis.Style = .{ .fg = .{ .index = 4 } };
 
-    var longest_col: u16 = 0;
-
     // Print the list
-    for (bindings.items) |bind| {
+    for (bindings) |bind| {
         win.clear();
 
         var result: vaxis.Window.PrintResult = .{ .col = 0, .row = 0, .overflow = false };
-        const trigger = bind.trigger;
-        if (trigger.mods.super) {
-            result = win.printSegment(.{ .text = "super", .style = super_style }, .{ .col_offset = result.col });
-            result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
-        }
-        if (trigger.mods.ctrl) {
-            result = win.printSegment(.{ .text = "ctrl ", .style = ctrl_style }, .{ .col_offset = result.col });
-            result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
-        }
-        if (trigger.mods.alt) {
-            result = win.printSegment(.{ .text = "alt  ", .style = alt_style }, .{ .col_offset = result.col });
-            result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
-        }
-        if (trigger.mods.shift) {
-            result = win.printSegment(.{ .text = "shift", .style = shift_style }, .{ .col_offset = result.col });
-            result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
-        }
-
-        const key = switch (trigger.key) {
-            .translated => |k| try std.fmt.allocPrint(alloc, "{s}", .{@tagName(k)}),
-            .physical => |k| try std.fmt.allocPrint(alloc, "physical:{s}", .{@tagName(k)}),
-            .unicode => |c| try std.fmt.allocPrint(alloc, "{u}", .{c}),
-        };
-        // We don't track the key print because we index the action off the *widest* key so we get
-        // nice alignment no matter what was printed for mods
-        _ = win.printSegment(.{ .text = key }, .{ .col_offset = result.col });
+        var maybe_trigger = bind.triggers.first;
+        while (maybe_trigger) |trigger| : (maybe_trigger = trigger.next) {
+            if (trigger.data.mods.super) {
+                result = win.printSegment(.{ .text = "super", .style = super_style }, .{ .col_offset = result.col });
+                result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
+            }
+            if (trigger.data.mods.ctrl) {
+                result = win.printSegment(.{ .text = "ctrl ", .style = ctrl_style }, .{ .col_offset = result.col });
+                result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
+            }
+            if (trigger.data.mods.alt) {
+                result = win.printSegment(.{ .text = "alt  ", .style = alt_style }, .{ .col_offset = result.col });
+                result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
+            }
+            if (trigger.data.mods.shift) {
+                result = win.printSegment(.{ .text = "shift", .style = shift_style }, .{ .col_offset = result.col });
+                result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
+            }
+            const key = switch (trigger.data.key) {
+                .translated => |k| try std.fmt.allocPrint(alloc, "{s}", .{@tagName(k)}),
+                .physical => |k| try std.fmt.allocPrint(alloc, "physical:{s}", .{@tagName(k)}),
+                .unicode => |c| try std.fmt.allocPrint(alloc, "{u}", .{c}),
+            };
+            result = win.printSegment(.{ .text = key }, .{ .col_offset = result.col });
 
-        if (longest_col < result.col) longest_col = result.col;
+            // Print a separator between chorded keys
+            if (trigger.next != null) {
+                result = win.printSegment(.{ .text = "  >  ", .style = .{ .bold = true, .fg = .{ .index = 6 } } }, .{ .col_offset = result.col });
+            }
+        }
 
         const action = try std.fmt.allocPrint(alloc, "{}", .{bind.action});
         // If our action has an argument, we print the argument in a different color
@@ -181,12 +273,69 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 {
                 .{ .text = action[0..idx] },
                 .{ .text = action[idx .. idx + 1], .style = .{ .dim = true } },
                 .{ .text = action[idx + 1 ..], .style = .{ .fg = .{ .index = 5 } } },
-            }, .{ .col_offset = longest_col + widest_key + 2 });
+            }, .{ .col_offset = widest_chord + 3 });
         } else {
-            _ = win.printSegment(.{ .text = action }, .{ .col_offset = longest_col + widest_key + 2 });
+            _ = win.printSegment(.{ .text = action }, .{ .col_offset = widest_chord + 3 });
         }
         try vx.prettyPrint(writer);
     }
     try buf_writer.flush();
     return 0;
 }
+
+fn iterateBindings(alloc: Allocator, iter: anytype, win: *const vaxis.Window) !struct { []ChordBinding, u16 } {
+    var widest_chord: u16 = 0;
+    var bindings = std.ArrayList(ChordBinding).init(alloc);
+    while (iter.next()) |bind| {
+        const width = blk: {
+            var buf = std.ArrayList(u8).init(alloc);
+            const t = bind.key_ptr.*;
+
+            if (t.mods.super) try std.fmt.format(buf.writer(), "super + ", .{});
+            if (t.mods.ctrl) try std.fmt.format(buf.writer(), "ctrl  + ", .{});
+            if (t.mods.alt) try std.fmt.format(buf.writer(), "alt   + ", .{});
+            if (t.mods.shift) try std.fmt.format(buf.writer(), "shift + ", .{});
+
+            switch (t.key) {
+                .translated => |k| try std.fmt.format(buf.writer(), "{s}", .{@tagName(k)}),
+                .physical => |k| try std.fmt.format(buf.writer(), "physical:{s}", .{@tagName(k)}),
+                .unicode => |c| try std.fmt.format(buf.writer(), "{u}", .{c}),
+            }
+
+            break :blk win.gwidth(buf.items);
+        };
+
+        switch (bind.value_ptr.*) {
+            .leader => |leader| {
+
+                // Recursively iterate on the set of bindings for this leader key
+                var n_iter = leader.bindings.iterator();
+                const sub_bindings, const max_width = try iterateBindings(alloc, &n_iter, win);
+
+                // Prepend the current keybind onto the list of sub-binds
+                for (sub_bindings) |*nb| {
+                    const prepend_node = try alloc.create(TriggerList.Node);
+                    prepend_node.* = TriggerList.Node{ .data = bind.key_ptr.* };
+                    nb.triggers.prepend(prepend_node);
+                }
+
+                // Add the longest sub-bind width to the current bind width along with a padding
+                // of 5 for the '  >  ' spacer
+                widest_chord = @max(widest_chord, width + max_width + 5);
+                try bindings.appendSlice(sub_bindings);
+            },
+            .leaf => |leaf| {
+                const node = try alloc.create(TriggerList.Node);
+                node.* = TriggerList.Node{ .data = bind.key_ptr.* };
+                const triggers = TriggerList{
+                    .first = node,
+                };
+
+                widest_chord = @max(widest_chord, width);
+                try bindings.append(.{ .triggers = triggers, .action = leaf.action });
+            },
+        }
+    }
+
+    return .{ try bindings.toOwnedSlice(), widest_chord };
+}