[go: up one dir, main page]

Skip to content

Styles

In this chapter we will explore how you can apply styles to your application to create beautiful user interfaces.

Styles object

Every Textual widget class provides a styles object which contains a number of attributes. These attributes tell Textual how the widget should be displayed. Setting any of these attributes will update the screen accordingly.

Note

These docs use the term screen to describe the contents of the terminal, which will typically be a window on your desktop.

Let's look at a simple example which sets styles on screen (a special widget that represents the screen).

screen.py
from textual.app import App


class ScreenApp(App):
    def on_mount(self) -> None:
        self.screen.styles.background = "darkblue"
        self.screen.styles.border = ("heavy", "white")


if __name__ == "__main__":
    app = ScreenApp()
    app.run()

The first line sets the background style to "darkblue" which will change the background color to dark blue. There are a few other ways of setting color which we will explore later.

The second line sets border to a tuple of ("heavy", "white") which tells Textual to draw a white border with a style of "heavy". Running this code will show the following:

ScreenApp ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Styling widgets

Setting styles on screen is useful, but to create most user interfaces we will also need to apply styles to other widgets.

The following example adds a static widget which we will apply some styles to:

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


class WidgetApp(App):
    def compose(self) -> ComposeResult:
        self.widget = Static("Textual")
        yield self.widget

    def on_mount(self) -> None:
        self.widget.styles.background = "darkblue"
        self.widget.styles.border = ("heavy", "white")


if __name__ == "__main__":
    app = WidgetApp()
    app.run()

The compose method stores a reference to the widget before yielding it. In the mount handler we use that reference to set the same styles on the widget as we did for the screen example. Here is the result:

WidgetApp ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Textual ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Widgets will occupy the full width of their container and as many lines as required to fit in the vertical direction.

Note how the combined height of the widget is three rows in the terminal. This is because a border adds two rows (and two columns). If you were to remove the line that sets the border style, the widget would occupy a single row.

Information

Widgets will wrap text by default. If you were to replace "Textual" with a long paragraph of text, the widget will expand downwards to fit.

Colors

There are a number of style attributes which accept colors. The most commonly used are color which sets the default color of text on a widget, and background which sets the background color (beneath the text).

You can set a color value to one of a number of pre-defined color constants, such as "crimson", "lime", and "palegreen". You can find a full list in the Color API.

Here's how you would set the screen background to lime:

self.screen.styles.background = "lime"

In addition to color names, you can also use any of the following ways of expressing a color:

  • RGB hex colors starts with a # followed by three pairs of one or two hex digits; one for the red, green, and blue color components. For example, #f00 is an intense red color, and #9932CC is dark orchid.
  • RGB decimal color start with rgb followed by a tuple of three numbers in the range 0 to 255. For example rgb(255,0,0) is intense red, and rgb(153,50,204) is dark orchid.
  • HSL colors start with hsl followed by a angle between 0 and 360 and two percentage values, representing Hue, Saturation and Lightness. For example hsl(0,100%,50%) is intense red and hsl(280,60%,49%) is dark orchid.

The background and color styles also accept a Color object which can be used to create colors dynamically.

The following example adds three widgets and sets their color styles.

colors01.py
from textual.app import App, ComposeResult
from textual.color import Color
from textual.widgets import Static


class ColorApp(App):
    def compose(self) -> ComposeResult:
        self.widget1 = Static("Textual One")
        yield self.widget1
        self.widget2 = Static("Textual Two")
        yield self.widget2
        self.widget3 = Static("Textual Three")
        yield self.widget3

    def on_mount(self) -> None:
        self.widget1.styles.background = "#9932CC"
        self.widget2.styles.background = "hsl(150,42.9%,49.4%)"
        self.widget2.styles.color = "blue"
        self.widget3.styles.background = Color(191, 78, 96)


if __name__ == "__main__":
    app = ColorApp()
    app.run()

Here is the output:

ColorApp Textual One Textual Two Textual Three

Alpha

Textual represents color internally as a tuple of three values for the red, green, and blue components.

Textual supports a common fourth value called alpha which can make a color translucent. If you set alpha on a background color, Textual will blend the background with the color beneath it. If you set alpha on the text color, then Textual will blend the text with the background color.

There are a few ways you can set alpha on a color in Textual.

  • You can set the alpha value of a color by adding a fourth digit or pair of digits to a hex color. The extra digits form an alpha component which ranges from 0 for completely transparent to 255 (completely opaque). Any value between 0 and 255 will be translucent. For example "#9932CC7f" is a dark orchid which is roughly 50% translucent.
  • You can also set alpha with the rgba format, which is identical to rgb with the additional of a fourth value that should be between 0 and 1, where 0 is invisible and 1 is opaque. For example "rgba(192,78,96,0.5)".
  • You can add the a parameter on a Color object. For example Color(192, 78, 96, a=0.5) creates a translucent dark orchid.

The following example shows what happens when you set alpha on background colors:

colors01.py
from textual.app import App, ComposeResult
from textual.color import Color
from textual.widgets import Static


class ColorApp(App):
    def compose(self) -> ComposeResult:
        self.widgets = [Static("") for n in range(10)]
        yield from self.widgets

    def on_mount(self) -> None:
        for index, widget in enumerate(self.widgets, 1):
            alpha = index * 0.1
            widget.update(f"alpha={alpha:.1f}")
            widget.styles.background = Color(191, 78, 96, a=alpha)


if __name__ == "__main__":
    app = ColorApp()
    app.run()

Notice that at an alpha of 0.1 the background almost matches the screen, but at 1.0 it is a solid color.

ColorApp alpha=0.1 alpha=0.2 alpha=0.3 alpha=0.4 alpha=0.5 alpha=0.6 alpha=0.7 alpha=0.8 alpha=0.9 alpha=1.0

Dimensions

Widgets occupy a rectangular region of the screen, which may be as small as a single character or as large as the screen (potentially larger if scrolling is enabled).

Box Model

The following styles influence the dimensions of a widget.

  • width and height define the size of the widget.
  • padding adds optional space around the content area.
  • border draws an optional rectangular border around the padding and the content area.

Additionally, the margin style adds space around a widget's border, which isn't technically part of the widget, but provides visual separation between widgets.

Together these styles compose the widget's box model. The following diagram shows how these settings are combined:

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXGtT2txcdTAwMTb+3l/h+H4tu/t+6cyZM9VqrbfWevfMO51cYlx1MDAwMaJAaFx1MDAxMlx1MDAxNHyn//2sXHUwMDFklCSERFCw6Dn50EpcdTAwMTKSxd7redaz1r78825lZTVcdTAwMWF03dWPK6tuv+q0vFrg3K6+t+dv3CD0/Fx1MDAwZVxcovHn0O9cdTAwMDXV+M5mXHUwMDE0dcOPXHUwMDFmPrSd4NqNui2n6qJcdTAwMWIv7DmtMOrVPFx1MDAxZlX99lx1MDAwNy9y2+G/7b/7Ttv9V9dv16JcdTAwMDAlL6m4NS/yg+G73JbbdjtRXGJP/1x1MDAwZnxeWfkn/jdlXeBWI6fTaLnxXHUwMDE34kuJgZzT8bP7fic2llBFleCck9FcdTAwMWRe+Fx1MDAxOd5cdTAwMTe5NbhcXFx1MDAwN5vd5Io9tTo46Tt6Y//TbvubXHUwMDFmnISHXHUwMDFkv16tJa+te63WYTRoXHKbwqk2e0HKqDBcbvxr99SrRU379rHzo+/V/MhcdTAwMWEwulx1MDAxY/i9RrPjhmHmS37XqXrRwJ7DeHR22FxmXHUwMDFmV5IzfdtcdTAwMDZYXCIhpMGYXHUwMDEwybA0cnQ5flx1MDAwMMdIY2FcYiZGKEFcdTAwMTVcdTAwMWYzbd1vQW+AaX/h+Ehsu3Sq11xyMLBTXHUwMDFi3Vx1MDAxM1x1MDAwNU4n7DpcdTAwMDH0WXLf7f2PXHUwMDE22CCmNJVUXHUwMDBiplxyS35P0/VcdTAwMWHNyFx1MDAxYUtcdMJaXHUwMDE4psTwbYmxoVx1MDAxYndcZtjJXGI8JfmR1oTu11rsI3+Pt2vTXHS69823XHUwMDFh2lx1MDAwZinzreVcdTAwMWLjXHUwMDBllnayVN9/r+v+vt5vXa5HV0dH52dVXHUwMDE11ndGz8p4pFx1MDAxM1x1MDAwNP7t6ujK7/u/XHUwMDEy03rdmjP0MlwiJSOSSo0l06PrLa9zXHJcdTAwMTc7vVYrOedXr1x1MDAxM8eMz/5+/1x1MDAwNERIZVxuXHUwMDExYYji4Fx1MDAwNFRNjYjdra+tur9f3aq3XHUwMDA3P1x1MDAxYd7N7lx1MDAwMd1cZlx1MDAwYlx1MDAxMFx1MDAxMfqA75nxMPatx+DAXHUwMDFlRYNcdTAwMDI0XHUwMDE4TVx1MDAxOVx1MDAxNVx1MDAxYyshWFx1MDAxNlxylGokJSZKXHS4R6cvj6NB1FmtykvR8Fx1MDAxN69Kty7ySGBCIS1cdTAwMDSXWok8XGKoMIhcdTAwMWFcdTAwMDNewVx1MDAxOcNU5EFAXHUwMDE5XHUwMDEzklxirl9cdTAwMTZcdTAwMDTVb7vnzbOt49rAmE5vn3RVs+a9Qlx1MDAxMHBZXGZcdTAwMDImMeVUSjE1XGKud6+u2mHw6zRw+4HZONu46PHPT1x1MDAwYlx1MDAwYrRcYlx1MDAwNjUnbM43LFxiRpFSXFxIYaiSWpAsXHUwMDBlXHUwMDE0uKDWWlx1MDAxOUOoIIqwQlx1MDAxY7hSqedEXHUwMDA18O88XHUwMDA0SMq1XHUwMDFmiJ9pXGJh0DX8pZz+wZdcIrdcdTAwMWZlvXzY8TtcdTAwMTef9k5+bW1drG1dXHUwMDA0P2818/prTsrn309+7PDLd1x1MDAwM7355Yiz3Z1+c933mt+rXHUwMDFi11+WXHUwMDEzS5nfn5Z/KZCMwchIQjhcdTAwMDeKmlx1MDAxYUU3XHUwMDFlZnzT7Kvewefq9aU52D6W3+YsrmZcZiaPg0hcdTAwMWGDuLA/lEpmtKFcdTAwMTlcdTAwMTBcdMKQ1pJcdTAwMTgjtNRcdTAwMTBNXHUwMDE2pqwkzUOIilx1MDAxY4KwkFQxiHHzR9A8nTHpdL9cdTAwMTNcdTAwMWR6d7bdKc6c3XTaXmuQ6bfYS8HSPSdoeJ10W4YuvDMmd525+1PLa1g/Xm259ayDR1x1MDAxZWQjo8uRn/rlVXi7XHUwMDAzj1x1MDAwYr7Wxn+FXHUwMDFmePBmp3WUteRJ2GKiXHUwMDEwW1x1MDAwNGSaUcDabGpwXHLCzs7hQe1qw2l+3dq9lp2Dm7D5gplcdTAwMGJ+XCK6IERRK4K0XHLLXFxk0Fx1MDAwNalcdTAwMWJcdTAwMDLgYSpccmRcdTAwMTOMXHUwMDFhtTB4pXKicngpSFxcXGZ+YVXmqeb2TXV7/etGh31yu+etSPw8mmskSdTSosH73anVvE5jXHUwMDE50PtgytNCI1bjZ1x1MDAxZuBcdTAwMGLaXHUwMDFkXHUwMDEyXFwpplx1MDAwZo2Tdcayo1cwVVwiMFx1MDAxOSdIvZDA5Fx1MDAxM1x1MDAwNCZNvW9cYl9gXHUwMDFiyLYgUL98cFxcXHUwMDE0vlhcdTAwMGVf63BcdTAwMTmsWoGmciaDzExcdTAwMDZZXHUwMDE1vuVcdTAwMDYlMGt7tVo628pcIu2xJGlcdTAwMWN8XHUwMDE5O0tcdTAwMTFYnuhpXlx1MDAwNEPCpWCCSjJ9tWPnR6O5dbm5b1x1MDAwNs2rk8b+USDO+rdcdTAwMDU4rFx1MDAwNn5cdTAwMThWmk5UbVx1MDAxNmFxvNC2OJlcdTAwMWFcdTAwMTc9tFx1MDAxMYRgI1xyIYxnsEipRJBZXHRNjZREM1lcXFx1MDAwMpyi6FGKxcdcdTAwMGJcdTAwMWaGcCzzwVVcYlxyvVx1MDAwNVx1MDAwMvZlY+vF55/O2t2nauv48MugvXf16cfBXHUwMDE1ny62lmZ/e9v9XHLj17c3RXBUOe+37rw9df3HYnYpwIbvn1x1MDAwNC4qcVx1MDAxMbooMUopkGVTg6u8pWdcdTAwMDZXYSVl7uBcdTAwMTJGIU4heGCNtVx1MDAwMcfO5oBcdTAwMTSucmhcdGIgXHUwMDAzNGRxOSAjiHJcYqZcdTAwMWNiKSeQdubxxTRcdTAwMTJcdTAwMDRMZFx1MDAxNFBuMOXjKCNYWv+RqXxyapjFpr50XHUwMDEwXGYjJ4jWvE6s1D6mkPYwcjSMPj0xOOmJdXzt4cPTtmxf3ahcdTAwMTM/XHUwMDA1N1xim9Ve7Fx1MDAwMlxiY26w4Fx1MDAxYfqCXHUwMDFhbFL3NJxu3ESIKMNcdTAwMTVcdTAwMTAp3Mah3+/vXHUwMDE4XHUwMDAxftXt1Fx1MDAxZTepPJikTKpgRDU3jFxi8DBwMS1VziiKjFx1MDAwMXMgXHSC+5SUQuWMajlhtO63257Ved99r1x1MDAxM403cdyWnyzam66Tk8fwo9LXxmmha5+YpdPkr5VcdTAwMDQy8YfR33+/n3h3oSvbo5Lz4uRx79L/z6rZ7buK+Ixhrlx1MDAxOZMzpNzlLvdcInz2RN1O49ZXoFx1MDAxNojmNFW5XHUwMDE51rQ4XHUwMDAyNU9cdTAwMTlIXHUwMDA1cDSlx+yaY00rXHT1JUm35sImXHUwMDEwL1dcdTAwMTR+ti5YQPyeJSfI59xrflBLS/s/l3LfW/I0OWL5sVxivuCwilE9vdQv12fzXHUwMDE505k7cpUmXGJcdTAwMTRcYoGMmyvKx1Q+5DqIgFx1MDAxMiFcdTAwMDRcdTAwMTBDXHUwMDE0Xdw4P1x1MDAwNCwhpVxyVMDdXFzTXHTFaWB4XHUwMDA29zCOXHUwMDE52GnrezkpXCKYYoxi/lx1MDAwNGQvg1x1MDAxNFx1MDAxOVx1MDAwZp5zUFx1MDAwNMNYr5GWXHUwMDE0Q8tcdTAwMTgsQX/wtERJaVx1MDAwNoWNVkpyyZTQkHK9akFQ6FH2qOSdaUZFUMwpjJeMXHUwMDE0K2MoVnz6Ql75JJIlZlx1MDAxNWh6XHUwMDAzv5RA2/Ox/IZhXHUwMDA0XHQ7tFx1MDAxM8EgkPi4XfNkXHUwMDE1XHTv4LFcdTAwMTZcdTAwMDbP5olcZs7QitJcdTAwMTRslWCtJFxc5jNcdTAwMWNcdTAwMDHGgqh5ylx1MDAxONjrpJXyWWsp0oCO5EZcdTAwMDCSKFFcdTAwMDI4I2m8JPl55SxS5EH2yPvOjCxcdTAwMTJrplx0JKJTXHUwMDEyepxDpGBcdTAwMDY4LTUp6zFcdTAwMGVZ29w4ucWuUP0tsbl95ZxXfTlY9nFyYFxyXHUwMDA09IHt1Cqp+Vx1MDAxOIdAXHUwMDFlh1xmqFx1MDAxMiPhXHUwMDA2I1x1MDAwNF/kXHUwMDE0RESkMpPLj1x1MDAwNI1XJlx1MDAxZkhDXHUwMDEzhTGHZPOtkUb2YfPFcubaXFyBPKFcdTAwMTftMeq/OVx1MDAwMVfIYuBqJY2tqE9fXHUwMDEwYNu/blx1MDAwZqtcdTAwMDbj3unNSbOmyMHaN7b0wIWmVpBdXHUwMDFiqZSwXHUwMDAzXGJcdTAwMTngcq3tbF3OoNlcdTAwMDEgIDdcdTAwMTdcdTAwMDZcXFx1MDAwZbJXXHUwMDEwkVx1MDAxZVx1MDAxOVx1MDAxOOFcdTAwMTYjXHUwMDE2K8NcdMDlXHUwMDAwXFyRKVP8XHUwMDFmuH9cdTAwMTC4+V60RyXpwHlcdPd0oT1cdTAwMDddyCVcdTAwMTQ1eHrofu7XpXvxq7Le9DaOf7Y/fz6+/LGx9NBVXHUwMDE0KfBHyqmiXHUwMDEyXHUwMDA0XVx1MDAwNrqMcWQ4t9OIXHUwMDE1aFx1MDAxZbLIaoBRRnHGXGJjoKxcZp9Q14PcXHUwMDAxXHSMNYeeseV3kUoj7mc8cylcdTAwMTSh4pVcdTAwMDK5SJz7Z8e7J25w1T350Vx1MDAxZmzc7eytNUNZMFxugDlcdTAwMTaMQdBRiimuVaoqnoxNUFx1MDAwMklcdTAwMWFcdTAwMTNwp5mQ879cdTAwMTiFLFTHV4pdKr6c96Z50VxuKFg2fvqBV5TClNmumZpWSO07ub08qG5+ObxsrK/fnlx1MDAwN+J4b/lphSBbeLKDUYaQ8Wk9SiNlsJ3xg+2s2MVccndcdTAwMTJkbbAlXHUwMDA3zOFl6XZcdTAwMWbRiiHgKKBbiKCaKZErXHUwMDA2KEq4XHUwMDA2J3mlsv7ZpFx1MDAwMjKaQKbLJFx1MDAwNVxmYYEpyXOKRra4Q5WhmFx1MDAxMSHMXHUwMDFi5ZRid7LHuCPNyCdFI466eFx1MDAwMoWCfrEj0dOLlPJeX1Y24Vx1MDAxY0lNtDBKXGLI/rOFXHUwMDAxxlx1MDAwNbI+Z9dCcWpSq3bmXlx1MDAxN0hcdTAwMWVdMthoQDpcdTAwMTKhzVx1MDAwYk/wLY9cdTAwMTNcdTAwMTlPm2lcdTAwMTJSuawtfe6D2y+C5Z40iLk17LWUXHUwMDAz/KlBzHtLSlx0oajiYEwhIVx1MDAxMMUhrIKomX698vlp4/i6urfryruoe0hMv1x1MDAxMX3dWnZG4DZtXHUwMDExTFPQd5ZcdTAwMDGzaVx1MDAwYsVcdTAwMDYkrq38gPbA6ULiXHUwMDAyhlx1MDAxYqC9gfCp4cD8lEzQXHUwMDE3QE9cdTAwMTiDXHUwMDEw1Vx1MDAwMi5TjXW+XHUwMDAwXHUwMDAxeZfAdsnz65RcdTAwMThvrVx1MDAwMFHcq/ao5Dt0xkhfXHUwMDA07LQ8XHUwMDFkn5sgjLaZw/TDiL1m55Z4XHUwMDBl+bV5Snb5trk46vhF85CXXHUwMDA215pcdESltPPqMNCcyuZcclx1MDAxNEtEQIcyw5nGgJrFVVx1MDAxMjHSRmktXHUwMDA1SCwstZ5Uj+B2XHUwMDE3XHUwMDAyUIFK2GXimuSHXHUwMDExJaRcdTAwMGWcavXmZie8VlxcXHUwMDE3dao9Krn+nFx1MDAxMdbFXHUwMDA1gZJcdTAwMTVcdTAwMDbUzlx1MDAxMOCQXHUwMDExT4/s5vb15UG3Ujlccu+2XHUwMDBl1ytsoPYrRZOgl1x1MDAwNtmSaGR3XHUwMDE2XHUwMDAxXHUwMDE1z5igKrtMj1x1MDAxOMu5XHUwMDE4Wt72S3pa8twrXHUwMDAyTFwiXHUwMDEwXGbA21x1MDAwNOjFpEZdUyN8QENcdTAwMWOua8ZcdTAwMTRcdTAwMDVcdJFbZc4451pcdTAwMTP6SuN1UUngSNd37i62XHUwMDFiP9yzjbWdwDn9eXjzs6DOSOxcXFx1MDAwZYhJXHUwMDEyMmHDSXqyTVJntHOMsVx1MDAxMZgzm1x1MDAxMt3f8NaKXHUwMDAylUKXXHUwMDFhXs1509xoJb1mLbdxXHUwMDExwVSotPZ9jFakS25PXHUwMDBm1k7Xe7+O62FwuH6MXbr0tGIoMsDXkFx1MDAwYmBlXGZcdTAwMWKjXHUwMDE1bZBcItb9qFLMyOLlg8+mXHUwMDE1amfcXHUwMDAxfeF4jDNN+JlcdTAwMTFIwlx0RJ94sJHp9GLke8VAiMBcdTAwMDJj/r/KLFx1MDAxOMHPl1RBXG5cdTAwMGKQsqFxwvxcIoKInZwllMKEQ7fn1zG8XHJmKXYqe1Qm+NOM1FJUclQlK1wiXHK1vjLDfMbyvl9WXrFNXHUwMDBmylx1MDAxZNI7q81otuRIlERSc6OJsfNcdTAwMWQoW1x1MDAxY7GI5NFlu1xuQMakiZb6XHS88ZyiY7lcdTAwMTTN+NpMRcfyWFT63Fx1MDAwN8dfXHUwMDA02z2p6Dh03pRcdTAwMDP8qZrj0JCnaVxyXjxVgjOioEVnmJ1YvqvR0s5wZlxiYo3dQ4RplopLw0lOQiFiXGaVIDMoXHUwMDExophcdTAwMTBcdTAwMThnXHUwMDBlf1ZlXHUwMDAyXHUwMDEzO81cdTAwMTAzXGJcdTAwMGbAw8JM2nfEqlwiXHUwMDAxYcFcdTAwMTZBIUayXFxcbmOkgUeItzdZsUiClO8tMFx1MDAxMlx1MDAxN1x1MDAxMlx1MDAxOWm1uuTSjk7zlJhP0lx1MDAxZrtcdTAwMDCUXHUwMDAw30L+w6lcdTAwMDHflzlcdTAwMDXymnRGpcSn4ut5d5pRaFx1MDAxNOcwqmRcdTAwMDcjIZmgRE5PLOWb3CwtsVDEscF2koFcdTAwMTFybPKkXHUwMDA0satBXGZcdTAwMTOFXHUwMDAxPCUrKZ/PK5rH0/ol1liI9JKqpDKikVx1MDAwNPlHIKM1oI7YhFx1MDAwNVlgpmBY0SdswbBcZrxSuD6idG+sLDdASlwiuYYkRoNOJGpCZUTYtTKScFx1MDAwNUhcdTAwMTOg3lx1MDAxZiqNby2BqVx1MDAxNDuVPfLuNCOtlG7rYkjhts5cdTAwMTRDjMaE0umzmIprjs+jy7NQRc2Ts1x1MDAwZebru79cdTAwMGVcbqhlubZ10Vx1MDAxONJlu1kpt9BMVyaG27pcdTAwMThky3eGXHRNKFBNMcM8f1tcdTAwMTeCXHUwMDE4J0VcdTAwMGIrqKCIMUj9qWRcdTAwMDVcdTAwMTO1IeBcdTAwMWFcdTAwMGXukrTEsq/jLs1yXHUwMDE2ur+LgoQwtfXmtPu7vLt/6KrT7Vx1MDAxZUbwyFx1MDAxMSlCW3u1++wneczqjeferk3Y1bhcdTAwMWVcdTAwMWbW5LhcdTAwMTEsQlxc29L//H73+7/nXHUwMDBiXHUwMDAzXCIifQ== MarginPaddingContent areaBorderHeightWidth

Width and height

Setting the width restricts the number of columns used by a widget, and setting the height restricts the number of rows. Let's look at an example which sets both dimensions.

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


TEXT = """I must not fear.
Fear is the mind-killer.
Fear is the little-death that brings total obliteration.
I will face my fear.
I will permit it to pass over me and through me.
And when it has gone past, I will turn the inner eye to see its path.
Where the fear has gone there will be nothing. Only I will remain."""


class DimensionsApp(App):
    def compose(self) -> ComposeResult:
        self.widget = Static(TEXT)
        yield self.widget

    def on_mount(self) -> None:
        self.widget.styles.background = "purple"
        self.widget.styles.width = 30
        self.widget.styles.height = 10


if __name__ == "__main__":
    app = DimensionsApp()
    app.run()

This code produces the following result.

DimensionsApp I must not fear. Fear is the mind-killer. Fear is the little-death that  brings total obliteration. I will face my fear. I will permit it to pass over  me and through me. And when it has gone past, I  will turn the inner eye to see its path.

Note how the text wraps in the widget, and is cropped because it doesn't fit in the space provided.

Auto dimensions

In practice, we generally want the size of a widget to adapt to its content, which we can do by setting a dimension to "auto".

Let's set the height to auto and see what happens.

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


TEXT = """I must not fear.
Fear is the mind-killer.
Fear is the little-death that brings total obliteration.
I will face my fear.
I will permit it to pass over me and through me.
And when it has gone past, I will turn the inner eye to see its path.
Where the fear has gone there will be nothing. Only I will remain."""


class DimensionsApp(App):
    def compose(self) -> ComposeResult:
        self.widget = Static(TEXT)
        yield self.widget

    def on_mount(self) -> None:
        self.widget.styles.background = "purple"
        self.widget.styles.width = 30
        self.widget.styles.height = "auto"


if __name__ == "__main__":
    app = DimensionsApp()
    app.run()

If you run this you will see the height of the widget now grows to accommodate the full text:

DimensionsApp I must not fear. Fear is the mind-killer. Fear is the little-death that  brings total obliteration. I will face my fear. I will permit it to pass over  me and through me. And when it has gone past, I  will turn the inner eye to see its path. Where the fear has gone there  will be nothing. Only I will  remain.

Units

Textual offers a few different units which allow you to specify dimensions relative to the screen or container. Relative units can better make use of available space if the user resizes the terminal.

  • Percentage units are given as a number followed by a percent (%) symbol and will set a dimension to a proportion of the widget's parent size. For instance, setting width to "50%" will cause a widget to be half the width of its parent.
  • View units are similar to percentage units, but explicitly reference a dimension. The vw unit sets a dimension to a percentage of the terminal width, and vh sets a dimension to a percentage of the terminal height.
  • The w unit sets a dimension to a percentage of the available width (which may be smaller than the terminal size if the widget is within another widget).
  • The h unit sets a dimension to a percentage of the available height.

The following example demonstrates applying percentage units:

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


TEXT = """I must not fear.
Fear is the mind-killer.
Fear is the little-death that brings total obliteration.
I will face my fear.
I will permit it to pass over me and through me.
And when it has gone past, I will turn the inner eye to see its path.
Where the fear has gone there will be nothing. Only I will remain."""


class DimensionsApp(App):
    def compose(self) -> ComposeResult:
        self.widget = Static(TEXT)
        yield self.widget

    def on_mount(self) -> None:
        self.widget.styles.background = "purple"
        self.widget.styles.width = "50%"
        self.widget.styles.height = "80%"


if __name__ == "__main__":
    app = DimensionsApp()
    app.run()

With the width set to "50%" and the height set to "80%", the widget will keep those relative dimensions when resizing the terminal window:

DimensionsApp I must not fear. Fear is the mind-killer. Fear is the little-death that  brings total obliteration. I will face my fear. I will permit it to pass over  me and through me. And when it has gone past, I  will turn the inner eye to see its path. Where the fear has gone there  will be nothing. Only I will  remain.

DimensionsApp I must not fear. Fear is the mind-killer. Fear is the little-death that brings  total obliteration. I will face my fear. I will permit it to pass over me and  through me. And when it has gone past, I will turn  the inner eye to see its path. Where the fear has gone there will be  nothing. Only I will remain.

DimensionsApp I must not fear. Fear is the mind-killer. Fear is the little-death that brings total obliteration. I will face my fear. I will permit it to pass over me and through me. And when it has gone past, I will turn the inner eye to see  its path. Where the fear has gone there will be nothing. Only I will  remain.

FR units

Percentage units can be problematic for some relative values. For instance, if we want to divide the screen into thirds, we would have to set a dimension to 33.3333333333% which is awkward. Textual supports fr units which are often better than percentage-based units for these situations.

When specifying fr units for a given dimension, Textual will divide the available space by the sum of the fr units on that dimension. That space will then be divided amongst the widgets as a proportion of their individual fr values.

Let's look at an example. We will create two widgets, one with a height of "2fr" and one with a height of "1fr".

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


TEXT = """I must not fear.
Fear is the mind-killer.
Fear is the little-death that brings total obliteration.
I will face my fear.
I will permit it to pass over me and through me.
And when it has gone past, I will turn the inner eye to see its path.
Where the fear has gone there will be nothing. Only I will remain."""


class DimensionsApp(App):
    def compose(self) -> ComposeResult:
        self.widget1 = Static(TEXT)
        yield self.widget1
        self.widget2 = Static(TEXT)
        yield self.widget2

    def on_mount(self) -> None:
        self.widget1.styles.background = "purple"
        self.widget2.styles.background = "darkgreen"
        self.widget1.styles.height = "2fr"
        self.widget2.styles.height = "1fr"


if __name__ == "__main__":
    app = DimensionsApp()
    app.run()

The total fr units for height is 3. The first widget will have a screen height of two thirds because its height style is set to 2fr. The second widget's height style is 1fr so its screen height will be one third. Here's what that looks like.

DimensionsApp I must not fear. Fear is the mind-killer. Fear is the little-death that brings total obliteration. I will face my fear. I will permit it to pass over me and through me. And when it has gone past, I will turn the inner eye to see its path. Where the fear has gone there will be nothing. Only I will remain. I must not fear. Fear is the mind-killer. Fear is the little-death that brings total obliteration. I will face my fear. I will permit it to pass over me and through me. And when it has gone past, I will turn the inner eye to see its path. Where the fear has gone there will be nothing. Only I will remain.

Maximum and minimums

The same units may also be used to set limits on a dimension. The following styles set minimum and maximum sizes and can accept any of the values used in width and height.

Padding

Padding adds space around your content which can aid readability. Setting padding to an integer will add that number additional rows and columns around the content area. The following example sets padding to 2:

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


TEXT = """I must not fear.
Fear is the mind-killer.
Fear is the little-death that brings total obliteration.
I will face my fear.
I will permit it to pass over me and through me.
And when it has gone past, I will turn the inner eye to see its path.
Where the fear has gone there will be nothing. Only I will remain."""


class PaddingApp(App):
    def compose(self) -> ComposeResult:
        self.widget = Static(TEXT)
        yield self.widget

    def on_mount(self) -> None:
        self.widget.styles.background = "purple"
        self.widget.styles.width = 30
        self.widget.styles.padding = 2


if __name__ == "__main__":
    app = PaddingApp()
    app.run()

Notice the additional space around the text:

PaddingApp I must not fear. Fear is the mind-killer. Fear is the little-death  that brings total  obliteration. I will face my fear. I will permit it to pass  over me and through me. And when it has gone past, I will turn the inner eye  to see its path. Where the fear has gone  there will be nothing.  Only I will remain.

You can also set padding to a tuple of two integers which will apply padding to the top/bottom and left/right edges. The following example sets padding to (2, 4) which adds two rows to the top and bottom of the widget, and 4 columns to the left and right of the widget.

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


TEXT = """I must not fear.
Fear is the mind-killer.
Fear is the little-death that brings total obliteration.
I will face my fear.
I will permit it to pass over me and through me.
And when it has gone past, I will turn the inner eye to see its path.
Where the fear has gone there will be nothing. Only I will remain."""


class PaddingApp(App):
    def compose(self) -> ComposeResult:
        self.widget = Static(TEXT)
        yield self.widget

    def on_mount(self) -> None:
        self.widget.styles.background = "purple"
        self.widget.styles.width = 30
        self.widget.styles.padding = (2, 4)


if __name__ == "__main__":
    app = PaddingApp()
    app.run()

Compare the output of this example to the previous example:

PaddingApp I must not fear. Fear is the  mind-killer. Fear is the  little-death that  brings total  obliteration. I will face my fear. I will permit it to  pass over me and  through me. And when it has gone  past, I will turn the  inner eye to see its  path. Where the fear has  gone there will be  nothing. Only I will  remain.

You can also set padding to a tuple of four values which applies padding to each edge individually. The first value is the padding for the top of the widget, followed by the right of the widget, then bottom, then left.

Border

The border style draws a border around a widget. To add a border set styles.border to a tuple of two values. The first value is the border type, which should be a string. The second value is the border color which will accept any value that works with color and background.

The following example adds a border around a widget:

border01.py
from textual.app import App, ComposeResult
from textual.widgets import Label

TEXT = """I must not fear.
Fear is the mind-killer.
Fear is the little-death that brings total obliteration.
I will face my fear.
I will permit it to pass over me and through me.
And when it has gone past, I will turn the inner eye to see its path.
Where the fear has gone there will be nothing. Only I will remain."""


class BorderApp(App):
    def compose(self) -> ComposeResult:
        self.widget = Label(TEXT)
        yield self.widget

    def on_mount(self) -> None:
        self.widget.styles.background = "darkblue"
        self.widget.styles.width = "50%"
        self.widget.styles.border = ("heavy", "yellow")


if __name__ == "__main__":
    app = BorderApp()
    app.run()

Here is the result:

BorderApp ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ I must not fear. Fear is the mind-killer. Fear is the little-death that brings  total obliteration. I will face my fear. I will permit it to pass over me and  through me. And when it has gone past, I will turn the inner eye to see its path. Where the fear has gone there will be  nothing. Only I will remain. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

There are many other border types. Run the following from the command prompt to preview them.

textual borders

Title alignment

Widgets have two attributes, border_title and border_subtitle which (if set) will be displayed within the border. The border_title attribute is displayed in the top border, and border_subtitle is displayed in the bottom border.

There are two styles to set the alignment of these border labels, which may be set to "left", "right", or "center".

The following example sets both titles and changes the alignment of the title (top) to "center".

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

TEXT = """I must not fear.
Fear is the mind-killer.
Fear is the little-death that brings total obliteration.
I will face my fear.
I will permit it to pass over me and through me.
And when it has gone past, I will turn the inner eye to see its path.
Where the fear has gone there will be nothing. Only I will remain."""


class BorderTitleApp(App[None]):
    def compose(self) -> ComposeResult:
        self.widget = Static(TEXT)
        yield self.widget

    def on_mount(self) -> None:
        self.widget.styles.background = "darkblue"
        self.widget.styles.width = "50%"
        self.widget.styles.border = ("heavy", "yellow")
        self.widget.border_title = "Litany Against Fear"
        self.widget.border_subtitle = "by Frank Herbert, in “Dune”"
        self.widget.styles.border_title_align = "center"


if __name__ == "__main__":
    app = BorderTitleApp()
    app.run()

Note the addition of the titles and their alignments:

BorderTitleApp ━━━━━━━━ Litany Against Fear ━━━━━━━━━ I must not fear. Fear is the mind-killer. Fear is the little-death that brings  total obliteration. I will face my fear. I will permit it to pass over me and  through me. And when it has gone past, I will turn the inner eye to see its path. Where the fear has gone there will be  nothing. Only I will remain. ━━━━━━━━ by Frank Herbert, in “Dune” 

Outline

Outline is similar to border and is set in the same way. The difference is that outline will not change the size of the widget, and may overlap the content area. The following example sets an outline on a widget:

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


TEXT = """I must not fear.
Fear is the mind-killer.
Fear is the little-death that brings total obliteration.
I will face my fear.
I will permit it to pass over me and through me.
And when it has gone past, I will turn the inner eye to see its path.
Where the fear has gone there will be nothing. Only I will remain."""


class OutlineApp(App):
    def compose(self) -> ComposeResult:
        self.widget = Static(TEXT)
        yield self.widget

    def on_mount(self) -> None:
        self.widget.styles.background = "darkblue"
        self.widget.styles.width = "50%"
        self.widget.styles.outline = ("heavy", "yellow")


if __name__ == "__main__":
    app = OutlineApp()
    app.run()

Notice how the outline overlaps the text in the widget.

OutlineApp ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ear is the mind-killer. ear is the little-death that brings  otal obliteration.  will face my fear.  will permit it to pass over me and  hrough me. nd when it has gone past, I will turn  he inner eye to see its path. here the fear has gone there will be  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Outline can be useful to emphasize a widget, but be mindful that it may obscure your content.

Box sizing

When you set padding or border it reduces the size of the widget's content area. In other words, setting padding or border won't change the width or height of the widget.

This is generally desirable when you arrange things on screen as you can add border or padding without breaking your layout. Occasionally though you may want to keep the size of the content area constant and grow the size of the widget to fit padding and border. The box-sizing style allows you to switch between these two modes.

