How to Create a Custom Layout in Hyprland: A Complete Lua Guide

To create a custom layout in Hyprland, write a Lua script that calls hl.layout.register() with a recalculate callback that manipulates the layout context (ctx) to arrange windows using ctx:split() and target:place().

Hyprland’s tiling system is extensible through Lua scripts that hook into the compositor’s layout engine. When you create a custom layout in Hyprland, you define how windows subdivide the available screen space without touching the underlying C++ source. The CLuaTiledAlgorithm class in src/config/lua/layout/LuaLayoutProvider.hpp bridges your Lua code with Hyprland’s native CLayoutManager, passing a context object that exposes monitor geometry and window lists.

How Hyprland Processes Custom Layouts

The layout system follows a three-stage pipeline that delegates geometry calculations to your Lua code while Hyprland handles the heavy lifting of window positioning and rendering.

Layout Registration

When Hyprland initializes, the LuaLayoutProvider scans for Lua files and executes any calls to hl.layout.register(name, config). This function binds a layout name (e.g., "spiral" or "single_column") to a table containing at minimum a recalculate function. The provider stores these definitions and makes them available to the CLayoutManager defined in src/layout/LayoutManager.hpp.

The Recalculation Cycle

Every time a window opens, closes, or a monitor changes resolution, CLayoutManager triggers a recalculation. For Lua-based layouts, this invokes your registered recalculate callback with a single argument: the layout context (ctx). The context contains ctx.area (the usable monitor rectangle) and ctx.targets (a list of tiled windows to position).

Window Placement

Inside recalculate, you arrange windows by calling target:place(rect), where rect is a geometry table with x, y, width, and height fields. The ctx:split(area, side, ratio) helper divides rectangles cleanly, returning a new geometry table while shrinking the remaining area for subsequent windows.

The Layout Context API Reference

Understanding the ctx object is essential when you create a custom layout in Hyprland. The context provides the following properties and methods:

  • ctx.area: A table representing the full usable tiling area for the current monitor, containing x, y, w, and h fields.
  • ctx.targets: An array of window objects representing the currently tiled windows. Each target exposes target:place(rect) to set its final geometry.
  • ctx:split(area, side, ratio): Returns a new rectangle representing a portion of area split off from the specified side ("left", "right", "top", or "bottom"). The ratio parameter (0.0 to 1.0) determines what fraction of the area to carve off.

Creating a Minimal Single-Column Layout

Start with a simple layout that stacks windows vertically in equal strips. Save this as ~/.config/hypr/layouts/single_column.lua:

hl.layout.register("single_column", {
    recalculate = function(ctx)
        local area = ctx.area
        local n = #ctx.targets
        
        if n == 0 then return end
        
        local height = area.h / n
        
        for i, target in ipairs(ctx.targets) do
            local rect = {
                x = area.x,
                y = area.y + (i - 1) * height,
                w = area.w,
                h = height
            }
            target:place(rect)
        end
    end,
    
    layout_msg = function(_, msg)
        return "single_column: no configurable options"
    end
})

The layout_msg function is mandatory and handles runtime commands sent via hyprctl, though it can return a static message if your layout requires no dynamic configuration.

Implementing Advanced Patterns

For complex arrangements, recursively subdivide the remaining area. The spiral layout from example/layouts/spiral.lua demonstrates this pattern:

local opposite = { left = "right", right = "left", top = "bottom", bottom = "top" }

hl.layout.register("spiral", {
    recalculate = function(ctx)
        local n = #ctx.targets
        if n == 0 then return end
        
        local area = ctx.area
        local sides = {"left", "top", "right", "bottom"}
        
        for i, target in ipairs(ctx.targets) do
            if i == n then
                target:place(area)
            else
                local side = sides[((i - 1) % 4) + 1]
                local split_ratio = 0.58
                target:place(ctx:split(area, side, split_ratio))
                area = ctx:split(area, opposite[side], 1 - split_ratio)
            end
        end
    end,
    
    layout_msg = function(_, msg)
        -- Handle commands like "ratio 0.6" or "rotate"
        return "spiral: command received"
    end
})

This example uses ctx:split to carve out the main window, then reassigns the remaining area to create a Fibonacci-style spiral. For grid-based arrangements, examine example/layouts/grid.lua in the repository, which calculates row and column counts dynamically based on window count.

Loading and Activating Your Layout

Once you create a custom layout in Hyprland, you must load the script and activate the layout:

  1. Place your Lua file in ~/.config/hypr/layouts/ (create the directory if needed).
  2. Source the file in ~/.config/hypr/hyprland.conf using an exec statement:
exec = lua ~/.config/hypr/layouts/single_column.lua
  1. Switch to your layout using hyprctl:
hyprctl dispatch switchlayout single_column

Or bind it to a key in your configuration:

bind = $mainMod, L, exec, hyprctl dispatch switchlayout single_column

The MonitorLayoutController class in src/state/MonitorLayoutController.hpp handles the transition between layouts, triggering a full recalculation when you switch.

Key Source Files in the Hyprland Repository

The following files define the architecture that enables Lua-based layouts:

Summary

  • Registration: Use hl.layout.register(name, { recalculate = func }) to define a new layout name and callback.
  • Context: The ctx object provides ctx.area (screen geometry), ctx.targets (window list), and ctx:split() (area subdivision).
  • Placement: Call target:place(rect) on each window object to set its position and size.
  • Activation: Load Lua files via exec in hyprland.conf and switch layouts using hyprctl dispatch switchlayout.
  • Performance: Custom layouts run efficiently because Hyprland’s C++ engine handles the actual window moves and monitor updates based on your Lua geometry calculations.

Frequently Asked Questions

Can I write Hyprland layouts in Python instead of Lua?

No. Hyprland’s layout extension system specifically uses Lua scripting through the LuaLayoutProvider infrastructure. The hl.layout.register function and the context API are only exposed to the Lua runtime embedded within Hyprland. You must write layout scripts in Lua to interface with the CLuaTiledAlgorithm class.

How do I debug a custom layout that is not working?

Check the Hyprland logs using hyprctl log or by running Hyprland from a terminal. Lua syntax errors or missing required fields (like layout_msg) will appear as runtime errors in the log output. Ensure your script file is executable and the path in your exec statement is correct. You can also add print() statements to your Lua code; output appears in the Hyprland log.

Are custom Lua layouts slower than built-in C++ layouts?

No. While the tiling logic runs in Lua, the actual window positioning, damage tracking, and rendering remain native C++ operations handled by CLayoutManager. The Lua script only calculates geometry rectangles. For typical desktop usage with fewer than 50 windows, the overhead is negligible compared to the built-in dwindle or master layouts.

Can I distribute my custom layout to other users?

Yes. Since layouts are self-contained Lua files, you can share them by distributing the .lua file. Users simply place it in their ~/.config/hypr/layouts/ directory and add the exec line to their configuration. Ensure you document any specific layout_msg commands your layout supports so users can configure it via hyprctl.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →