Skip to content

Layout

In Textual, the layout defines how widgets will be arranged (or laid out) inside a container. Textual supports a number of layouts which can be set either via a widget's styles object or via CSS. Layouts can be used for both high-level positioning of widgets on screen, and for positioning of nested widgets.

Vertical

The vertical layout arranges child widgets vertically, from top to bottom.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO2ZW2/aSFx1MDAxNIDf8ytcIvrauHO/VFqtXHUwMDAyTdrck9KkTVdVNLFcdTAwMDdwMLbXXHUwMDFlXHUwMDEyoOp/37FJMVx1MDAxOFx1MDAxY1FcdTAwMWFRtrt+MPjM7XjmO2fOXHUwMDE5f93a3q6ZYaxrr7dreuCqwPdcdTAwMTL1UHuZye91kvpRaItQ/pxG/cTNa3aMidPXr171VNLVJlx1MDAwZZSrnXs/7asgNX3Pj1x1MDAxYzfqvfKN7qV/ZvdT1dN/xFHPM4lTXGayoz3fRMl4LFx1MDAxZOieXHUwMDBlTWp7/8s+b29/ze9T2iXaNSpsXHUwMDA3Om+QXHUwMDE3XHUwMDE1XG5CLsrS0yjMlYVcdTAwMDJRxFx1MDAxMGZsUsNP39jxjPZsccvqrIuSTFS7rNdcdTAwMGbj+/6RSNBdfFF/84lcdTAwMGZIUlxm2/KDoGmGQa6Wm0RputNRxu1cdTAwMTQ1UpNEXf3R90wn06Akn7RNIztcdTAwMTNFqyTqtzuhTtOZNlGsXFzfXGYzXHUwMDE5XHUwMDAwXHUwMDEz6XgmXm9cdTAwMTeSQbZOQjqSUFxmXHUwMDExncjzloI6XHUwMDE4gVx1MDAxOflYl0ZcdTAwMTTYJbC6vFx1MDAwMPlVaHOr3G7bqlx1MDAxNHqTOiZRYVx1MDAxYavELlRR7+HxLYlkXHUwMDBl5kJcdTAwMDI2NUhH++2OsaVcdTAwMThcdEdcdTAwMTDMp8bX+fxD24ZcdTAwMGLBWVGSjVx1MDAxYVx1MDAxZng5XHUwMDBiX8pz11FJ/DhHtTR7mNI4U3ZvXG6konE/9tR4vSFjXGJJiVx1MDAwMVx1MDAxN6yYvMBcdTAwMGa7tjDsXHUwMDA3QSGL3G6BSC799nJcdTAwMTU2KapiU2AkhSRkeTRcdTAwMTk4XCJcdTAwMThGh+qm8/Gi07jY8y/gTVx1MDAwNZolvGahROuDUlx1MDAwModIXHUwMDA0XHUwMDA1L0NJXHUwMDFjgGd5eX4oiVNBJGJcdTAwMGVEXHUwMDEwyFx1MDAwNUxcIkgpwFx1MDAxOK5cdTAwMTFJXGZcdTAwMDCUjHD0XFxI6iDw43QxkKjSWVxujJl1XHUwMDE0kixccuS+PLl517x6f/k+uvp09Fx1MDAwZXXdYd1bXHUwMDA1yPV5SVxmoFx1MDAwMyBlZSdpWSmJV8DxRUtRu+HMo1xikYMgmfWBXHUwMDEzXHUwMDE4IXRKbvu7e+SIQYq5/Fx1MDAxN3vHp1BcdTAwMTSVvpFbs4VcdTAwMThRsDSKny/SgTm6eXu81/Aurlx1MDAxM91NSXC04ShcIupQXHUwMDA2KGFz3lFcIuu5XHUwMDEwnt0zV+LxXHUwMDE2XHUwMDAw+lxcPFwiwFx0XHUwMDAzkEn8e1x1MDAwMmnjxCogXHQj2DpqKZZcdTAwMDbyslx1MDAxZTY/XHUwMDFm3lx1MDAwNVx1MDAwZnz0rn/gN+7vXFx8sOFAUuhAYO9cdTAwMGLcI3IwXHUwMDEwVP4skFx1MDAxMN1cbsGeXHUwMDBiSMKJpIKJ3zZ8xE+kNvaShEO8PJLnXHUwMDFmklH3nDY8vvu3fHOTXHUwMDA0QI3OKpDsKLfTT/RcdTAwMDZAXHSBXHUwMDAz5YJcdTAwMTDSukeHlZBZfc+mXHUwMDBivCQhwsk8nlxcSCW1Sc04r1wiMrtcdTAwMDQr44lcdTAwMTBEmGNcdTAwMDLXiifN7Eg8XHUwMDE3nkZcdTAwMGbMQl9Z6SohQ4xhgsDyiVxybV2NTsO3g+udYDS63mVR86ZcdTAwMTFvOpg2S5jlkZKf4fDJVIaRef5cdTAwMTbEi1x1MDAxNkKbSeBcco9cdTAwMTeLdY1C0/RHOlx1MDAwZi1mpPuq51x1MDAwN8OZpclBtJrahW5rMz2VqbZjjk97ZmrvXHUwMDA2fjtDtVx1MDAxNujWLMPGd1UwKTbR1Ju7dnRlu0tcdTAwMGW88ltEid/2Q1x1MDAxNXyY1WR1705cdTAwMTCv9u6SXHUwMDAwIaRcXD5cdTAwMDKW51x1MDAxMFx1MDAxY1x1MDAwZY7htZa3rZM9fkz76d6mXHUwMDFiXHUwMDExXHUwMDA20mFcdTAwMDLPXHUwMDA2XHUwMDE2w9ztXHUwMDEzR1xiJH7y1OpcdTAwMDVcdTAwMDEuoJwtXGI5XGIlXHUwMDBlp7jihFx1MDAwMELqXGLISFaaXHUwMDBmXHUwMDAz56yNXCJJKON0vdFcdTAwMDdcdTAwMDXUboZriT4oJlV8YmRcdTAwMTM0xvnyeKrPO83hzd6nncuTw+O6XHUwMDFj+Gr/Q3Pj8bTBXHUwMDA3gVxczmVoWWhApSzFXHUwMDA2K1x1MDAwMeoy3aKLXHUwMDAxtUFxJaCEOyjXazzIPJ9cYoBcZm+I15utUWhDtWfjUyVJ9LD4XHUwMDFjqzpX43ZcdTAwMGKU8Fx1MDAwN+KP+5OUjc5cdTAwMGUur0x8XHUwMDE2JOfDs1x1MDAwN4YvVmNzfUerwsa/slx1MDAxY1x1MDAwMH8/yypHrWUybVx1MDAxYaawfprMqlxcXHI4nHO8OFXDXGI5XHUwMDA0US5cdTAwMTZcdTAwMWZnXHRcblxiWyFcdTAwMWPONVt3eJJcdTAwMWGVmLpcdTAwMWZ6ftguN9GhV1FcdTAwMTKo1DSiXs83Vo3zyFx1MDAwZk25Rt7vblx1MDAwNnZHq7kow/Y8XVa2gDjrsfhSll3Fv+1cdTAwMDKR/GHy/8vLxbXnVjK7ptew6GFr+vdHs1x1MDAwNVwiy8JJoGMxlVhQtLy1plx1MDAwN/qwvdc+xfuXcrA/knetNiSbvpNYN+0ghNjc0Vxutnnk/LeIX5M/XHUwMDAwTG3ehjb9POW/lEBUWZR84ps3QcKGrj/wzftYJo2O2b3bXHUwMDFk6uOr3TPVc+/ev918i+JcdTAwMGVcdTAwMTFg/vScWIviXGbymVx1MDAxM6NfZFFYXHUwMDEwXHS53aP/t6i1W9TW475XU3HcNHaGbI2xfdlF8L3H1yz6q937+qG+6IQwv7JecyvN7EFnS/D129a3f1x1MDAwMLFE1Vx1MDAwMCJ9 WidgetWidgetWidget

The example below demonstrates how children are arranged inside a container with the vertical layout.

VerticalLayoutExample ┌──────────────────────────────────────────────────────────────────────────────┐ One └──────────────────────────────────────────────────────────────────────────────┘ ┌──────────────────────────────────────────────────────────────────────────────┐ Two └──────────────────────────────────────────────────────────────────────────────┘ ┌──────────────────────────────────────────────────────────────────────────────┐ Three └──────────────────────────────────────────────────────────────────────────────┘

from textual.app import App, ComposeResult
from textual.widgets import Static


class VerticalLayoutExample(App):
    CSS_PATH = "vertical_layout.css"

    def compose(self) -> ComposeResult:
        yield Static("One", classes="box")
        yield Static("Two", classes="box")
        yield Static("Three", classes="box")


if __name__ == "__main__":
    app = VerticalLayoutExample()
    app.run()
Screen {
    layout: vertical;
}

.box {
    height: 1fr;
    border: solid green;
}

Notice that the first widget yielded from the compose method appears at the top of the display, the second widget appears below it, and so on. Inside vertical_layout.css, we've assigned layout: vertical to Screen. Screen is the parent container of the widgets yielded from the App.compose method, and can be thought of as the terminal window itself.

Note

The layout: vertical CSS isn't strictly necessary in this case, since Screens use a vertical layout by default.

We've assigned each child .box a height of 1fr, which ensures they're each allocated an equal portion of the available height.

You might also have noticed that the child widgets are the same width as the screen, despite nothing in our CSS file suggesting this. This is because widgets expand to the width of their parent container (in this case, the Screen).

Just like other styles, layout can be adjusted at runtime by modifying the styles of a Widget instance:

widget.styles.layout = "vertical"

Using fr units guarantees that the children fill the available height of the parent. However, if the total height of the children exceeds the available space, then Textual will automatically add a scrollbar to the parent Screen.

Note

A scrollbar is added automatically because Screen contains the declaration overflow-y: auto;.

For example, if we swap out height: 1fr; for height: 10; in the example above, the child widgets become a fixed height of 10, and a scrollbar appears (assuming our terminal window is sufficiently small):

VerticalLayoutScrolledExample ┌────────────────────────────────────────────────────────────────────────────┐ One └────────────────────────────────────────────────────────────────────────────┘▂▂ ┌────────────────────────────────────────────────────────────────────────────┐ Two

With the parent container in focus, we can use our mouse wheel, trackpad, or keyboard to scroll it.

Horizontal

The horizontal layout arranges child widgets horizontally, from left to right.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO2aa0/bSFx1MDAxNIa/8ytQ+rVM536ptFpcdTAwMTEuLTQtlNAtdFVVjj1JZnFsYztcdGnFf9+xQ+PE2GxcYlHKatdCSTzX45nnXHUwMDFjvzPDj63t7UY6iXTj9XZD37iOb7zYXHUwMDE5N15m6SNcdTAwMWQnJlxmbFx1MDAxNs7vk3BcdTAwMTi7ecl+mkbJ61evXHUwMDA2Tnyl08h3XFxcckYmXHUwMDE5On6SXHUwMDBlPVx1MDAxM1x1MDAwMjdcdTAwMWO8MqlcdTAwMWUkv2efXHUwMDFmnIH+LVxuXHUwMDA3Xlx1MDAxYYOik1x1MDAxZO2ZNIynfWlfXHUwMDBmdJAmtvU/7f329o/8c866WLupXHUwMDEz9HydV8izXG5cdTAwMDNcdTAwMTFH5dRcdTAwMGZhkFx1MDAxYitcdTAwMDRnTFBOZ1x1MDAwNUyyb7tLtWdzu9ZkXeRkSY1wrHY6o1x1MDAxMbuiTv8vfja8uWw3o6LXrvH9djrxc6vcOEySnb6Tuv2iRJLG4ZX+bLy0n9lWSp/VTUI7XHUwMDEwRa04XHUwMDFj9vqBTpKFOmHkuCadZGlcdTAwMTDOUqdcdTAwMDPxertIubF3XHUwMDFjXHUwMDAxXHUwMDA0XHUwMDEx45jNkvOKXHUwMDA0XHUwMDAyXHUwMDBlXHUwMDA1xUhcblYyZi/07Vx1MDAxNFhjXsD8KszpOO5Vz9pcdTAwMTR4szJp7Fx1MDAwNEnkxHaiinLju8ekilx1MDAwM1wipILz3fe16fVTm0uwXHUwMDA0kpL5/nU+XHUwMDAxXGJhXHUwMDAyle2ZzHKyXqMjL2fha3nw+k5cdTAwMWPdXHJSI8lu5izOjD2YXHUwMDAzqag8jDxnOuGIc4yVXCJcbjNajJ5vgiubXHUwMDE5XGZ9v0hcdTAwMGLdq4KRPPX25SpsXHUwMDEyXsemopxcdTAwMGKO4fJs9odB29//LryTZvzHXHUwMDA1jpLjT+ZdXHKbJb5cdTAwMTapxJukktPFuc8rYlx1MDAwNVx1MDAwNJSqRMXaqaSgXHUwMDA2ScxcdTAwMDHCXGKqKii55Vx1MDAxMVNcdTAwMDHR5qAkXHUwMDEwYiigQOuCUvu+iZJqJFx1MDAxMalDkiNCqP1cdTAwMTNLI/lpsPd2ctChqOd8eXt4zVvHcFx1MDAxZq2C5OZcdTAwMDKlwFx1MDAwMImFaDiNk1xuMIZcdTAwMDV5KpEvulx1MDAwZcNcZt+nXHUwMDExYYBRyVx1MDAxN2Y8XCJcdTAwMDQoI1xi36NcdTAwMTFbw1xiUYJvNERaKyGkXHUwMDFioZGLOlx1MDAxYe1gXHREkIBqaVx1MDAxY9E5vWmpTtg62lx1MDAxOUYnh63v3eB493njaN+cwlx1MDAwZYLi94mUgFx1MDAxMlXGYiVcIjtcdTAwMTCytVx1MDAxMVx0MWFIWiY3TyTeXHUwMDAwkVx1MDAxONfKSeuLUmJCKVmayPeuuL5pXlx1MDAxY6TiYHzcla0v+/1o/3lcdTAwMTOJsOWC2ZBTISaFXHUwMDE1ckzAJyOJcEdKvi4kXHUwMDExQlx1MDAxMCumXHUwMDE4+1x1MDAxNyP5oI7k9WtcdTAwMWOroZVgXG7hpZmM3r9PzpvDvdZxcn74RSr+5qzztobJvuP2h7H+9VRcblx1MDAwMaxywVx1MDAxMpWZXHUwMDE0XGYwWKZ19Vx1MDAxNzevopJZuShcdFOVWGIuXHUwMDAxrMKSWIVPIedqk1QqSlx1MDAxMCNiXVSm+iatVpGyXHUwMDE2SIVskGRKLS8jj1x1MDAwZttJ2mpCdlx1MDAxMnjJ8FrH74KLi+dO5DROksVcdTAwMTVGVlx1MDAxNUtcYlxixvjJSD64upnb1ChQrFjNWFxirYhcIlx1MDAxYlxczWShUVwiwsgjICzmOlxm0rb5rnOlsZB66FxmjD9ZmK6cTmupnfyeTufHMtG2z5xGuVB61ze9jN+Gr7uLYKfGdfxZdlx1MDAxYc49uWt7d2xz8ZFXfoowNj1cdTAwMTM4/vmiJatHeilonWNcdFx1MDAxYuelJIIv7Ve4NWiN+67eXHUwMDFm6dbHyWRMh2ejo+fuV5gxUN5cdTAwMWGYRnr7XG6AVns+OdJP1UdlpIdcdTAwMWPYt7pYeM3MRXpcdTAwMDZ4aZPtp59Jqz6gIHijXHUwMDEyRFFcdTAwMWJq4WP8bHUwXHUwMDE1YnVgXCJMoWBYLs3ldde9hEcn15+Cvve5N1x1MDAxYZs2Pfz23Lkkklx1MDAwMJS90O8pXHUwMDEwXHUwMDA0eFmarIIlxrKjq7HkXHUwMDE4SJ53QVV2iSo4XHUwMDE1kFx1MDAwNFcqXHUwMDExpCRWVJBccitcdTAwMTEsXHUwMDA15OuCs1aJqHouKYNcdTAwMWOJR+xnnYs3x6NT7/SjY051e6/3wfF26/aznlxymFhIIGBpVTZcdTAwMTVcIlx1MDAxMqh1bCGsQYhQqlxis4vHTetcdTAwMTAqXHUwMDFmJYb/Szqk1qPkXHUwMDAzXHUwMDEyhNs4w+dm8Vx1MDAxZlx1MDAwZtQ+XHUwMDFmOJff6NklvDa9sVaBOPlr8tw9imJcdTAwMDHKbvPToaDgpf3jX6TsieJ2sUfxpj0qO7T636Oyr3tcdTAwMWXlxHE4rnQpWOtSdrVoXHUwMDAzOH/EnuK3uHnK946uXHUwMDA35uDj8I1vlHmz667mUlx1MDAxYjxcdJSA4vu73FxmXCLwkCtJ0WWdp1x1MDAxY1x1MDAwMTJA+KK7XHUwMDE2+4mAK1U6XHUwMDE4v/MtaHOs4EUr7HHn1q3mW1xmMkVcdTAwMWVz6jJnh1x1MDAxM6dNXHUwMDEzeCbolavowKvJ8Z0k3Vx1MDAwYlx1MDAwN1x1MDAwM5NaM05DXHUwMDEzpOVcdTAwMTJ5u7tcdTAwMTnVfe3cc1x1MDAxMdvyfF5cdTAwMTn/KGux+K+O7Cp+bVx1MDAxN3zkN7PfX19Wlq6Yyewq5rBoYGv++3brrsmGXHUwMDEzRe3UXHUwMDBluDVo6rh2To13XHUwMDE3kYrnaoyMXHUwMDFlN6v2XHUwMDA38ytcdTAwMGJcdTAwMDC5+2d+prOn+3G7dfs38GbaXHUwMDA3In0= WidgetWidgetWidget

The example below shows how we can arrange widgets horizontally, with minimal changes to the vertical layout example above.

HorizontalLayoutExample ┌────────────────────────┐┌─────────────────────────┐┌─────────────────────────┐ OneTwoThree └────────────────────────┘└─────────────────────────┘└─────────────────────────┘

from textual.app import App, ComposeResult
from textual.widgets import Static


class HorizontalLayoutExample(App):
    CSS_PATH = "horizontal_layout.css"

    def compose(self) -> ComposeResult:
        yield Static("One", classes="box")
        yield Static("Two", classes="box")
        yield Static("Three", classes="box")


if __name__ == "__main__":
    app = HorizontalLayoutExample()
    app.run()
Screen {
    layout: horizontal;
}

.box {
    height: 100%;
    width: 1fr;
    border: solid green;
}

We've changed the layout to horizontal inside our CSS file. As a result, the widgets are now arranged from left to right instead of top to bottom.

We also adjusted the height of the child .box widgets to 100%. As mentioned earlier, widgets expand to fill the width of their parent container. They do not, however, expand to fill the container's height. Thus, we need explicitly assign height: 100% to achieve this.

A consequence of this "horizontal growth" behaviour is that if we remove the width restriction from the above example (by deleting width: 1fr;), each child widget will grow to fit the width of the screen, and only the first widget will be visible. The other two widgets in our layout are offscreen, to the right-hand side of the screen. In the case of horizontal layout, Textual will not automatically add a scrollbar.

To enable horizontal scrolling, we can use the overflow-x: auto; declaration:

HorizontalLayoutExample ┌──────────────────────────────────────────────────────────────────────────────┐ One └──────────────────────────────────────────────────────────────────────────────┘

from textual.app import App, ComposeResult
from textual.widgets import Static


class HorizontalLayoutExample(App):
    CSS_PATH = "horizontal_layout_overflow.css"

    def compose(self) -> ComposeResult:
        yield Static("One", classes="box")
        yield Static("Two", classes="box")
        yield Static("Three", classes="box")


if __name__ == "__main__":
    app = HorizontalLayoutExample()
    app.run()
Screen {
    layout: horizontal;
    overflow-x: auto;
}

.box {
    height: 100%;
    border: solid green;
}

With overflow-x: auto;, Textual automatically adds a horizontal scrollbar since the width of the children exceeds the available horizontal space in the parent container.

Utility containers

Textual comes with several "container" widgets. These are Vertical, Horizontal, and Grid which have the corresponding layout.

The example below shows how we can combine these containers to create a simple 2x2 grid. Inside a single Horizontal container, we place two Vertical containers. In other words, we have a single row containing two columns.

UtilityContainersExample ┌──────────────────────────────────────┐┌──────────────────────────────────────┐ OneThree └──────────────────────────────────────┘└──────────────────────────────────────┘ ┌──────────────────────────────────────┐┌──────────────────────────────────────┐ TwoFour └──────────────────────────────────────┘└──────────────────────────────────────┘

from textual.app import App, ComposeResult
from textual.containers import Horizontal, Vertical
from textual.widgets import Static


class UtilityContainersExample(App):
    CSS_PATH = "utility_containers.css"

    def compose(self) -> ComposeResult:
        yield Horizontal(
            Vertical(
                Static("One"),
                Static("Two"),
                classes="column",
            ),
            Vertical(
                Static("Three"),
                Static("Four"),
                classes="column",
            ),
        )


if __name__ == "__main__":
    app = UtilityContainersExample()
    app.run()
Static {
    content-align: center middle;
    background: crimson;
    border: solid darkred;
    height: 1fr;
}

.column {
    width: 1fr;
}

You may be tempted to use many levels of nested utility containers in order to build advanced, grid-like layouts. However, Textual comes with a more powerful mechanism for achieving this known as grid layout, which we'll discuss next.

Grid

The grid layout arranges widgets within a grid. Widgets can span multiple rows and columns to create complex layouts. The diagram below hints at what can be achieved using layout: grid.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN2ZW3PaRlx1MDAxNMff8yk85DVsds/eM9PpONimwZc40NZ2O52MkFx1MDAxNiQjkCxcdOM4k+/elUhcdTAwMTFcYpNQyrhcdTAwMDQ9aEZnb8e7P875n/XnXHUwMDE3XHUwMDA3XHUwMDA3texTbGpvXHUwMDBlaubBdcLAS5xJ7VVuvzdJXHUwMDFhRCPbXHUwMDA0xXdcdTAwMWGNXHUwMDEzt+jpZ1mcvnn9eugkXHUwMDAzk8Wh41x1MDAxYXRcdTAwMWakYydMs7FcdTAwMTdEyI2Gr4PMXGbTn/P3hTM0P8XR0MtcdTAwMTJULlI3XpBFyXQtXHUwMDEzmqFcdTAwMTllqZ39T/t9cPC5eM95l1x1MDAxODdzRv3QXHUwMDE0XHUwMDAziqbSQVx1MDAwZbRqvYhGhbNSYYEpV3rWIUiP7HKZ8Wxrz7psypbcVLtpnVx1MDAxZl596NavLlx1MDAxZlx1MDAxZVx1MDAxYVx1MDAxZlx1MDAwNmN8d/rQKlftXHUwMDA1YdjJPoWFV25cdTAwMTKlad13Mtcve6RZXHUwMDEyXHLMVeBlvu1DKvbZ2DSyXHUwMDFiUY5KonHfXHUwMDFmmTRdXHUwMDE4XHUwMDEzxY5cdTAwMWJkn3JcdTAwMWLGM+t0I95cdTAwMWOUloeiXHUwMDA3Q1RSzISSfNZSjFx1MDAxNVxuXHUwMDExxjglwCvuNKLQXHUwMDFlgnXnJS6e0qGu41x1MDAwZfrWq5E365MlziiNncRcdTAwMWVV2W/y9Vx1MDAwZmVa2OWVxmJuXHUwMDEx31x1MDAwNH0/s61cdTAwMTRcdTAwMTRSjM45lpriXGKIXHUwMDEyUlGQsjzCfNX4nVfQ8Fd1+3wnib9uUy3NP+Y8zp09nkOpXHUwMDFjPI49Z3rkRFxioFhcYo0xLfcvXGZGXHUwMDAz2zhcdTAwMWGHYWmL3EFJSWH98mpcdTAwMDM6iYaVdFxuKblQhKxN51x1MDAxOVHtWH7sezeP/lHnulx1MDAwMVx1MDAwZbm+XUFnhbBFLuFZuVx1MDAwNFxuXGbIMpdcdTAwMTIxXCL1XHUwMDAysNvnkqFcdTAwMTVQgkBcdTAwMDRcYtZPYSmVXHUwMDEyhCtJfmAsTVx1MDAxOFx1MDAwNnH6NJRCroKSgKCSYGB8bSpvvZPTNlx1MDAxN73r5nEr6pCscXtydrZcdJXPXHUwMDE4LVx0Q0phxVx1MDAxN06/XHUwMDE4K1x1MDAwNdJcdTAwMTJcdTAwMDRcdTAwMTf/LVq+7DlcdTAwMWM4LFx1MDAxM0lcdTAwMDBcdTAwMDFhi9FwxiQhqFx1MDAxYainRCrGXGLYUfr5gSTPXHUwMDAwJFx1MDAwMFlccqSywYNpvj6QN2MxOUs8t3nphONcdTAwMTNcdTAwMTI770izu+NAUo1cdTAwMThwqebPfsojR1x1MDAxNUw3o7GLMd9cdTAwMTaNVHPFlFx1MDAwNrmnNM5tRpVGLG101DZGrk2j4dK9ad+178bvo7fHl0o49cbJjtMobNZcXFx1MDAxNpJcdTAwMTbFLVx1MDAwNEZcdTAwMDJdm123hVwiyVx1MDAxMzVVWvxcdTAwMGapemssfltBXHUwMDEyvlJCMlxuQtn6Zv1cdTAwMDKn58i02ei/u3J9OrlodY/Os4+rkrXvuP44MTvAI1x1MDAxMMujkMtMgqVmSV1unq7FU1xcaoxA0pzL6UR0XHUwMDE5T1x0/0hZpotnXHRTomxxROeF195hOldmV6OmxsDtr1x1MDAxNNbnNFx1MDAxOep2M2xcdTAwMWReXHUwMDBl07f3LGh15MVxvOucUmo5kFhiWk3jgDXKz2KxXHUwMDE22S6oXG4jOc8p2YRT4FQrofT+ckphdfGDqS3KQdP1Of2j0W1cdTAwMDa3V4T83nPdX25cdTAwMWb751x1MDAxN0fOrnNq01x1MDAwNlJSaL7MqY2nXFxhvChEt1x1MDAxY1CZLb6YzJXElEK5XGZqrkB4cZc1XVx1MDAwYosqqfZcdTAwMTclOFx1MDAwNjJcdTAwMTf3941UXHUwMDAyeCWpXHUwMDAyXHUwMDEzRYGT9XXo+aVw621y18x4NOanp93eIVx1MDAxOe06qXnmZ5zj5cKIYkBWjW8h9U9cdTAwMDXp06mfXCLNXGLLg/bmqZ8qZXOCwPurUC2H30j9Np3k97xrg0rYUVx1MDAwMHU2oYG+gUFrXHUwMDEyTn5cdTAwMTk+7DqolDKElV4sX6ac2lpKq8rF/HY53U7mXHUwMDE33Go0YPub+blcdTAwMTIrMZVC2OPjZP1bps71yfk5jDJcdTAwMGbjTP7620X9LIuPdlx1MDAxZNOikpJM4KWLT6ol4lpXWjbhXHUwMDE0QHXNk5yCJohcbrlcdTAwMTCx/1xyofm9PFx1MDAxMXR/XHUwMDAzKdPf0Ka5XHUwMDFlgLl7ju9cdTAwMDH6cVx1MDAxON++T4Pe44fw0L+Pzvw2ucG7XHUwMDBlKLNxlC39X2ZcdTAwMDboVqTpakBcdFx1MDAxM0hPr2FXStPvclxuWFx1MDAxMK2A/Vx1MDAxOJf19l1MWnPiuJPZKW3zlFrrdeB1gkezME3tPjCTt09cdP7iqb34yn7Ol8l9/vzlxZe/XHUwMDAxUO5ccsMifQ==

Note

Grid layouts in Textual have little in common with browser-based CSS Grid.

To get started with grid layout, define the number of columns and rows in your grid with the grid-size CSS property and set layout: grid. Widgets are inserted into the "cells" of the grid from left-to-right and top-to-bottom order.

The following example creates a 3 x 2 grid and adds six widgets to it

GridLayoutExample ┌────────────────────────┐┌─────────────────────────┐┌─────────────────────────┐ OneTwoThree └────────────────────────┘└─────────────────────────┘└─────────────────────────┘ ┌────────────────────────┐┌─────────────────────────┐┌─────────────────────────┐ FourFiveSix └────────────────────────┘└─────────────────────────┘└─────────────────────────┘

from textual.app import App, ComposeResult
from textual.widgets import Static


class GridLayoutExample(App):
    CSS_PATH = "grid_layout1.css"

    def compose(self) -> ComposeResult:
        yield Static("One", classes="box")
        yield Static("Two", classes="box")
        yield Static("Three", classes="box")
        yield Static("Four", classes="box")
        yield Static("Five", classes="box")
        yield Static("Six", classes="box")


if __name__ == "__main__":
    app = GridLayoutExample()
    app.run()
Screen {
    layout: grid;
    grid-size: 3 2;
}

.box {
    height: 100%;
    border: solid green;
}

If we were to yield a seventh widget from our compose method, it would not be visible as the grid does not contain enough cells to accommodate it. We can tell Textual to add new rows on demand to fit the number of widgets, by omitting the number of rows from grid-size. The following example creates a grid with three columns, with rows created on demand:

GridLayoutExample ┌────────────────────────┐┌─────────────────────────┐┌─────────────────────────┐ OneTwoThree └────────────────────────┘└─────────────────────────┘└─────────────────────────┘ ┌────────────────────────┐┌─────────────────────────┐┌─────────────────────────┐ FourFiveSix └────────────────────────┘└─────────────────────────┘└─────────────────────────┘ ┌────────────────────────┐ Seven └────────────────────────┘

from textual.app import App, ComposeResult
from textual.widgets import Static


class GridLayoutExample(App):
    CSS_PATH = "grid_layout2.css"

    def compose(self) -> ComposeResult:
        yield Static("One", classes="box")
        yield Static("Two", classes="box")
        yield Static("Three", classes="box")
        yield Static("Four", classes="box")
        yield Static("Five", classes="box")
        yield Static("Six", classes="box")
        yield Static("Seven", classes="box")


if __name__ == "__main__":
    app = GridLayoutExample()
    app.run()
Screen {
    layout: grid;
    grid-size: 3;
}

.box {
    height: 100%;
    border: solid green;
}

Since we specified that our grid has three columns (grid-size: 3), and we've yielded seven widgets in total, a third row has been created to accommodate the seventh widget.

Now that we know how to define a simple uniform grid, let's look at how we can customize it to create more complex layouts.

Row and column sizes

You can adjust the width of columns and the height of rows in your grid using the grid-columns and grid-rows properties. These properties can take multiple values, letting you specify dimensions on a column-by-column or row-by-row basis.

Continuing on from our earlier 3x2 example grid, let's adjust the width of the columns using grid-columns. We'll make the first column take up half of the screen width, with the other two columns sharing the remaining space equally.

GridLayoutExample ┌──────────────────────────────────────┐┌──────────────────┐┌──────────────────┐ OneTwoThree └──────────────────────────────────────┘└──────────────────┘└──────────────────┘ ┌──────────────────────────────────────┐┌──────────────────┐┌──────────────────┐ FourFiveSix └──────────────────────────────────────┘└──────────────────┘└──────────────────┘

from textual.app import App, ComposeResult
from textual.widgets import Static


class GridLayoutExample(App):
    CSS_PATH = "grid_layout3_row_col_adjust.css"

    def compose(self) -> ComposeResult:
        yield Static("One", classes="box")
        yield Static("Two", classes="box")
        yield Static("Three", classes="box")
        yield Static("Four", classes="box")
        yield Static("Five", classes="box")
        yield Static("Six", classes="box")


if __name__ == "__main__":
    app = GridLayoutExample()
    app.run()
Screen {
    layout: grid;
    grid-size: 3;
    grid-columns: 2fr 1fr 1fr;
}

.box {
    height: 100%;
    border: solid green;
}

Since our grid-size is 3 (meaning it has three columns), our grid-columns declaration has three space-separated values. Each of these values sets the width of a column. The first value refers to the left-most column, the second value refers to the next column, and so on. In the example above, we've given the left-most column a width of 2fr and the other columns widths of 1fr. As a result, the first column is allocated twice the width of the other columns.

Similarly, we can adjust the height of a row using grid-rows. In the following example, we use % units to adjust the first row of our grid to 25% height, and the second row to 75% height (while retaining the grid-columns change from above).

GridLayoutExample ┌──────────────────────────────────────┐┌──────────────────┐┌──────────────────┐ OneTwoThree └──────────────────────────────────────┘└──────────────────┘└──────────────────┘ ┌──────────────────────────────────────┐┌──────────────────┐┌──────────────────┐ FourFiveSix └──────────────────────────────────────┘└──────────────────┘└──────────────────┘

from textual.app import App, ComposeResult
from textual.widgets import Static


class GridLayoutExample(App):
    CSS_PATH = "grid_layout4_row_col_adjust.css"

    def compose(self) -> ComposeResult:
        yield Static("One", classes="box")
        yield Static("Two", classes="box")
        yield Static("Three", classes="box")
        yield Static("Four", classes="box")
        yield Static("Five", classes="box")
        yield Static("Six", classes="box")


if __name__ == "__main__":
    app = GridLayoutExample()
    app.run()
Screen {
    layout: grid;
    grid-size: 3;
    grid-columns: 2fr 1fr 1fr;
    grid-rows: 25% 75%;
}

.box {
    height: 100%;
    border: solid green;
}

If you don't specify enough values in a grid-columns or grid-rows declaration, the values you have provided will be "repeated". For example, if your grid has four columns (i.e. grid-size: 4;), then grid-columns: 2 4; is equivalent to grid-columns: 2 4 2 4;. If it instead had three columns, then grid-columns: 2 4; would be equivalent to grid-columns: 2 4 2;.

Cell spans

Cells may span multiple rows or columns, to create more interesting grid arrangements.

To make a single cell span multiple rows or columns in the grid, we need to be able to select it using CSS. To do this, we'll add an ID to the widget inside our compose method so we can set the row-span and column-span properties using CSS.

Let's add an ID of #two to the second widget yielded from compose, and give it a column-span of 2 to make that widget span two columns. We'll also add a slight tint using tint: magenta 40%; to draw attention to it.

GridLayoutExample ┌────────────────────────┐┌────────────────────────────────────────────────────┐ OneTwo (column-span: 2) └────────────────────────┘└────────────────────────────────────────────────────┘ ┌────────────────────────┐┌─────────────────────────┐┌─────────────────────────┐ ThreeFourFive └────────────────────────┘└─────────────────────────┘└─────────────────────────┘ ┌────────────────────────┐ Six └────────────────────────┘

from textual.app import App, ComposeResult
from textual.widgets import Static


class GridLayoutExample(App):
    CSS_PATH = "grid_layout5_col_span.css"

    def compose(self) -> ComposeResult:
        yield Static("One", classes="box")
        yield Static("Two [b](column-span: 2)", classes="box", id="two")
        yield Static("Three", classes="box")
        yield Static("Four", classes="box")
        yield Static("Five", classes="box")
        yield Static("Six", classes="box")


if __name__ == "__main__":
    app = GridLayoutExample()
    app.run()
Screen {
    layout: grid;
    grid-size: 3;
}

#two {
    column-span: 2;
    tint: magenta 40%;
}

.box {
    height: 100%;
    border: solid green;
}

Notice that the widget expands to fill columns to the right of its original position. Since #two now spans two cells instead of one, all widgets that follow it are shifted along one cell in the grid to accommodate. As a result, the final widget wraps on to a new row at the bottom of the grid.

Note

In the example above, setting the column-span of #two to be 3 (instead of 2) would have the same effect, since there are only 2 columns available (including #two's original column).

We can similarly adjust the row-span of a cell to have it span multiple rows. This can be used in conjunction with column-span, meaning one cell may span multiple rows and columns. The example below shows row-span in action. We again target widget #two in our CSS, and add a row-span: 2; declaration to it.

GridLayoutExample ┌────────────────────────┐┌────────────────────────────────────────────────────┐ OneTwo (column-span: 2 and row-span: 2) └────────────────────────┘ ┌────────────────────────┐ Three └────────────────────────┘└────────────────────────────────────────────────────┘ ┌────────────────────────┐┌─────────────────────────┐┌─────────────────────────┐ FourFiveSix └────────────────────────┘└─────────────────────────┘└─────────────────────────┘

from textual.app import App, ComposeResult
from textual.widgets import Static


class GridLayoutExample(App):
    CSS_PATH = "grid_layout6_row_span.css"

    def compose(self) -> ComposeResult:
        yield Static("One", classes="box")
        yield Static("Two [b](column-span: 2 and row-span: 2)", classes="box", id="two")
        yield Static("Three", classes="box")
        yield Static("Four", classes="box")
        yield Static("Five", classes="box")
        yield Static("Six", classes="box")


app = GridLayoutExample()
if __name__ == "__main__":
    app.run()
Screen {
    layout: grid;
    grid-size: 3;
}

#two {
    column-span: 2;
    row-span: 2;
    tint: magenta 40%;
}

.box {
    height: 100%;
    border: solid green;
}

Widget #two now spans two columns and two rows, covering a total of four cells. Notice how the other cells are moved to accommodate this change. The widget that previously occupied a single cell now occupies four cells, thus displacing three cells to a new row.

Gutter

The spacing between cells in the grid can be adjusted using the grid-gutter CSS property. By default, cells have no gutter, meaning their edges touch each other. Gutter is applied across every cell in the grid, so grid-gutter must be used on a widget with layout: grid (not on a child/cell widget).

To illustrate gutter let's set our Screen background color to lightgreen, and the background color of the widgets we yield to darkmagenta. Now if we add grid-gutter: 1; to our grid, one cell of spacing appears between the cells and reveals the light green background of the Screen.

GridLayoutExample OneTwoThree FourFiveSix

from textual.app import App, ComposeResult
from textual.widgets import Static


class GridLayoutExample(App):
    CSS_PATH = "grid_layout7_gutter.css"

    def compose(self) -> ComposeResult:
        yield Static("One", classes="box")
        yield Static("Two", classes="box")
        yield Static("Three", classes="box")
        yield Static("Four", classes="box")
        yield Static("Five", classes="box")
        yield Static("Six", classes="box")


if __name__ == "__main__":
    app = GridLayoutExample()
    app.run()
Screen {
    layout: grid;
    grid-size: 3;
    grid-gutter: 1;
    background: lightgreen;
}

.box {
    background: darkmagenta;
    height: 100%;
}

Notice that gutter only applies between the cells in a grid, pushing them away from each other. It doesn't add any spacing between cells and the edges of the parent container.

Tip

You can also supply two values to the grid-gutter property to set vertical and horizontal gutters respectively. Since terminal cells are typically two times taller than they are wide, it's common to set the horizontal gutter equal to double the vertical gutter (e.g. grid-gutter: 1 2;) in order to achieve visually consistent spacing around grid cells.

Docking

Widgets may be docked. Docking a widget removes it from the layout and fixes its position, aligned to either the top, right, bottom, or left edges of a container. Docked widgets will not scroll out of view, making them ideal for sticky headers, footers, and sidebars.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN2aWXPbNlx1MDAxMIDf8ys0ymvM4D4y0+nItpKocWwnjuOj0+nQJCTSokiWpCxLXHUwMDE5//cuaVXURVx1MDAxZrKqqtGDLWFcdGBcdHy72Fx1MDAwNfDjVa1Wz4axqb+r1c2tY1x1MDAwN76b2IP6m7z8xiSpXHUwMDFmhSBcIsXvNOonTvGkl2Vx+u7t256ddE1cdTAwMTZcdTAwMDe2Y6xcdTAwMWI/7dtBmvVdP7KcqPfWz0wv/TX/e2j3zC9x1HOzxCo72TGun0XJfV8mMD1cdTAwMTNmKbT+O/yu1X5cdTAwMTR/p7RLjJPZYScwRYVCVCrIXHSbLz2MwkJZzDXHTCuMJk/46T70l1x1MDAxOVx1MDAxN8Rt0NmUkryovk9cdTAwMWKNLDiPm7vvveD4YtQ6b3mdstu2XHUwMDFmXHUwMDA0J9kwKNRykihNdzw7c7zyiTRLoq45893MyzWYK5/UTSNcdTAwMTiJslZcdTAwMTL1O15o0nSmTlx1MDAxNNuOn1xy8zJUvsL9SLyrlSW3+TBcYksppLimkk9cdTAwMDR5VVwiLc0wQ4LwOWX2olx1MDAwMOZcdTAwMDCUeY2KT6nOle10O6BT6E6eyVx1MDAxMjtMYzuBmSqfXHUwMDFijF+TaWFRqfRMJ57xO15cdTAwMDZSSpSl2LReqSkmQFLMXHUwMDE4RVxcTVx1MDAwNHmnccstWPhjfuw8O4nHY1RP81x1MDAxZlNcbue6NqdAKiv3Y9e+n28sXHUwMDA0YVx1MDAxYyOtJKJcdTAwMTN54IddXHUwMDEwhv0gKMtcIqdbXCJSlN69WYFNrGklm4wjXCI1k09nM+yfXHUwMDFj73qX3/rdXHUwMDEz3jvY/Vx1MDAxNCFf7lWwOcfXLJVkk1RKhlxiZUuoJJjNUbF2KplVgSRcdTAwMTFcdTAwMTYmQMIyKJHGXHUwMDFhXHUwMDFjXHUwMDA3+lx1MDAxZkNpgsCP0+VIXG5VhaQg4D84werJRLZUvG9uj6L9a9p2/mzEPlx1MDAxYuhwXHUwMDE1XCI35yeFtKhSQmI1RyRcdTAwMDVUheZcdTAwMTS/zE++btuccLJIIyaLxE94xNhic13f08hcdTAwMDVSQlx1MDAwYkV+Tlx1MDAxYVx0IVU0YiykJuA7no6jz0dnXel+uP14ddL63CW94Pw82G5cdTAwMWM1tjSli6s25ZbgL1xcslx1MDAwMcUrhPi6UCyCXGLNlPhJUZSiXG5FmFx1MDAxZKQwsPhkXHUwMDEym8R8O/R6XzzxvXf05f1+s5mNXHUwMDBltptEjKXF5Cx0Y1x1MDAxMl9cdTAwMWE7vsbkXG587rpA1FgqJqT8P6/QXHUwMDBmh41cdTAwMTC5VLpFSlx1MDAxOVx1MDAxM1x1MDAxMOc/PW68uTj8XHUwMDFj68ug2T/5yptcdTAwMTk7v4pdv1x1MDAwMkbPdrx+Yv57XHUwMDFjuVx1MDAwNjTIXFxKkVfldJ3rtFhGJdWWXHUwMDEwRcZ031x1MDAxMF2Ek1wiYtFZ5cZ0XG4miCZywys2jFx1MDAwNkViM3TKXHUwMDA3Mm4qXGJoJJ+R1Vx1MDAxY1x1MDAxZYlcdTAwMTZpXFz3XHUwMDAzX35cdTAwMTmq9vfWzaCRbjudXHUwMDA0c0stYJjXXHUwMDE11FKQ2NGXZjZjn7mMT8iYLYZcdTAwMTHmS1NcdTAwMWJJQCghZJRMXHUwMDE3n3lAMVx1MDAwNF2SK0o2SygkXHUwMDE3SMrNXHUwMDEwqpSsXCKU5IqoZ1x1MDAxMbpz9MGcfo/OzcDP1M7p8V+O23K2n1BlgUdcdTAwMTBswYFiziyBJCMzKdB6XHUwMDExJVpZfFVAXHUwMDA1olQwyjaLJ5jFxvBElVmPRlhTNZ1cdTAwMTY9Rif969PnVn/onO3tfrn0mmfNTvL5z+2nXHUwMDEzsu2lyzthzFKU6lx1MDAxN29ccj1Cp0RcdTAwMTDN4dVcdTAwMWMoQ+A/MaGbXHJAYc2hXHUwMDAyb4RQgmTlLlx1MDAxMURHVEJcdTAwMWH4jGyo8enyMOy6UUz6R3vu7c23bD+Mt1x1MDAxZFFOmSU5X7KjLoWl+Dr8JyHqyiwlXHUwMDE0wotcIrZcdTAwMThcdTAwMTOotVjkXHUwMDE0XHUwMDBibTGxdFx1MDAwZlx1MDAxM1x1MDAxMiRcblx1MDAxM1x1MDAwNPRveIlcdTAwMTeYboZQrFHl1rpSmmBKpnKox1x1MDAwMFx1MDAxNa33e8e/XHRcdTAwMTnstM2+STJFXHUwMDBm+61tXHUwMDA3NF/hXHRcdTAwMTWUqPlcdTAwMTiUQs4uOV5DkvSQXHUwMDBmXHUwMDA1K1x1MDAxOJ83XHUwMDE1XHItI1RbsJapXHUwMDEyYzpcdTAwMGYqpVx1MDAxNFDlerMnQDBykESvi1M7SaLBUi/KKlx1MDAxMWWUS4LwM1x1MDAwZSbbl1xyXHUwMDE571x1MDAwN5/E6FwiuKTy4243XHUwMDFjXHUwMDBlVkN0c8c/WECaRDBmRFx1MDAxMIWEwnKGU5afTT5cZil3NEPuqpDmm1xiXHUwMDEwXHUwMDAyc5jyonu2yChB2lr0n1x1MDAwMoPqXG6vcjJZKLdcdTAwMWGXlMKQPsd/TulhJ9muXHUwMDFmun7YXHUwMDAx4T+M1ian661cdTAwMDKig49fvUBcdTAwMGVcdGmedk4v5ECnX9HpRNdcdTAwMWOjyOnnWu4gXHUwMDBiwlxywlx1MDAxNbhcdTAwMTdCNSyDik891rHjYv4teZ/ojiV3XHUwMDEzfUzoltrMvoCdZntRr+dn8OrHkVx1MDAxZmbzT1x1MDAxNO/SyI3KM7Y7L4WWp2Xz1lx1MDAxN+ctlldcdPJP+a1W4ln8mHz/483Sp3cq+SmkXHUwMDA1OmVcdTAwMWKvpv9XOYvM3GZLfVx1MDAwNa5cZrgkg0BcdTAwMTRcdTAwMDLO0sE+5itcdTAwMWWe5i1dznCxnC1cdTAwMWNcdTAwMTQzXG6xOnn5WUi1k4BwfolbWPBcdFx1MDAxME9cdIhcdP+N449cdEM/pvB6kt+fYeveXHUwMDE0JpK7f4D8N1x1MDAxY047XG6zXHUwMDEzf1TsqKCZ0vd2z1x1MDAwZoYzXHUwMDE4XHUwMDE00OeXa4q2ajDwXHUwMDFkk01PV2qg61wiuVAzlVx1MDAxYYHfya2jXHUwMDFlmPas2WS+Y1x1MDAwN1x1MDAxM3FcdTAwMTZNXHKvXHUwMDAzStjQXFzSWnBcdTAwMWVR4nf80Fx1MDAwZb4tVWglw8XVW02QIVxuSVx1MDAxOFJPz5RcdTAwMGVcdTAwMDJzdINij35cdTAwMThdOFe/XVx1MDAwZkbBaMWt+s2t8lRLaz6NXHUwMDA3O7aUnj+/WfNcdTAwMDVcdTAwMGbMS4VcdTAwMWawXFyMXHUwMDE01lx1MDAxMHuwXHKb7uHxdYNeKXamlZdcXCeDXHUwMDBmo1x1MDAwMTtYm+lyjMRz9qteZrpcdTAwMDf2MOpnY0tJt8F25zRaMUTn1Vx1MDAxN7RcdTAwMTCh+DnXs1x1MDAxZZ7uLbVdxoVFXHUwMDE5yq/hIaXx1GWL+1x1MDAwMJ1Z+rG7g0q2+dXqRqykhbjGeqzA1MZTadJcdTAwMTBcdTAwMWFUXdeCcsGJ5qusyy9cdNVz81OrmN9TQ/VcdTAwMDdXgtlQXHUwMDFkVpj8SFxcUYihJFXlJJahOrG0QkyonzdWr+SokE4jVFx1MDAxNbK/XHUwMDFhN1634/gkg/meTFx1MDAwZiDlu2OfWb5h/cY3g91lR8vFJ3dJxSjnpm/y9/xx9+rub5B4Q/4ifQ== Docked widgetLayout widgets

To dock a widget to an edge, add a dock: <EDGE>; declaration to it, where <EDGE> is one of top, right, bottom, or left. For example, a sidebar similar to that shown in the diagram above can be achieved using dock: left;. The code below shows a simple sidebar implementation.

DockLayoutExample Sidebarfor sticky headers, footers, and sidebars. Docking a widget removes it from the layout and fixes its  position, aligned to either the top, right, bottom, or left  edges of a container. Docked widgets will not scroll out of view, making them ideal  for sticky headers, footers, and sidebars. ▂▂ Docking a widget removes it from the layout and fixes its  position, aligned to either the top, right, bottom, or left  edges of a container. Docked widgets will not scroll out of view, making them ideal  for sticky headers, footers, and sidebars. Docking a widget removes it from the layout and fixes its ▁▁ position, aligned to either the top, right, bottom, or left  edges of a container. Docked widgets will not scroll out of view, making them ideal  for sticky headers, footers, and sidebars. Docking a widget removes it from the layout and fixes its 