If you set box_sizing to "content-box" then the space required for padding and border will be added to the widget dimensions. The default value of box_sizing is "border-box". Compare the box model diagram for content-box to the box model for border-box.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXGlT28pcdTAwMTL9nl9Bcb/Gysz0bJ2qV6/Yl1x1MDAxMCAsXHTJq1spYVx1MDAwYizwXHUwMDE2W8bArfz31yNcdTAwMTZJliWwMY6TukpCsEaWRjN9Tp/uWf55s7CwXHUwMDE43XSCxfdcdTAwMGKLwXXVb4S1rj9YfOvOX1x1MDAwNd1e2G5RkYg/99r9bjW+slx1MDAxZUWd3vt375p+9zKIOlxyv1x1MDAxYXhXYa/vN3pRv1x1MDAxNra9arv5LoyCZu+/7ueu31xm/tNpN2tR10tcdTAwMWVSXHRqYdTu3j0raFx1MDAwNM2gXHUwMDE19eju/6PPXHUwMDBiXHUwMDBi/8Q/U7XrXHUwMDA21chvnTeC+Fx1MDAwYnFRUkGpzPDZ3XYrrqxUXFxLieyxPOyt0tOioEaFZ1TjIClxp1x1MDAxNtVJX+2Y48anXHUwMDBmK8tcdTAwMDfXjWZ40lo+Tlx1MDAxZXpcdTAwMTY2XHUwMDFhh9FN464h/Gq9301VqVx1MDAxN3Xbl8GXsFx1MDAxNtWpnFx1MDAwZp1//F6tXHUwMDFkuVxuPFx1MDAxNnfb/fN6K+j1Ml9qd/xqXHUwMDE43bhzLKn/XSO8X0jOXFzTp1xuXGJcdTAwMGa1NVxcXHUwMDFhRW9rRNIg7lx1MDAwNkJ5Qlx1MDAxYtDKMlx1MDAwYkYrXHUwMDA1Q1VbaTeoL6hqf7H4SOp26lcvz6mCrdrjNVHXb/U6fpd6LLlucP/SiqFcdTAwMDfGXG4trFx1MDAwMouQvE89XGLP65HrXHUwMDEzwT1mXHUwMDE1glF3T9NJbYK4Y1x1MDAwNENtuNaQvKWrQ2erXHUwMDE2m8jfw1xyW/e7nfv2W+y5XHUwMDBmqfq7qq9ccttX2sZSnf/j+uvG9tnJPr+48LdVyGorXHUwMDFmLlx1MDAwZVx1MDAxZe+VMUi/221cdTAwMGZcdTAwMTZcdTAwMWZLft7/llSt36n5d2bmXkRIxplcdTAwMTQqeaFG2Lqkwla/0UjOtauXiWXGZ3++nVx1MDAwMFx1MDAxMFx1MDAxYaFcdTAwMTBcdTAwMTCAXHUwMDFjJTPPR0TQ6e5o4P5t31x1MDAxZixvfP+6Jzu6WYCIXpvQPTZcdTAwMWWGvvVcdTAwMTRcdTAwMWPgKTSA8VxiXHUwMDAymlx1MDAxYq1RSc6MzKCBc/C44Fx1MDAxYYAjWSAyVYhcdTAwMDZ1XHUwMDA2taosRcNfsqqDM5VHXHUwMDAyKONZpSTBUuVBIFx1MDAxNHpcdTAwMDJcdLNMXHUwMDAyMDKMXHUwMDFjXGK4Je5ijIOdLVxiqns7X+snm8e1XHUwMDFixFZ/l3dMvVx1MDAxNv6GIJBWXHUwMDE3gYBcYlxuXGZcIpfPXHUwMDA2XHUwMDAxRqfB7sdGY1x1MDAxYsz+5kr9495BuLkxmVtcdTAwMTCFbsHv1afrXHUwMDE2XHUwMDEwPCEtKs6UloZZkcWBXHUwMDA2jyhcdTAwMThAKaG4JNdQiINAXHUwMDFi81x1MDAxMq+Q7vNHXGJwaYdtXHUwMDFlXHUwMDA01YLqNWOT34K1o5Poy97H9kmwXHUwMDFmNcLLz8fV76NNPlxurqOUxb8tu+3h0fopu8L95tGn6HJ5aXn1eFuL5yGp9L5Tr27m6rfPfeCvw32mnmmlamxcdTAwMTHkpSaq1TiG37tcdTAwMTZcdTAwMWSz/vXHSudQXpyurP6wrZrembJcdTAwMTJcdTAwMWPT8z2NeOdWUFgrNaAwRmJcdTAwMDbxgNZcdTAwMDPpXHUwMDA0IEeUWlxmU9H0ZKBcdTAwMTZ5vFx1MDAwYjVcZndcdTAwMGVcbiW5YvNcbjpvmsaYdHq7XHUwMDE1XHUwMDFkhrdBrFEzZ9f9Zti4yfRbbKVU049+9zxspduyXHUwMDE30DOD2Mdnrl5qhOfOjlx1MDAxN1x1MDAxYsFZ1sCjkFx1MDAwMqfH4qidevMqPd2n23W3asNv0e6G9GS/cZStyUTYXHUwMDAyI1xusSWsZEKq57vTpUF978dutHVzc1xm6+2br6dflr75M4yy2ITgXCLhiNJyi1JcdTAwMDCyrDtcdTAwMDVhXHR65G+F0lrSP/Nq6EpcdNoydGkjKVx1MDAxYzMptz9cdTAwMTNvurRdw9Wr3dMts1x1MDAwNlx1MDAxZjuDXHK5PMDOL1x1MDAxM5Avw+6+X6uFrfN5XHUwMDAw70NVJvOMglx1MDAwZp99QC93XHUwMDAyUY+RXCIpl1x1MDAxZvNcbl5yfSVaWFxi7YlcdTAwMTlpYTlCXHUwMDBii9Tz7tHLhdRcbq36g3wj5PC1QsVUq1x1MDAwNWorfzTIcDTIqvStoFtcdTAwMDKzZlirpVx1MDAwM8Ms0p5cbuiGwZepZylcdTAwMDLLY1IsXHUwMDE0qFxcOM/CpFx1MDAxNM9cdTAwMDZitX/bWPmxXHUwMDE0+T40XHUwMDBlj1pcdTAwMWbPw5NOrVx1MDAwMIjVbrvXq9T9qFovXHUwMDAyoyxcdTAwMDLj1FWqS9BQkIekQJXT5CqDRc6ZR1wilYFVxlpGTqxcdTAwMTCLz8jPlGLx6Vx1MDAxY1xyXHUwMDEyXHUwMDE56Lxv1Vx1MDAxNtEgn3F+ctnfq96eXW037GF7XHUwMDBm+X57U/k4hYCyxj5/qHZPbtesXHUwMDFhyNslXFw7XHK7dj5TPnfPXHUwMDFmXHUwMDA1rZLgT3AgzjdqXGYnV97UY2OrMOkzdWxxMmlkoLlLsFx1MDAxYiVTuVx1MDAxNHdcdTAwMDPJOVx1MDAxNVtcdTAwMGLUXHUwMDFhWnElhlE/PZVcbpw8LpJcdTAwMWIzXHUwMDFjJLd6xFBcdTAwMDBYj0JRbUFw61x1MDAwNLVcdTAwMWPGXHUwMDE5xalWKlx1MDAxNJMgLa7qrL1gL/K70XLYiqXa+1x1MDAxNNjIXHUwMDEzVvtxt3qMSWRKWmpdgVxmXHUwMDEznC2e+524kz1uUFx1MDAxYdTuMomJOFh4XHUwMDFjK7vzYitwc7Mp+ss7XHUwMDFi+uhi/Zj8WCNYekDnI+ZcdTAwMTeDVq20Slx1MDAxNeZRXGKHwFx1MDAxNZBp0N8kdnmslPBcdTAwMTCpOiCQrjNaK1NUqdFuKVepht+LVtrNZuiU3n47bEXDTVx1MDAxY7flklx1MDAwM3w98HP6mF4qXTbMXGZcdTAwMWR3xyyjJr8tJKCJPzz+/vfbkVdcdTAwMTeasjsqOStObvcm/f+4op2DKkxhc1x0zKF7XGZKXHUwMDFibSwzpbTJpLvlnlx1MDAxMZI4nNpfMMwltdBcdTAwMTOCsKKEJTzxYrXw4qRW0lx1MDAxYiVht+JEwZKzXHUwMDE5R90v0Fx1MDAwNq/gw8eJXG7yUfdyu1tLi/tfXHUwMDE3dN/XZDJJwpUsnJjA3aBcdTAwMWaF3qlBqqfwW67SpjNcdTAwMDY1deyCXHUwMDE0XHUwMDFlkMKnuJskPcEziVx1MDAwMe/kXGLzNLNGXHUwMDEzsIFcdTAwMWPecEJgeuAlXHUwMDE3p7R2ro34W1oxXCJBTSxPjKtBMpD0x/JcXFAuXGYyhlx1MDAwMn9TNTLsP59cdTAwMTJcdTAwMDWGoTVGS01q0VKclFx1MDAxM1x1MDAwNdazmjqOSJm5aTYyrWX+dE1QaFDuqORtaUxRUMwqXHUwMDE0y1x1MDAxNFx1MDAwNjqMXHUwMDE0PSGNP19cdTAwMTWUz3mZY1bhhFxyXHUwMDE0SiphWHaCh+SS7NJYRnJNkVx1MDAwNac4dvqsotHB06lngopMJf3TtGKssFx1MDAwNFx1MDAxMUZcdTAwMTdwqfNRXHUwMDBlt4bCXHUwMDAxnCRT/9vxXG7ziClcdTAwMTSBQ3CjiFaS5khcdTAwMDKgXCJcdTAwMWFcdTAwMTk9O+83p5FcIlx1MDAwYnJH3nbGpJFYNo1gXHUwMDExhOJMpDUomLYqueIpXHUwMDEy0cd0Tn7b3T9cdTAwMWaovY3o1lZcdTAwMDbt7mQkMruxcsmUZ610Q+HkwFxmXHUwMDFmykJq4Vx1MDAxMblQkVx1MDAxNKhcZvLXXHUwMDFizpPoWdTU4kZcbklcdTAwMDYgR1xm7zGPWSlcdTAwMDGsJDVlNaRH9Vx1MDAxZcSJdrGRSc0km1x0iVx1MDAxMM+mXHUwMDA2l16DRLI3my62M2VTXHUwMDA1dnGvuqMyokOnXHUwMDA0bVxyavjsI7RcdTAwMTGIbUhrPz/qODw+Xr3+hD++NU5cdTAwMDbtlVUxOPig+nNcdTAwMGZtLjwmjFFcdTAwMWFJXHUwMDA0gM7mQIH0gbGK2oFcdTAwMTSEpk+vlzKQpJBcdTAwMTVX6Vx1MDAxMYRcdTAwMTSkIVx1MDAxNo9mxLxcdTAwMThO6Fx1MDAxMnbWUFx1MDAxNoCQxDz/QvnhyPeiOypJXHUwMDA3Tkvbo8XhsylpT0EygHq+tF/fXGYvwP9yeHKw9v3G9PR688BuzT10XHUwMDAxPDBAYVxmMi7lkFems1x1MDAxZfltyzl55Hio/lx1MDAxNaW9Mlx1MDAwNlx1MDAxMJVcdTAwMDYg8YVyRPaPOF5cdTAwMTC9W+DGpfRVzidzelx1MDAwN6TXkDNW9lxc8pQpTWX8XCIjxPXF8vbpkeZXl37YPFx1MDAxZfRcdTAwMWHLXHUwMDExT09cdTAwMWRNZ1x1MDAxMShcdTAwMDJcdTAwMTbIKFZcdTAwMDOOViPafFx1MDAxZYFTME2UJ8m6XHUwMDE1oHrAU8FcYsZrksirav1KsU3FxTlzmlx1MDAxNq9wLorlvlx1MDAxMoxcdTAwMTGNyedrgsp1/fvR5dbFWuvbh481pj/rw09zPzVWUiylwFquSbNcIiP1lWVcdTAwMTZccp5QXHUwMDAy3ChcdTAwMGXxrFx1MDAxNq+3Ror8XHUwMDA2Z4JcZl4yN8/BwKj5fORzgOojjXGLXHUwMDE1VC5nXHUwMDAwZCDaoJpgqvyLiIXE6lxcXHUwMDEwXHUwMDBi8zhDNMqNgVx0hdqkoPSYndQgUbhoXHUwMDE2uFL4h7JKiTm5Y9iQxuSUosFJa1xus5DWcCZcdTAwMTlcdTAwMWIjXHRZ3uvzSihMeU5cdTAwMTRKaS1YJYaUikBPXHUwMDEzj1x1MDAxM4ujdvmU11MqKlx1MDAwMWTJuCRIt1x1MDAxYc6KXHRiipeMS5b7ioypjTVnqVxc3Jbe98HuS2hutuOdm3e9ljKAXzXeeV+TUkYoyjtcdTAwMTBcdTAwMWZcdTAwMTdSQiwySM2PkXhY2j483Fplllx1MDAxZKxcdTAwMWT8WGnsbKnjrVx0XHUwMDA3JmbHXHTIPa1Rc5JcdTAwMTkk45TKJlx1MDAxZVB6XHUwMDFjhdVCXHUwMDE40NrK11t/wz3rVsBcdTAwMDI9xFxyQOmU/EtcdTAwMTSG8khcdTAwMDVZXHUwMDA0i0ZKrUx+XHQquVxmdGNcdTAwMTMzjl6IU8FMgr4/PFx1MDAwZlEp7ta4ON+jYzr7XCJoW1W4+EdwXHUwMDA2XGJcIj2g/uTKOlx1MDAxZV2cXHUwMDFlX0U7387WQrH+5Wrtw+lg3pFNze2RXHUwMDE3Z8LSoVx1MDAxZIqz0HbJIGFcdTAwMTW5e+Pmmr4mtEFow9yQXHUwMDExcVxijExLMM+6OaBWaYoxOeaCh3heoeapqGI2uDYgXne88bfFdUGfxqW57lx1MDAxY1x1MDAxM9UlXHUwMDEzlFThsiDBlFx1MDAwMW7ZXHUwMDE4XHUwMDEzXGZbS2ebn7+sRlx1MDAxYqe3N7jx+Vx1MDAxY7aD02kvXGaa/nxp48JEQ3yqKebnILJcdTAwMTOUXGZRKvlrZrTlbjFcdTAwMWRcdTAwMGVVbHq4ttTLwjDDrTI8PTybXHUwMDFhMDBWulx1MDAwNFx1MDAxYVx1MDAwMFx1MDAxOIGc59bPc0GBl8suzTgrQO2X0lxi089cbny++bz27Wj95OKK0LpcdTAwMTNt3X5a2d0uSDdyQOGmlFx1MDAxOVCMJNaIqcxuzlx1MDAxOTlHaigmKTTmf2y6scik4sK8NU2NVqB4taFVaMmN4vPXXG53t0Cavf7tdme9YTe+Nb/uhLti3lnFLVx1MDAxNSbbc0pcZrQxmF1taKRHoolcdTAwMTlFP5hg/PXEgqWIg7ulZZLiLyPtiEQj97hcdTAwMTVcdTAwMWNccuNuzbKFXHUwMDE0wz2QilIxoGZMKsJcdTAwMWGchlp4MakwjyCkXGJJWjO00pr0eox7SqFWXHUwMDE0JFx1MDAwMJWhdpTMilx1MDAwN+X9p3FKoUG5o5KzpTEppSjZaIrXTYJbiWdcdTAwMTV//mSl8n6fU0LhXHUwMDE2PWDAOVx1MDAxMDzIv1x1MDAwZi3r0tTyUlPUrIxb7FaypdWLk42JPirbe4BcIiVyrVx1MDAwMFx1MDAxM0RcdTAwMTgvyTaWS9CMrY2VbSx3QqX3fTD8XHUwMDEypptttvHOeFNcdTAwMDbwq5KNd1x1MDAxNZlMY5BwK5RcdTAwMThWU4huxtjdq3yjprmdXHUwMDAzXHKeUlJQjOYmobCko+M5TtJlXCKZXHUwMDExQFElXHRcdTAwMTBePJxJN/Lli4YzyYJJSVx1MDAxMztcdItSKVx1MDAxY7U9ifBQkY7Qd9tUMsjHLmi15NJMsunjbzdcdLriVoFyt5OMdfNL2ajhS+2h5syCllTKpEzJ96yQXHUwMDE5vZdBToD8TjKjUmJUcXnensaUXHUwMDFhxdFLSnRcdTAwMGUxXHUwMDBiUiBFfTVcdTAwMDazlO+FM7fMXCI8a4lcdTAwMTZAo0tGXHLNnpTK01x1MDAxMq1LXHUwMDBmoYSS9ZYvJ1x1MDAxNivjdVx1MDAwMlx1MDAxNH0wRcH6qOjFepriSW7BXHUwMDA1llx1MDAxNvJrtrj7srRcdTAwMTNtJjtcdTAwMGa8Mpo9JDOaXkpcdTAwMTjLjeYmtfjoIVwioV5cIk/odlx1MDAwNFaotMoss8jQx+jNuHL08WfEL5Vio3JH3pzGpJXS7V+wZFx1MDAxNlx1MDAxNrqlMEaPseji5lxmev1PW8dw+vX6y/fT6569Piqa3jlX279I5lHrut0nQIGgTshcdTAwMGWluP15rZvFgi5nJYUuli4v3/+Fe6SUXGZcdTAwMTZtXHUwMDAwIzxcdTAwMDBhXHUwMDE41aFgpjZcdTAwMTlcdTAwMTEhkU8yU/vfnWCet1x1MDAxM8yb+5su+p3OYUS3fCRFauuwdlx1MDAxZv8kt1m8XG6DwfKInZrP4sNVOW5cdTAwMDSHkMC19D8/3/z8P1KdJ/cifQ== MarginPaddingContent areaBorderHeightWidth

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXGtT28hcdTAwMTL9vr+CYr/G2nn0vFJ161x1MDAxNs9cdTAwMTBcdTAwMTJcYpCsgdzaSim2sFx1MDAwNbJlZFx1MDAxMSBb+e+3RybWw5KwjU2c7CqpXHUwMDA0NLLU1vQ5fbrn8fdva2vr8f3AW3+5tu7dtdzAb0fu7fpcdTAwMGJ7/otcdTAwMTdccv2wj00s+X1cdTAwMTjeRK3kym5cdTAwMWNcdTAwMGaGL//4o+dGV148XGLclud88Yc3bjCMb9p+6LTC3lx1MDAxZn7s9Yb/tf9cdTAwMWW6Pe8/g7DXjiMnfUjDa/txXHUwMDE4jZ7lXHUwMDA1Xs/rx0O8+//w97W1v5N/M9ZFXit2+53ASz6QNKVcdTAwMDaCkMWzh2E/MVZIXHUwMDAyyihcdTAwMTi3+8NtfFrstbHxXHUwMDAyLfbSXHUwMDE2e2r9rue/8+NmcPL57IB5XHUwMDE3LvGvXHUwMDA2nfShXHUwMDE3flx1MDAxMLyP74PRi3Bb3ZsoY9IwjsIr79Rvx11sp4Xz48+1w9hcdTAwMWEwbo7Cm06371xyh7lcdTAwMGaFXHUwMDAzt+XH9/ZcdTAwMWMh47Ojl/ByLT1zh79x7nBKtNRCcVwiOOhxq/082GaiOVx1MDAxN1xmXHUwMDE4SC5cdTAwMGKGbYVcdTAwMDH2XHUwMDA0XHUwMDFh9jtJjtSyz27rqoPm9dvja+LI7Vx1MDAwZlx1MDAwN26E/ZVed/vwlVx1MDAwNTFcdTAwMGVXmkmmXHUwMDA114an36br+Z1ubI1h1CFaXHUwMDE4rsToaVx1MDAxOWu8pFskp5wrotm4wZoweN1O/OOv4lvtutHg4eWtXHUwMDBm7S9cdTAwMTnzreU7RefKOlim51xy8TZO7k7J3edBXHUwMDEzneY0eHfahPG9ct7oRlF4uz5u+fbwU2razaDtjnyMSslcdTAwMTlcdTAwMTB8+6D5uD3w+1fY2L9cdIL0XFzYukrdMjn77cVcdTAwMWNokIZVoYFqYjRcdTAwMTDK2NR4ODm97MZcdTAwMWLhtup97Hxccj5+eCvfvb2vwMMwRGzPjIbCp1x1MDAxZVx1MDAwM1x1MDAwM39cZlx1MDAwYpSgt1x1MDAwYsZcYjFcdTAwMDZ9jGb8yH5eoP9RySRBJzXEyKJdKVx1MDAxOMRcdTAwMDVvt6BcdTAwMTZcZr9DS3pcdTAwMTdiXHUwMDEyXGJcXChHXHUwMDBiXHUwMDAxUisxiVx1MDAwMSaMw4yRmiA0XHRcdTAwMTOTXHUwMDE44JxqLkHA82Kg9e7tefds78/2vTH9m0M6UN22/1x1MDAxM2JcdTAwMDA0VGKAUMJcdTAwMDDUXGYxYftcXJxcXDc9ev9n8+xEXHUwMDBm995cdTAwMDSXXHUwMDFmTueLXHSsXG5cdTAwMDVtd9hdbEyg6GTC8jBwXHLAqVB5XHUwMDFjKOooQzlcdTAwMDEjXGZjylTiwJNKPSUooH9PQoBmYtSDzzNcboDxKXv5szj94U5M3vpcdTAwMWRcdTAwMTF/jravTuLgXCLce/Wm3Olj7y7O+PyLutteXGZOXulX/a2d/YbYi09aXHUwMDAxOT6/m1x1MDAwZUtcdTAwMTX3zVnxYtov8uMgmrMzKyiVrkInSCRFaVx1MDAxNJlcdTAwMWGcXHUwMDA3XHUwMDFmVaB29oP30cX713s3w43b1n60YME2Y4iaQq8x6TCigVx1MDAxOcWI4kJCXHUwMDBlm/hcblx1MDAxY661tGKNXHUwMDBiMLxcdTAwMTKbT1x1MDAxNWySTUKTiVwiMlx1MDAwMcMkXG6yZUSjRTpj2ulhP37vf7XvnZHc2V2351x1MDAwN/e5fku81DqSXHUwMDFidfx+9lVcdTAwMGU9fOZIN+Wu3lxi/I714/XAu8g7eOxjfjNujsPMN2/h0128XfS6XfxcdTAwMTZh5OOT3eBD3pK5sMVcdTAwMTWtwlx1MDAxNkXppylcdTAwMTAxfeR7c35w+nZn+PntznHcUTuvOifdT3NGvrmyITJcdTAwMWa6qHFAXHUwMDAxXHUwMDEwXGZrhGNOkkeX4tIxkoNAiYgvg6uloSujMWrQRVxyXHUwMDEzXHUwMDEyreR68fCqXHUwMDBiUFFHXHUwMDFlNFx1MDAwM/r5LrzqkyDUp2Hv4vCHib2ngffIbbf9fmdcdTAwMTXQ+92U+UJj5itcdTAwMTfhK4iNXHUwMDE3POMmj8G3XtmsKnyphDrhisjF5OlZhCuUXGJXllx0xlx1MDAwZvhcdTAwMDVcblx1MDAxOMzR8X+d8MgnXHUwMDEwtoXNaNVcdTAwMWG+K7dcdTAwMWNmplx1MDAxY2Yt/JRcdTAwMTfVXHUwMDAwree329k0Lo+1x7KvXCL8cnbWYrA+gzSqXHUwMDEyiEQxRtksXHUwMDE5ZGfbb59s3HXce/ewXHUwMDE37t1cZm72gFRcdTAwMDCxXHUwMDE1hcNho+vGrW5cdTAwMTVcdTAwMTihXG6MXHUwMDBiXHUwMDE3qkkxXHUwMDA1OCgjXHUwMDA0l0LlXHUwMDBii4JqR6AyJJpQplxyiEooTlFLqYXi4/VcdTAwMTSDykaWXHUwMDA0V8JcdTAwMTlVmstnriY2L11/6+p6p8ng3WB/+PngVu2cPyn7XHUwMDFi3fe8/fVi19/a273YdU9PT10uP7Y+rWaFZvT8MmwhdqrAhXxcdTAwMGWU80xF7DFs1b/pmbFVWaFZOLZcdTAwMThcdTAwMDOHalx1MDAxNKuGIbKooDxcdTAwMDcujSrWgNFcdTAwMDQ1LJGELS9cdOTUYWAoXHUwMDAzRTlQLUuq9txcdTAwMDLdSI1g0txcdTAwMTBcdTAwMDZFnKH1wsBcXCliYulzx8Bh7Ebxpt9PpNrLXGbSMFx1MDAwZbZukk51XGJKXGZMXHUwMDEwNL5cXFsqTkG23nFcdTAwMDdJXHUwMDE3O1x1MDAxNN1VXHUwMDE5aS9cdTAwMDOTdtHaeExrXHUwMDE0w+KtY3bNdjY25T5rvTmmh/y8t/1cdTAwMWSaY8Cve/12rUlccuIwXHJcdTAwMDYlXHUwMDExJ1rjX6kmjGKOMWhcdTAwMGVnXHUwMDA2r1NSXG5VZVR5UJowKnCH8VbY6/lW51x1MDAxZIV+Py6+4uRdbli0dz13Qlx1MDAxZuOXyrZcdTAwMTVpYWDvmKfT9Ke1XHUwMDE0Mskv45//elF6daUn26Mx4cTp7X7L/j+raEfXXHUwMDE3xdNjsYCRx8pYPT2hlTvLs1x1MDAxMtqcwlx1MDAxZDRihitqhLKygFx1MDAxNataysGEXFxK7Fx1MDAxZMy6dbVaeHJVK33ZdXk31cJIhO3PI1xyllx1MDAxMMJnyVxuJvPuzTBqZ8X9j0u7XHUwMDFmLJlPkVBUlpVcYmbSKIXeP/2gab1IW8yA0cLRXHUwMDBiRDtSXHUwMDEwJE4g3GiTwedIjmAzZkVcdTAwMWP9TGnk0KWhXHUwMDE3g5yQ0lx1MDAwNjdkcNCspESNPM/xXHUwMDFhjpaCLVx1MDAxM9CJpNygnVx1MDAxNNRcdTAwMWMltVWQI8VcdTAwMDD6mCpQxGilJGDXXGKt02g0Vlx1MDAwNdrRkmEqRFxmkahUICtmfnVRUOlP9mhMutKMqqCaVLjkxdNjUsHwx7iZZWZS/fyUXHUwMDE1Jlx1MDAxNVx1MDAwMIZZu1accZpGkIRTXHUwMDE4qlWCnlx1MDAwYlRcdTAwMTCioLqU93ROkYZcdTAwMTJI1LN9Xiqcc6SiNNOIXHUwMDEwNFx1MDAwNclDTiQ50tY5XHUwMDA05mT/XHUwMDAwViHYb0YgNFx1MDAxOFVcdTAwMDJJJX1cdTAwMWJp/lNFXCLlk+h+clx1MDAxMqlyIHtMus6MJJJoplx1MDAxMlx1MDAwZdGiejyAII1cdTAwMGL0yOnHynfc+91B0CHNe9o6+nIs2NZ1pz9cdTAwMWaFPN9YOShw8ItcIj2D4sJQmtclglJcdTAwMDdTPWCUXCLhSrk8XVwiMN2XmOyXliCpU6xOfs8yXHUwMDEwPVxcSzPPdMbVJo38zVx1MDAxNovlXFzbQoFcXNKL9lx1MDAxOPffgoCLwKxcdTAwMDIuYlpcYok6e2rcfo1JTM8uL4/5K7155DF+pE/C1cetclx1MDAwNFKmRKFjU6g8bJUmXHUwMDBlalx1MDAwMi2JoFx1MDAxY4XZ8opcdTAwMDGA2ldQkVx1MDAxZFx1MDAxYlx1MDAxOKOWODyRhSWwTSZcdTAwMGZgXHUwMDE2NMeg3r+wTdpcdTAwMTZcbtvJXrRHI+3ARal2wyonp1FJmJI0XHUwMDFik1x1MDAxZkNu97T1seG7uumfiNthXHUwMDE0XGbgar9q4G91kGvszFHCXGZTzNhZNPmBXHTJXHUwMDE0akOpNGWYWGUnky9etVx1MDAxYrt4g9tcdTAwMDVcdTAwMDE2PYCSup5cdTAwMWShxORBg5DKlutFxpxcdTAwMTGUNbdF2J82XHUwMDAwl2f8XHUwMDA0iOBcXFx1MDAxYqFcdTAwMTRXoFVWi49HJ1BcdTAwMTNJ4Fx1MDAxOG64ydVcdTAwMDRyYl1cXEf87O71wac+XHUwMDA0fPPr8aG5fffpkbGJZXLIUmV8o9qlkuZJb1pcdTAwMTSvUKKqS4xIYVxcIK9MX2I87X9yNztcdTAwMWJcdTAwMDfXsblcdTAwMGVft44/6aG6Wn1iUY62Yy+Gg9CSZtZtJcSiqMOMXaYkUVx1MDAxNDC+vFx1MDAxMU9MXHUwMDE5gFx1MDAxM84lXHUwMDEwUJiz8ZJygKHoKlxuXHUwMDEzXHUwMDBlwTRXYrJcdTAwMTiAmFI8y0k/O62gMqaYvFwiWVwiLlxieiOdZFx1MDAxNe1IXHUwMDBlXHUwMDE4XHUwMDE4XGYjnFxuUVlH/EexSrU72aPoSDMyStWoo1bV1UXQXHUwMDA0I7NR009cdTAwMTUs769V51x1MDAxMyRcZoxvmml0VU15fqWX5MqRQlx1MDAwM1x1MDAwM8XxXmp583xTXHUwMDE41I43XHUwMDAyp9RcdTAwMGXaz05cdTAwMThPXHUwMDE5bqyPXHUwMDE0OVebaSZSvbStve93v19cdTAwMDbNzTWMuTfqtYxcdTAwMDP8qGHMXHUwMDA3S2pcdTAwMTmhquaQXHUwMDFioyzOq1wiRlx1MDAxYqpcdTAwMTlMP1x1MDAwZmEn2N30olx1MDAwZsHB9kWjcfl+b/NAdVc+d7GpXHUwMDBiQt1cYm2IMpLnU1x1MDAxN0GoYydvUmaFs1x1MDAxMMurOlx1MDAxMEdcdTAwMTLDkPIxg0LuZ7REYYBwXGJBMapcdTAwMDU2M010SfHQ2HnFdJ6lXHUwMDAxqyAyfrUqRHWv2qMx2aEzxvoqZGeHxlxuwFaCSsblXGZcdTAwMDOJW/os+qT3z/eGTdlvXHUwMDFmeTJqbp6tOq5BMocwzKE0SClcdTAwMDXPZ1x1MDAwZWCsLtVSXHUwMDAyaFBMXHUwMDE2J0gvXHUwMDEy11x1MDAxYWWVlkJJSqTWZSVcdLA7XHUwMDFjoFxmVIKJZMnVXHUwMDA0rDm26rmUwFNRndryL6rHXHUwMDE3VPapPVx1MDAxYVx1MDAxM905I6irS1x1MDAwMjWLXGYwvbKlRlx1MDAwZdOXXHUwMDA0ro/8+y3SNedcdTAwMWa67Y7vn53e7u6t/ChcdTAwMDFcdTAwMDNwXGaVRktjXGLDfLqwdYnmXHUwMDBlxUxTKWqzTbnEcVx1MDAwMlx1MDAwNKVDsEeYpkTR7Gq8zCCf5lx1MDAwMrBdc66YoXRiXHI7xYTDUFwi5plTuFxu0C4vNqKjUtSPJNlMXHUwMDA3aHZcdTAwMDZNWmy0U42JXHUwMDExXHUwMDA0OFx1MDAxM7Sy2rjt9ptnPGTn9+JcdTAwMGJcdTAwMWKqo2PYV41ftS7QqPSpUeuEOy2MWZioLFx1MDAwZVx1MDAxOCWTXYCmXHUwMDE3XGaXYuvd61x1MDAxYlxugygkV1+uXHUwMDA2zd1ccrq/6sRiXHUwMDE3XHUwMDAxK1wiXHUwMDExiShcdTAwMDeULTlcdTAwMTaIhTmMMrthXHUwMDEyXHUwMDAzu6JrebzC7OQ7u9hYKjvJSZcsXG62w1hcdTAwMTQoXHUwMDA2ICGl0lxcZ1x1MDAwMsN3ZtHG2PDzI1KBJTFcdTAwMGJxiCDSXHUwMDBls1x1MDAwMoKEI7WUzDGiXHUwMDBlZVxcM0zXXGJcdTAwMDVM6SqXM/yjmKXaqezRKPGnXHUwMDE5qaWq6qhqJFx1MDAwYrpcdTAwMGJcdTAwMTUzTEcq77FcdTAwMTXnXHUwMDE1JoQjNJN25Vx1MDAwMFxiY1xuq7ZAXHUwMDFhh2qFnopdw1x1MDAxOC9cdTAwMWG2wKJj+uC6oqO09Icq65lcdTAwMTc51IvRnKvNVHWsj0W19/3u98sgu7mqjiPnzTjAjyo6jlxmmU9qQMbFi8NcdTAwMTBUXHUwMDE4JG0mp6851m+atLKTnFxyKjmuXHTlRmCQyqcwXG6sXHUwMDEwwVx1MDAxNIeB3dOnZoJcIlx1MDAwN+7Ck2pcdTAwMTNcdTAwMDSNtSNRo9XYwpTtP8JcdTAwMWMjXHUwMDA0hlx1MDAwNTtcdTAwMDVcdTAwMGWN5ZM5XGbmWUJcdTAwMTIxzz5cXKugNGZbPGGXeVKN6krb7TUwnGaS/1x1MDAwN1xyYreLsVwiXVx1MDAwMrZcdTAwMTKAjIovSJDSrVxuJiTIzyQ0XHUwMDFhNU6VtE/604xKozqJ0ZVFT8G5xD9meq1Rv9nNXG5cdTAwMTNcdTAwMGKVnNi1lPhfcVxupeCONFx1MDAwNLVcYv4k6qZQPp1YNCQrXHUwMDAxJNGWXHUwMDE3oIRX7PIwRmzJSlx1MDAxOM3snKtcIq/gWck5Uz9gifiySiPUQaEnQWNcdTAwMTKjqZJUlVRGhGPXXHUwMDFlUVBcYlx1MDAxZCFFblx1MDAxZEWOPco325pgj18jgWlU+5Q9Jr1pRlap3dzFsMokxqC254ZPTyyt4y+dm6NBcDxodT9s0r1cdTAwMGZfL5tVeyyt2NYu0tFcbr8tui9nupDH2G1yXHI6LbW+bZipnjzx9K1dqMOBVi2sYII5ljUsXHUwMDAzlk/VtjsoamaL88+b4vy8W7zMQoe/fb9xctN1dzB4XHUwMDFm4y3HhIjv2m8/pD7pbda/+N7tZsmGyVx1MDAxN8lhTU5egsWHZ9/0399++/Z/3Vx1MDAxNVx1MDAwZVx1MDAwMiJ9 MarginPaddingContent areaBorderHeightWidth

The following example creates two widgets with a width of 30, a height of 6, and a border and padding of 1. The first widget has the default box_sizing ("border-box"). The second widget sets box_sizing to "content-box".

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


TEXT = """I must not fear.
Fear is the mind-killer.
Fear is the little-death that brings total obliteration.
I will face my fear.
I will permit it to pass over me and through me.
And when it has gone past, I will turn the inner eye to see its path.
Where the fear has gone there will be nothing. Only I will remain."""


class BoxSizing(App):
    def compose(self) -> ComposeResult:
        self.widget1 = Static(TEXT)
        yield self.widget1
        self.widget2 = Static(TEXT)
        yield self.widget2

    def on_mount(self) -> None:
        self.widget1.styles.background = "purple"
        self.widget2.styles.background = "darkgreen"
        self.widget1.styles.width = 30
        self.widget2.styles.width = 30
        self.widget1.styles.height = 6
        self.widget2.styles.height = 6
        self.widget1.styles.border = ("heavy", "white")
        self.widget2.styles.border = ("heavy", "white")
        self.widget1.styles.padding = 1
        self.widget2.styles.padding = 1
        self.widget2.styles.box_sizing = "content-box"


if __name__ == "__main__":
    app = BoxSizing()
    app.run()

The padding and border of the first widget is subtracted from the height leaving only 2 lines in the content area. The second widget also has a height of 6, but the padding and border adds additional height so that the content area remains 6 lines.