from textual.app import App, ComposeResult
from textual.widgets import Static

TEXT = """\
Docking a widget removes it from the layout and fixes its position, aligned to either the top, right, bottom, or left edges of a container.

Docked widgets will not scroll out of view, making them ideal for sticky headers, footers, and sidebars.

"""


class DockLayoutExample(App):
    CSS_PATH = "dock_layout1_sidebar.css"

    def compose(self) -> ComposeResult:
        yield Static("Sidebar", id="sidebar")
        yield Static(TEXT * 10, id="body")


if __name__ == "__main__":
    app = DockLayoutExample()
    app.run()
#sidebar {
    dock: left;
    width: 15;
    height: 100%;
    color: #0f2b41;
    background: dodgerblue;
}

If we run the app above and scroll down, the body text will scroll but the sidebar does not (note the position of the scrollbar in the output shown above).

Docking multiple widgets to the same edge will result in overlap. The first widget yielded from compose will appear below widgets yielded after it. Let's dock a second sidebar, #another-sidebar, to the left of the screen. This new sidebar is double the width of the one previous one, and has a deeppink background.

DockLayoutExample Sidebar1right, bottom, or left edges of a container. Docked widgets will not scroll out of view,  making them ideal for sticky headers, footers,  and sidebars. Docking a widget removes it from the layout and  fixes its position, aligned to either the top, ▃▃ right, bottom, or left edges of a container. Docked widgets will not scroll out of view,  making them ideal for sticky headers, footers,  and sidebars. Docking a widget removes it from the layout and ▂▂ fixes its position, aligned to either the top,  right, bottom, or left edges of a container. Docked widgets will not scroll out of view,  making them ideal for sticky headers, footers,  and sidebars. Docking a widget removes it from the layout and  fixes its position, aligned to either the top, 

from textual.app import App, ComposeResult
from textual.widgets import Static

TEXT = """\
Docking a widget removes it from the layout and fixes its position, aligned to either the top, right, bottom, or left edges of a container.

Docked widgets will not scroll out of view, making them ideal for sticky headers, footers, and sidebars.

"""


class DockLayoutExample(App):
    CSS_PATH = "dock_layout2_sidebar.css"

    def compose(self) -> ComposeResult:
        yield Static("Sidebar2", id="another-sidebar")
        yield Static("Sidebar1", id="sidebar")
        yield Static(TEXT * 10, id="body")


app = DockLayoutExample()
if __name__ == "__main__":
    app.run()
#another-sidebar {
    dock: left;
    width: 30;
    height: 100%;
    background: deeppink;
}

#sidebar {
    dock: left;
    width: 15;
    height: 100%;
    color: #0f2b41;
    background: dodgerblue;
}

Notice that the original sidebar (#sidebar) appears on top of the newly docked widget. This is because #sidebar was yielded after #another-sidebar inside the compose method.

Of course, we can also dock widgets to multiple edges within the same container. The built-in Header widget contains some internal CSS which docks it to the top. We can yield it inside compose, and without any additional CSS, we get a header fixed to the top of the screen.

DockLayoutExample Sidebar1DockLayoutExample Docking a widget removes it from the layout and fixes its  position, aligned to either the top, right, bottom, or left  edges of a container. Docked widgets will not scroll out of view, making them ideal  for sticky headers, footers, and sidebars. Docking a widget removes it from the layout and fixes its  position, aligned to either the top, right, bottom, or left  edges of a container. Docked widgets will not scroll out of view, making them ideal  for sticky headers, footers, and sidebars. Docking a widget removes it from the layout and fixes its  position, aligned to either the top, right, bottom, or left  edges of a container. Docked widgets will not scroll out of view, making them ideal  for sticky headers, footers, and sidebars. Docking a widget removes it from the layout and fixes its  position, aligned to either the top, right, bottom, or left 

from textual.app import App, ComposeResult
from textual.widgets import Static, Header

TEXT = """\
Docking a widget removes it from the layout and fixes its position, aligned to either the top, right, bottom, or left edges of a container.

Docked widgets will not scroll out of view, making them ideal for sticky headers, footers, and sidebars.

"""


class DockLayoutExample(App):
    CSS_PATH = "dock_layout3_sidebar_header.css"

    def compose(self) -> ComposeResult:
        yield Header(id="header")
        yield Static("Sidebar1", id="sidebar")
        yield Static(TEXT * 10, id="body")


if __name__ == "__main__":
    app = DockLayoutExample()
    app.run()
#sidebar {
    dock: left;
    width: 15;
    height: 100%;
    color: #0f2b41;
    background: dodgerblue;
}

If we wished for the sidebar to appear below the header, it'd simply be a case of yielding the sidebar before we yield the header.

Layers

Textual has a concept of layers which gives you finely grained control over the order widgets are placed.

When drawing widgets, Textual will first draw on lower layers, working its way up to higher layers. As such, widgets on higher layers will be drawn on top of those on lower layers.

Layer names are defined with a layers style on a container (parent) widget. Descendants of this widget can then be assigned to one of these layers using a layer style.

The layers style takes a space-separated list of layer names. The leftmost name is the lowest layer, and the rightmost is the highest layer. Therefore, if you assign a descendant to the rightmost layer name, it'll be drawn on the top layer and will be visible above all other descendants.

An example layers declaration looks like: layers: one two three;. To add a widget to the topmost layer in this case, you'd add a declaration of layer: three; to it.

In the example below, #box1 is yielded before #box2. Given our earlier discussion on yield order, you'd expect #box2 to appear on top. However, in this case, both #box1 and #box2 are assigned to layers which define the reverse order, so #box1 is on top of #box2

LayersExample box1 (layer = above) box2 (layer = below)

from textual.app import App, ComposeResult
from textual.widgets import Static


class LayersExample(App):
    CSS_PATH = "layers.css"

    def compose(self) -> ComposeResult:
        yield Static("box1 (layer = above)", id="box1")
        yield Static("box2 (layer = below)", id="box2")


if __name__ == "__main__":
    app = LayersExample()
    app.run()
Screen {
    align: center middle;
    layers: below above;
}

Static {
    width: 28;
    height: 8;
    color: auto;
    content-align: center middle;
}

#box1 {
    background: darkcyan;
    layer: above;
}

#box2 {
    layer: below;
    background: orange;
    offset: 12 6;
}

Offsets