BoxSizing ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ I must not fear. Fear is the mind-killer. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ I must not fear. Fear is the mind-killer. Fear is the little-death that  brings total obliteration. I will face my fear. I will permit it to pass over  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Margin

Margin is similar to padding in that it adds space, but unlike padding, margin is outside of the widget's border. It is used to add space between widgets.

The following example creates two widgets, each with a margin of 2.

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


TEXT = """I must not fear.
Fear is the mind-killer.
Fear is the little-death that brings total obliteration.
I will face my fear.
I will permit it to pass over me and through me.
And when it has gone past, I will turn the inner eye to see its path.
Where the fear has gone there will be nothing. Only I will remain."""


class MarginApp(App):
    def compose(self) -> ComposeResult:
        self.widget1 = Static(TEXT)
        yield self.widget1
        self.widget2 = Static(TEXT)
        yield self.widget2

    def on_mount(self) -> None:
        self.widget1.styles.background = "purple"
        self.widget2.styles.background = "darkgreen"
        self.widget1.styles.border = ("heavy", "white")
        self.widget2.styles.border = ("heavy", "white")
        self.widget1.styles.margin = 2
        self.widget2.styles.margin = 2


if __name__ == "__main__":
    app = MarginApp()
    app.run()

Notice how each widget has an additional two rows and columns around the border.

MarginApp ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ I must not fear. Fear is the mind-killer. Fear is the little-death that brings total obliteration. I will face my fear. I will permit it to pass over me and through me. And when it has gone past, I will turn the inner eye to see its path. Where the fear has gone there will be nothing. Only I will remain. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ I must not fear. Fear is the mind-killer. Fear is the little-death that brings total obliteration. I will face my fear. I will permit it to pass over me and through me. And when it has gone past, I will turn the inner eye to see its path. Where the fear has gone there will be nothing. Only I will remain. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Note

In the above example both widgets have a margin of 2, but there are only 2 lines of space between the widgets. This is because margins of consecutive widgets overlap. In other words when there are two widgets next to each other Textual picks the greater of the two margins.

More styles

We've covered the most fundamental styles used by Textual apps, but there are many more which you can use to customize many aspects of how your app looks. See the Styles reference for a comprehensive list.

In the next chapter we will discuss Textual CSS which is a powerful way of applying styles to widgets that keeps your code free of style attributes.