Widgets have a relative offset which is added to the widget's location, after its location has been determined via its parent's layout. This means that if a widget hasn't had its offset modified using CSS or Python code, it will have an offset of (0, 0).

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN2ZWVPbSFx1MDAxMIDf+Vx1MDAxNZTzXHUwMDFhK3NcdTAwMWap2triXlxiS8JcdTAwMTVcYlspSkhjWWtZUqQx2Enx37clXHUwMDFjS744XGZhXHR6sK3p0Uxr5utr/GNpeblhXHUwMDA3qWm8X26YvudGoZ+5V423RfulyfIwiUFEyvs86WVe2bNtbZq/f/eu62ZcdTAwMWRj08j1jHNcdTAwMTnmPTfKbc9cdTAwMGZcdTAwMTPHS7rvQmu6+Z/F557bNX+kSde3mVNN0jR+aJPsZi5cdTAwMTOZroltXHUwMDBlo/9cdTAwMDP3y8s/ys+adpnxrFx1MDAxYlx1MDAwN5EpXHUwMDFmKEWVgpywyda9JC6VJVxcaM1cdTAwMDRBo1x1MDAwZWG+XHUwMDBl01njg7RcdTAwMDUqm0pSNDU2/L/pRvvbv70g+1x1MDAxNlxmVulgTZ82q1lbYVx1MDAxNFx1MDAxZNpBVGrlZUmeN9uu9dpVj9xmScechL5tQ1x1MDAxZjzRPno2T2AhqqeypFx1MDAxN7Rjk+djzySp64V2ULSh6lx1MDAxNW5cdTAwMTbi/XLV0i96UO4ohVx1MDAxNNdU8pGkeJYw7miGXHUwMDE5XHUwMDEyhE+os5ZEsFx0oM5cdTAwMWJUXpVCXHUwMDE3rtdcdECr2Fx1MDAxZvWxmVx1MDAxYuepm8FWVf2uhi/KtHCoVHpskrZcdIO2XHUwMDA1KSXKUayuWG7KLSCIYKWkpnokKWZNt/2Shq+Ty9d2s3S4TI28uKlpXFwou1FDqXq4l/ruzZZjIWA5XGJcdTAwMTDBdbV+UVx1MDAxOHdAXHUwMDE496Koaku8TkVJ2Xr9dlx1MDAwMTqxpvPoxJxgyTGW5N54XHUwMDFl2Y39081wV7ubXHUwMDFiJjv1z9OrvXl4TiA2XHUwMDBlJnlWMCVDhLJZYFx1MDAxMswmwHhyMJkzh0pcIlx1MDAxY0ww0jO4xEpQyonC9Dfm0kRRmOazqVx1MDAxNGoulVJcdTAwMTJcdTAwMDFb8lx1MDAwMCqT1Ytm/7CXx53Vg72TY4hcdTAwMDWfcGdcdTAwMTEqn9FdMvBXSlx0idUklZw5UmhO8ePc5ZuWy1x0J9NEYjJN/YhJjFx1MDAxZDYx9VxykUIrXHUwMDBlNqTx61x1MDAwNJKQuW5SIcxcdTAwMDVcdTAwMTX8/jz2XHUwMDBljoONwzPvUH6x4V/euWKnweZcdTAwMGLnUVwiR4PLmY7enDqTcXUxXHUwMDFhL1x1MDAxMOJPRaNUQkjxat0jkWKue0RCYK5FrcedOaV111vrQaY656q/x21rv1x1MDAxYixcdTAwMTS0n1x1MDAxMUdI55hcdTAwMTS8Tt1PXHUwMDFhXHUwMDFmjVwiJlx1MDAxN+B5n1xuRXDgiEtBlfyNWbw9hYQ0cS6OTCHN1Vx1MDAwM2hcXNvaOuqibbZ1XHUwMDE2fVx1MDAxOFxmdnbyj0r259DYdr12LzP/P49Eclx1MDAwN3EhxouYkkilXHUwMDFjOonq4uFazOJSI4fIMn+9XHUwMDE5iE7jKYnDsNRCSabLa1xuU1akXHUwMDE0XHUwMDE0iV9Q6VxmXHUwMDA1XHUwMDE1V7Xt3jnZXHUwMDBm6cpcdTAwMGU/kGebO1v+Jv34feXbaKwxXGLdLEuuXHUwMDFhI8n18NdcdTAwMGIxXHUwMDAyRvlcXCNcdTAwMTBcdTAwMTLyOMJqSe1dVtBstdbTXcp2dz+fb1xyzk74v51IvXQroFx1MDAxYUNcdTAwMTlccu+JucRIsYlUgVx1MDAxMeJcYqxcdTAwMWZf6Vx1MDAwZj30bFugo8OEhW2BgsdiYFxmr8pcdTAwMTRcbv+An8dcdTAwMTSEnpueXHUwMDEwpLDWhUO8tylE6+GXo1x1MDAwM2mClt7vtN0zkXhb+Us3hVwiICguXHUwMDE5marfXHUwMDE4l1x1MDAwZSNS0sdcdTAwMWUr3Fx1MDAxNlx1MDAxMVx1MDAxNHNcdTAwMTRReGhcdTAwMDNcYolcdTAwMDWsXHUwMDAwXHUwMDA24Fx1MDAwNKL373z0dWMnM931/FxmmjHQXHUwMDAxq/s769tccvdBhD7fuVx1MDAxN9RcdTAwMTDFiShDgKnCUvFxTGmR0eC7jlx1MDAxOZRs8YvFXHUwMDBmv6BYccCPa4Uxhe3neFx1MDAxYdPicJgzpYnSklx1MDAwYkbVJKZQknIsXHUwMDE5W6DUKzVdXHUwMDEw08J8yVx1MDAwMzCt6eFmdjWM/TBcdTAwMGVAWMWBn/8zbN8jXHIuqEq8Xrn7XHUwMDBllpD4gVx1MDAxOeNcItxcdTAwMDG8tU6Bm5ZEO1BcdTAwMTiVNfpQdD1Sx8T+3crcno3UlGlcIlx1MDAwN4FLwVx1MDAwNPJdpTVcdTAwMDRRxKbUUY7kRCvGOIJccuVCTilcdTAwMTW5uV1Lut3QwqJ/SsLYTi5uuYorhXW3jetPSuGl6rJJN5BcdTAwMTYjjofj6tdyZSflzej317cze89luLim6K1GW6p/z/Nf1vTtLPfF0S3uiyNYf0iB7u2/Lq8+dE6PV7+vN9m2/Cy7l3ZggpdcdTAwMWVhsdLgv1x1MDAwNNNMICaAqGpFSv8llFx1MDAwM9ZQxC+tMUTbx4TaW51YLauvju6nj1x1MDAwMSTkpFJcdTAwMTDyzMdcdTAwMDBcdTAwMTTXI9lcdTAwMDP8VCuJ7WH4/SZpXHUwMDFia910u2E0XHUwMDE427iSU9D0Y6uVXHUwMDFiW1/L3MCcJZdqrPdKXHUwMDE0XHUwMDA2cZnemdY44jb03GgktkntzT2Y3YXhsu0pk0+yMFxiYzc6XHUwMDFh1+RcdTAwMTFZLNdkro1xjFx1MDAwNUVS3d/G9pvHXHUwMDFl2ewrlVE//JCGe8dcdTAwMWLu2Vx1MDAxM9uYn1x1MDAxNP7yaZNcdTAwMDTmcKRnnLRRSVx1MDAxZHDv4tf+bfsk5Vx1MDAxYyZcZnKEwlx1MDAxYV5TPVfkyVx1MDAwZq/nloaDNtw0PbQw5Cjqw5qE/tDgq2FcdTAwMWGXoblanVV8lFehcmldXHUwMDA1wKZYkVx1MDAxZtdL1/9cdTAwMDE0elVbIn0= Offset

The offset of a widget can be set using the offset CSS property. offset takes two values.

  • The first value defines the x (horizontal) offset. Positive values will shift the widget to the right. Negative values will shift the widget to the left.
  • The second value defines the y (vertical) offset. Positive values will shift the widget down. Negative values will shift the widget up.

Putting it all together

The sections above show how the various layouts in Textual can be used to position widgets on screen. In a real application, you'll make use of several layouts.

The example below shows how an advanced layout can be built by combining the various techniques described on this page.

CombiningLayoutsExample CombiningLayoutsExample ┌──────────────────────────────────────┐┌──────────────────────────────────────┐ HorizontallyPositionedChildrenHere Vertical layout, child 0 Vertical layout, child 1 ▅▅ Vertical layout, child 2└──────────────────────────────────────┘ ┌──────────────────────────────────────┐ Thispanelis Vertical layout, child 3 usinggrid layout! Vertical layout, child 4 └──────────────────────────────────────┘└──────────────────────────────────────┘

from textual.containers import Container, Horizontal, Vertical
from textual.app import ComposeResult, App
from textual.widgets import Static, Header


class CombiningLayoutsExample(App):
    CSS_PATH = "combining_layouts.css"

    def compose(self) -> ComposeResult:
        yield Header()
        yield Container(
            Vertical(
                *[Static(f"Vertical layout, child {number}") for number in range(15)],
                id="left-pane",
            ),
            Horizontal(
                Static("Horizontally"),
                Static("Positioned"),
                Static("Children"),
                Static("Here"),
                id="top-right",
            ),
            Container(
                Static("This"),
                Static("panel"),
                Static("is"),
                Static("using"),
                Static("grid layout!", id="bottom-right-final"),
                id="bottom-right",
            ),
            id="app-grid",
        )


if __name__ == "__main__":
    app = CombiningLayoutsExample()
    app.run()
#app-grid {
    layout: grid;
    grid-size: 2;  /* two columns */
    grid-columns: 1fr;
    grid-rows: 1fr;
}

#left-pane > Static {
    background: $boost;
    color: auto;
    margin-bottom: 1;
    padding: 1;
}

#left-pane {
    row-span: 2;
    background: $panel;
    border: dodgerblue;
}

#top-right {
    background: $panel;
    border: mediumvioletred;
}

#top-right > Static {
    width: auto;
    height: 100%;
    margin-right: 1;
    background: $boost;
}

#bottom-right {
    layout: grid;
    grid-size: 3;
    grid-columns: 1fr;
    grid-rows: 1fr;
    grid-gutter: 1;
    background: $panel;
    border: greenyellow;
}

#bottom-right-final {
    column-span: 2;
}

#bottom-right > Static {
    height: 100%;
    background: $boost;
}

Textual layouts make it easy to design and build real-life applications with relatively little code.