diff --git a/404.html b/404.html index 7ac56e2147..6d0e3bad94 100644 --- a/404.html +++ b/404.html @@ -244,7 +244,7 @@ - Welcome + Home @@ -502,7 +502,7 @@ - Welcome + Home diff --git a/FAQ/index.html b/FAQ/index.html index 9744e68e2e..05c899d487 100644 --- a/FAQ/index.html +++ b/FAQ/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -517,7 +517,7 @@ - Welcome + Home diff --git a/api/app/index.html b/api/app/index.html index c264d195a0..ac63e8c57a 100644 --- a/api/app/index.html +++ b/api/app/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -9706,8 +9706,8 @@

- class-attribute instance-attribute + class-attribute

@@ -9732,8 +9732,8 @@

- class-attribute instance-attribute + class-attribute

@@ -9825,8 +9825,8 @@

- class-attribute instance-attribute + class-attribute

@@ -10279,8 +10279,8 @@

- class-attribute instance-attribute + class-attribute

@@ -10311,8 +10311,8 @@

- class-attribute instance-attribute + class-attribute

diff --git a/api/await_remove/index.html b/api/await_remove/index.html index 4eec5c4a44..4666784faa 100644 --- a/api/await_remove/index.html +++ b/api/await_remove/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/api/binding/index.html b/api/binding/index.html index 0986804b9a..832e9f0155 100644 --- a/api/binding/index.html +++ b/api/binding/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -7311,8 +7311,8 @@

- class-attribute instance-attribute + class-attribute

@@ -7358,8 +7358,8 @@

- class-attribute instance-attribute + class-attribute

@@ -7382,8 +7382,8 @@

- class-attribute instance-attribute + class-attribute

@@ -7406,8 +7406,8 @@

- class-attribute instance-attribute + class-attribute

diff --git a/api/color/index.html b/api/color/index.html index 7cc12f951f..a306782ee5 100644 --- a/api/color/index.html +++ b/api/color/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -8962,8 +8962,8 @@

- class-attribute instance-attribute + class-attribute

diff --git a/api/command/index.html b/api/command/index.html index a386f90c20..bc3dc6e23d 100644 --- a/api/command/index.html +++ b/api/command/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -8492,8 +8492,8 @@

- class-attribute instance-attribute + class-attribute

@@ -8563,8 +8563,8 @@

- class-attribute instance-attribute + class-attribute

@@ -9146,8 +9146,8 @@

search - abstractmethod async + abstractmethod

@@ -9304,8 +9304,8 @@

- class-attribute instance-attribute + class-attribute

diff --git a/api/containers/index.html b/api/containers/index.html index bdba9f744a..115d4bf0bc 100644 --- a/api/containers/index.html +++ b/api/containers/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/api/content_switcher/index.html b/api/content_switcher/index.html index 009298aec2..17932932a7 100644 --- a/api/content_switcher/index.html +++ b/api/content_switcher/index.html @@ -251,7 +251,7 @@ - Welcome + Home @@ -509,7 +509,7 @@ - Welcome + Home @@ -7109,8 +7109,8 @@

- class-attribute instance-attribute + class-attribute

diff --git a/api/coordinate/index.html b/api/coordinate/index.html index e48153a9f6..5214f25f9c 100644 --- a/api/coordinate/index.html +++ b/api/coordinate/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/api/dom_node/index.html b/api/dom_node/index.html index d677376b28..5c3823add0 100644 --- a/api/dom_node/index.html +++ b/api/dom_node/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -8319,8 +8319,8 @@

- property writable + property

@@ -8425,8 +8425,8 @@

- class-attribute instance-attribute + class-attribute

@@ -8642,8 +8642,8 @@

- property writable + property

@@ -8713,8 +8713,8 @@

- property writable + property

@@ -9020,8 +9020,8 @@

- property writable + property

diff --git a/api/errors/index.html b/api/errors/index.html index 048fcdc2f2..1dd9a12a31 100644 --- a/api/errors/index.html +++ b/api/errors/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/api/events/index.html b/api/events/index.html index ea65719340..14eac764e2 100644 --- a/api/events/index.html +++ b/api/events/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -9265,8 +9265,8 @@

- property writable + property

diff --git a/api/filter/index.html b/api/filter/index.html index 8eb69554c5..bfd5a63106 100644 --- a/api/filter/index.html +++ b/api/filter/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/api/fuzzy_matcher/index.html b/api/fuzzy_matcher/index.html index e606d5a63d..357c985425 100644 --- a/api/fuzzy_matcher/index.html +++ b/api/fuzzy_matcher/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/api/geometry/index.html b/api/geometry/index.html index 37fa00e591..576eed9fac 100644 --- a/api/geometry/index.html +++ b/api/geometry/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -9002,8 +9002,8 @@

- class-attribute instance-attribute + class-attribute

@@ -9026,8 +9026,8 @@

- class-attribute instance-attribute + class-attribute

@@ -9505,8 +9505,8 @@

- class-attribute instance-attribute + class-attribute

@@ -9745,8 +9745,8 @@

- class-attribute instance-attribute + class-attribute

@@ -9769,8 +9769,8 @@

- class-attribute instance-attribute + class-attribute

@@ -9793,8 +9793,8 @@

- class-attribute instance-attribute + class-attribute

@@ -11584,8 +11584,8 @@

- class-attribute instance-attribute + class-attribute

@@ -11654,8 +11654,8 @@

- class-attribute instance-attribute + class-attribute

@@ -11888,8 +11888,8 @@

- class-attribute instance-attribute + class-attribute

@@ -11982,8 +11982,8 @@

- class-attribute instance-attribute + class-attribute

@@ -12006,8 +12006,8 @@

- class-attribute instance-attribute + class-attribute

@@ -12030,8 +12030,8 @@

- class-attribute instance-attribute + class-attribute

diff --git a/api/index.html b/api/index.html index 613ca18490..dfa36f5304 100644 --- a/api/index.html +++ b/api/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/api/logger/index.html b/api/logger/index.html index b4d4b53103..67981fbeed 100644 --- a/api/logger/index.html +++ b/api/logger/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/api/logging/index.html b/api/logging/index.html index bedcc01936..9855669de8 100644 --- a/api/logging/index.html +++ b/api/logging/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/api/map_geometry/index.html b/api/map_geometry/index.html index c546dcf3bf..ed7ed83726 100644 --- a/api/map_geometry/index.html +++ b/api/map_geometry/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/api/message/index.html b/api/message/index.html index f435df9ee0..be25d8b574 100644 --- a/api/message/index.html +++ b/api/message/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/api/message_pump/index.html b/api/message_pump/index.html index 249e203ad9..f49423f722 100644 --- a/api/message_pump/index.html +++ b/api/message_pump/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/api/on/index.html b/api/on/index.html index b3d444013c..2324557522 100644 --- a/api/on/index.html +++ b/api/on/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/api/pilot/index.html b/api/pilot/index.html index 92fe82213e..538690ed7c 100644 --- a/api/pilot/index.html +++ b/api/pilot/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/api/query/index.html b/api/query/index.html index 978f18df6e..4b3af5ffe9 100644 --- a/api/query/index.html +++ b/api/query/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/api/reactive/index.html b/api/reactive/index.html index 973d4becc2..0132ce3d9d 100644 --- a/api/reactive/index.html +++ b/api/reactive/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/api/screen/index.html b/api/screen/index.html index 2da5aab226..6ca2f05461 100644 --- a/api/screen/index.html +++ b/api/screen/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -8279,8 +8279,8 @@

- class-attribute instance-attribute + class-attribute

@@ -8390,8 +8390,8 @@

- class-attribute instance-attribute + class-attribute

@@ -8414,8 +8414,8 @@

- class-attribute instance-attribute + class-attribute

@@ -8438,8 +8438,8 @@

- class-attribute instance-attribute + class-attribute

diff --git a/api/scroll_view/index.html b/api/scroll_view/index.html index d2413904af..ff020bc6c6 100644 --- a/api/scroll_view/index.html +++ b/api/scroll_view/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/api/scrollbar/index.html b/api/scrollbar/index.html index 5a80ba5b78..45be69d99c 100644 --- a/api/scrollbar/index.html +++ b/api/scrollbar/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/api/strip/index.html b/api/strip/index.html index 506ce60259..a62a3c2b2e 100644 --- a/api/strip/index.html +++ b/api/strip/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/api/suggester/index.html b/api/suggester/index.html index faf57ed9a6..e90e714837 100644 --- a/api/suggester/index.html +++ b/api/suggester/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -7497,8 +7497,8 @@

get_suggestion - abstractmethod async + abstractmethod

diff --git a/api/system_commands_source/index.html b/api/system_commands_source/index.html index fffe9a9112..b42f92e94e 100644 --- a/api/system_commands_source/index.html +++ b/api/system_commands_source/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/api/timer/index.html b/api/timer/index.html index 89884b10e7..866c858b13 100644 --- a/api/timer/index.html +++ b/api/timer/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/api/types/index.html b/api/types/index.html index d35677c72b..912a950a1e 100644 --- a/api/types/index.html +++ b/api/types/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/api/validation/index.html b/api/validation/index.html index 116df859d4..61d6f82635 100644 --- a/api/validation/index.html +++ b/api/validation/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -8241,8 +8241,8 @@

- class-attribute instance-attribute + class-attribute

@@ -8288,8 +8288,8 @@

- class-attribute instance-attribute + class-attribute

@@ -9840,8 +9840,8 @@

- class-attribute instance-attribute + class-attribute

diff --git a/api/walk/index.html b/api/walk/index.html index 38ef250f2e..2109cc1f7a 100644 --- a/api/walk/index.html +++ b/api/walk/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/api/widget/index.html b/api/widget/index.html index 411a512084..b6d5929938 100644 --- a/api/widget/index.html +++ b/api/widget/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -10151,8 +10151,8 @@ @@ -10175,8 +10175,8 @@

- class-attribute instance-attribute + class-attribute

@@ -10199,8 +10199,8 @@

- class-attribute instance-attribute + class-attribute

@@ -10223,8 +10223,8 @@

- class-attribute instance-attribute + class-attribute

@@ -10247,8 +10247,8 @@

- class-attribute instance-attribute + class-attribute

@@ -10476,8 +10476,8 @@

- class-attribute instance-attribute + class-attribute

@@ -10541,8 +10541,8 @@

- class-attribute instance-attribute + class-attribute

@@ -10629,8 +10629,8 @@

- class-attribute instance-attribute + class-attribute

@@ -10653,8 +10653,8 @@ @@ -10722,8 +10722,8 @@

- class-attribute instance-attribute + class-attribute

@@ -11096,8 +11096,8 @@

- class-attribute instance-attribute + class-attribute

@@ -11120,8 +11120,8 @@

- property writable + property

@@ -11332,8 +11332,8 @@

- class-attribute instance-attribute + class-attribute

@@ -11358,8 +11358,8 @@

- class-attribute instance-attribute + class-attribute

@@ -11657,8 +11657,8 @@

- class-attribute instance-attribute + class-attribute

@@ -11683,8 +11683,8 @@

- class-attribute instance-attribute + class-attribute

@@ -11709,8 +11709,8 @@

- class-attribute instance-attribute + class-attribute

@@ -11815,8 +11815,8 @@

- property writable + property

@@ -11968,8 +11968,8 @@

- class-attribute instance-attribute + class-attribute

diff --git a/api/work/index.html b/api/work/index.html index 775753bdef..d19c82bcfe 100644 --- a/api/work/index.html +++ b/api/work/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/api/worker/index.html b/api/worker/index.html index fd0b15e4ba..8d75b56831 100644 --- a/api/worker/index.html +++ b/api/worker/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -8157,8 +8157,8 @@

- property writable + property

@@ -8673,8 +8673,8 @@

- class-attribute instance-attribute + class-attribute

@@ -8697,8 +8697,8 @@

- class-attribute instance-attribute + class-attribute

@@ -8721,8 +8721,8 @@

- class-attribute instance-attribute + class-attribute

@@ -8745,8 +8745,8 @@

- class-attribute instance-attribute + class-attribute

@@ -8769,8 +8769,8 @@

- class-attribute instance-attribute + class-attribute

diff --git a/api/worker_manager/index.html b/api/worker_manager/index.html index 984be1c2c9..15fff68727 100644 --- a/api/worker_manager/index.html +++ b/api/worker_manager/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/blog/2022/11/06/new-blog/index.html b/blog/2022/11/06/new-blog/index.html index 08bd0d45c0..9b8003a9ce 100644 --- a/blog/2022/11/06/new-blog/index.html +++ b/blog/2022/11/06/new-blog/index.html @@ -253,7 +253,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/blog/2022/11/08/version-040/index.html b/blog/2022/11/08/version-040/index.html index adbc501b14..37a07cda9d 100644 --- a/blog/2022/11/08/version-040/index.html +++ b/blog/2022/11/08/version-040/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -517,7 +517,7 @@ - Welcome + Home diff --git a/blog/2022/11/20/stealing-open-source-code-from-textual/index.html b/blog/2022/11/20/stealing-open-source-code-from-textual/index.html index 7b9b16d585..1d020481be 100644 --- a/blog/2022/11/20/stealing-open-source-code-from-textual/index.html +++ b/blog/2022/11/20/stealing-open-source-code-from-textual/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -517,7 +517,7 @@ - Welcome + Home diff --git a/blog/2022/11/22/what-i-learned-from-my-first-non-trivial-pr/index.html b/blog/2022/11/22/what-i-learned-from-my-first-non-trivial-pr/index.html index 5cf449d768..18969e2a98 100644 --- a/blog/2022/11/22/what-i-learned-from-my-first-non-trivial-pr/index.html +++ b/blog/2022/11/22/what-i-learned-from-my-first-non-trivial-pr/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -517,7 +517,7 @@ - Welcome + Home diff --git a/blog/2022/11/24/spinners-and-progress-bars-in-textual/index.html b/blog/2022/11/24/spinners-and-progress-bars-in-textual/index.html index 5587c7b8b3..062fedf055 100644 --- a/blog/2022/11/24/spinners-and-progress-bars-in-textual/index.html +++ b/blog/2022/11/24/spinners-and-progress-bars-in-textual/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -517,7 +517,7 @@ - Welcome + Home diff --git a/blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/index.html b/blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/index.html index 8d5330fdbd..7040094075 100644 --- a/blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/index.html +++ b/blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -517,7 +517,7 @@ - Welcome + Home diff --git a/blog/2022/12/07/letting-your-cook-multitask-while-bringing-water-to-a-boil/index.html b/blog/2022/12/07/letting-your-cook-multitask-while-bringing-water-to-a-boil/index.html index 932718a6ff..7a1d73d5ea 100644 --- a/blog/2022/12/07/letting-your-cook-multitask-while-bringing-water-to-a-boil/index.html +++ b/blog/2022/12/07/letting-your-cook-multitask-while-bringing-water-to-a-boil/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -517,7 +517,7 @@ - Welcome + Home diff --git a/blog/2022/12/08/be-the-keymaster/index.html b/blog/2022/12/08/be-the-keymaster/index.html index 8279ada61a..1d44af7aba 100644 --- a/blog/2022/12/08/be-the-keymaster/index.html +++ b/blog/2022/12/08/be-the-keymaster/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -517,7 +517,7 @@ - Welcome + Home diff --git a/blog/2022/12/11/version-060/index.html b/blog/2022/12/11/version-060/index.html index 79da6273fa..b5b61b946e 100644 --- a/blog/2022/12/11/version-060/index.html +++ b/blog/2022/12/11/version-060/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -517,7 +517,7 @@ - Welcome + Home diff --git a/blog/2022/12/20/a-year-of-building-for-the-terminal/index.html b/blog/2022/12/20/a-year-of-building-for-the-terminal/index.html index cf6808975b..0e60ef80a2 100644 --- a/blog/2022/12/20/a-year-of-building-for-the-terminal/index.html +++ b/blog/2022/12/20/a-year-of-building-for-the-terminal/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -517,7 +517,7 @@ - Welcome + Home diff --git a/blog/2022/12/30/a-better-asyncio-sleep-for-windows-to-fix-animation/index.html b/blog/2022/12/30/a-better-asyncio-sleep-for-windows-to-fix-animation/index.html index c73a5c5ffd..5e5f03ca3e 100644 --- a/blog/2022/12/30/a-better-asyncio-sleep-for-windows-to-fix-animation/index.html +++ b/blog/2022/12/30/a-better-asyncio-sleep-for-windows-to-fix-animation/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -517,7 +517,7 @@ - Welcome + Home diff --git a/blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/index.html b/blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/index.html index be169f26ed..2bbcc29e92 100644 --- a/blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/index.html +++ b/blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -517,7 +517,7 @@ - Welcome + Home diff --git a/blog/2023/02/11/the-heisenbug-lurking-in-your-async-code/index.html b/blog/2023/02/11/the-heisenbug-lurking-in-your-async-code/index.html index 9eb0653266..f1cb63e1a6 100644 --- a/blog/2023/02/11/the-heisenbug-lurking-in-your-async-code/index.html +++ b/blog/2023/02/11/the-heisenbug-lurking-in-your-async-code/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -517,7 +517,7 @@ - Welcome + Home diff --git a/blog/2023/02/15/textual-0110-adds-a-beautiful-markdown-widget/index.html b/blog/2023/02/15/textual-0110-adds-a-beautiful-markdown-widget/index.html index 5421d5da37..4d84ff85d2 100644 --- a/blog/2023/02/15/textual-0110-adds-a-beautiful-markdown-widget/index.html +++ b/blog/2023/02/15/textual-0110-adds-a-beautiful-markdown-widget/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -517,7 +517,7 @@ - Welcome + Home diff --git a/blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/index.html b/blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/index.html index a97d05be40..12a33a1793 100644 --- a/blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/index.html +++ b/blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -517,7 +517,7 @@ - Welcome + Home diff --git a/blog/2023/03/08/overhead-of-python-asyncio-tasks/index.html b/blog/2023/03/08/overhead-of-python-asyncio-tasks/index.html index 113624955e..013235a3e5 100644 --- a/blog/2023/03/08/overhead-of-python-asyncio-tasks/index.html +++ b/blog/2023/03/08/overhead-of-python-asyncio-tasks/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -517,7 +517,7 @@ - Welcome + Home diff --git a/blog/2023/03/09/textual-0140-shakes-up-posting-messages/index.html b/blog/2023/03/09/textual-0140-shakes-up-posting-messages/index.html index e60b292b96..453cf28f4f 100644 --- a/blog/2023/03/09/textual-0140-shakes-up-posting-messages/index.html +++ b/blog/2023/03/09/textual-0140-shakes-up-posting-messages/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -517,7 +517,7 @@ - Welcome + Home diff --git a/blog/2023/03/13/textual-0150-adds-a-tabs-widget/index.html b/blog/2023/03/13/textual-0150-adds-a-tabs-widget/index.html index 18f5c1b8ac..742e46b43f 100644 --- a/blog/2023/03/13/textual-0150-adds-a-tabs-widget/index.html +++ b/blog/2023/03/13/textual-0150-adds-a-tabs-widget/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -517,7 +517,7 @@ - Welcome + Home diff --git a/blog/2023/03/15/no-async-async-with-python/index.html b/blog/2023/03/15/no-async-async-with-python/index.html index 3261e701e2..a53488b0b9 100644 --- a/blog/2023/03/15/no-async-async-with-python/index.html +++ b/blog/2023/03/15/no-async-async-with-python/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -517,7 +517,7 @@ - Welcome + Home diff --git a/blog/2023/03/22/textual-0160-adds-tabbedcontent-and-border-titles/index.html b/blog/2023/03/22/textual-0160-adds-tabbedcontent-and-border-titles/index.html index af43a8c8e4..1ef8ee2977 100644 --- a/blog/2023/03/22/textual-0160-adds-tabbedcontent-and-border-titles/index.html +++ b/blog/2023/03/22/textual-0160-adds-tabbedcontent-and-border-titles/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -517,7 +517,7 @@ - Welcome + Home diff --git a/blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/index.html b/blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/index.html index 6f52497716..7815cb5a19 100644 --- a/blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/index.html +++ b/blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -517,7 +517,7 @@ - Welcome + Home diff --git a/blog/2023/04/04/textual-0180-adds-api-for-managing-concurrent-workers/index.html b/blog/2023/04/04/textual-0180-adds-api-for-managing-concurrent-workers/index.html index e673232758..73ec75338c 100644 --- a/blog/2023/04/04/textual-0180-adds-api-for-managing-concurrent-workers/index.html +++ b/blog/2023/04/04/textual-0180-adds-api-for-managing-concurrent-workers/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -517,7 +517,7 @@ - Welcome + Home diff --git a/blog/2023/05/03/textual-0230-improves-message-handling/index.html b/blog/2023/05/03/textual-0230-improves-message-handling/index.html index 0dd272821f..6c677c95cd 100644 --- a/blog/2023/05/03/textual-0230-improves-message-handling/index.html +++ b/blog/2023/05/03/textual-0230-improves-message-handling/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -517,7 +517,7 @@ - Welcome + Home diff --git a/blog/2023/05/08/textual-0240-adds-a-select-control/index.html b/blog/2023/05/08/textual-0240-adds-a-select-control/index.html index 456d104628..a1667444d8 100644 --- a/blog/2023/05/08/textual-0240-adds-a-select-control/index.html +++ b/blog/2023/05/08/textual-0240-adds-a-select-control/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -517,7 +517,7 @@ - Welcome + Home diff --git a/blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/index.html b/blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/index.html index ee5d0e056f..539602e158 100644 --- a/blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/index.html +++ b/blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -517,7 +517,7 @@ - Welcome + Home diff --git a/blog/2023/06/06/to-tui-or-not-to-tui/index.html b/blog/2023/06/06/to-tui-or-not-to-tui/index.html index adb1a8d21c..33aeffc853 100644 --- a/blog/2023/06/06/to-tui-or-not-to-tui/index.html +++ b/blog/2023/06/06/to-tui-or-not-to-tui/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -517,7 +517,7 @@ - Welcome + Home diff --git a/blog/2023/07/03/textual-0290-refactors-dev-tools/index.html b/blog/2023/07/03/textual-0290-refactors-dev-tools/index.html index 36625b7e59..72a31e4b7c 100644 --- a/blog/2023/07/03/textual-0290-refactors-dev-tools/index.html +++ b/blog/2023/07/03/textual-0290-refactors-dev-tools/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -517,7 +517,7 @@ - Welcome + Home diff --git a/blog/2023/07/17/textual-0300-adds-desktop-style-notifications/index.html b/blog/2023/07/17/textual-0300-adds-desktop-style-notifications/index.html index 2a89698f05..929e17686f 100644 --- a/blog/2023/07/17/textual-0300-adds-desktop-style-notifications/index.html +++ b/blog/2023/07/17/textual-0300-adds-desktop-style-notifications/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -517,7 +517,7 @@ - Welcome + Home diff --git a/blog/2023/07/27/using-rich-inspect-to-interrogate-python-objects/index.html b/blog/2023/07/27/using-rich-inspect-to-interrogate-python-objects/index.html index a05f56e183..b37b84f32b 100644 --- a/blog/2023/07/27/using-rich-inspect-to-interrogate-python-objects/index.html +++ b/blog/2023/07/27/using-rich-inspect-to-interrogate-python-objects/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -517,7 +517,7 @@ - Welcome + Home diff --git a/blog/2023/07/29/pull-requests-are-cake-or-puppies/index.html b/blog/2023/07/29/pull-requests-are-cake-or-puppies/index.html index 21444cd7d4..2d6b8aa5aa 100644 --- a/blog/2023/07/29/pull-requests-are-cake-or-puppies/index.html +++ b/blog/2023/07/29/pull-requests-are-cake-or-puppies/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -517,7 +517,7 @@ - Welcome + Home diff --git a/blog/2023/09/06/what-is-textual-web/index.html b/blog/2023/09/06/what-is-textual-web/index.html index a77b8fc7ec..4345a98dee 100644 --- a/blog/2023/09/06/what-is-textual-web/index.html +++ b/blog/2023/09/06/what-is-textual-web/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -517,7 +517,7 @@ - Welcome + Home diff --git a/blog/2023/09/15/textual-0370-adds-a-command-palette/index.html b/blog/2023/09/15/textual-0370-adds-a-command-palette/index.html index c302e38ed3..dba3399942 100644 --- a/blog/2023/09/15/textual-0370-adds-a-command-palette/index.html +++ b/blog/2023/09/15/textual-0370-adds-a-command-palette/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -517,7 +517,7 @@ - Welcome + Home diff --git a/blog/2023/09/18/things-i-learned-while-building-textuals-textarea/index.html b/blog/2023/09/18/things-i-learned-while-building-textuals-textarea/index.html index 54c7b982fd..e867aee9bf 100644 --- a/blog/2023/09/18/things-i-learned-while-building-textuals-textarea/index.html +++ b/blog/2023/09/18/things-i-learned-while-building-textuals-textarea/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -517,7 +517,7 @@ - Welcome + Home diff --git a/blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/index.html b/blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/index.html index 60ca41d591..2e7e2de77b 100644 --- a/blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/index.html +++ b/blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/index.html @@ -253,7 +253,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/blog/archive/2022/index.html b/blog/archive/2022/index.html index 9aac4f074f..ef71e3aa1f 100644 --- a/blog/archive/2022/index.html +++ b/blog/archive/2022/index.html @@ -253,7 +253,7 @@ - Welcome + Home @@ -513,7 +513,7 @@ - Welcome + Home diff --git a/blog/archive/2022/page/2/index.html b/blog/archive/2022/page/2/index.html index 42e515c536..d7da546285 100644 --- a/blog/archive/2022/page/2/index.html +++ b/blog/archive/2022/page/2/index.html @@ -253,7 +253,7 @@ - Welcome + Home @@ -513,7 +513,7 @@ - Welcome + Home diff --git a/blog/archive/2023/index.html b/blog/archive/2023/index.html index bf6e18e442..7c55c32ed1 100644 --- a/blog/archive/2023/index.html +++ b/blog/archive/2023/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/blog/archive/2023/page/2/index.html b/blog/archive/2023/page/2/index.html index 25e76bc466..3bf4e00ab2 100644 --- a/blog/archive/2023/page/2/index.html +++ b/blog/archive/2023/page/2/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/blog/archive/2023/page/3/index.html b/blog/archive/2023/page/3/index.html index 436cd84fc1..9c6de9815f 100644 --- a/blog/archive/2023/page/3/index.html +++ b/blog/archive/2023/page/3/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/blog/category/devlog/index.html b/blog/category/devlog/index.html index ad717e33c4..f0416fc094 100644 --- a/blog/category/devlog/index.html +++ b/blog/category/devlog/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/blog/category/devlog/page/2/index.html b/blog/category/devlog/page/2/index.html index f5a599c96a..c2e6d2eaf0 100644 --- a/blog/category/devlog/page/2/index.html +++ b/blog/category/devlog/page/2/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/blog/category/news/index.html b/blog/category/news/index.html index 3ec8e2eef3..3890605a3d 100644 --- a/blog/category/news/index.html +++ b/blog/category/news/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/blog/category/release/index.html b/blog/category/release/index.html index e573690e9e..7a2b1e52f5 100644 --- a/blog/category/release/index.html +++ b/blog/category/release/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/blog/category/release/page/2/index.html b/blog/category/release/page/2/index.html index 29efb005fe..8c28139e99 100644 --- a/blog/category/release/page/2/index.html +++ b/blog/category/release/page/2/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/blog/index.html b/blog/index.html index b6f53568aa..3191bcfbfb 100644 --- a/blog/index.html +++ b/blog/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/blog/page/2/index.html b/blog/page/2/index.html index 19019e1474..08482a3343 100644 --- a/blog/page/2/index.html +++ b/blog/page/2/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/blog/page/3/index.html b/blog/page/3/index.html index e874de3faf..481de8a0d3 100644 --- a/blog/page/3/index.html +++ b/blog/page/3/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/blog/page/4/index.html b/blog/page/4/index.html index 4336ecdf10..c15dcc1cd4 100644 --- a/blog/page/4/index.html +++ b/blog/page/4/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/css_types/border/index.html b/css_types/border/index.html index f698db77a9..ec96e383a6 100644 --- a/css_types/border/index.html +++ b/css_types/border/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/css_types/color/index.html b/css_types/color/index.html index c147ac3f83..3550da30f8 100644 --- a/css_types/color/index.html +++ b/css_types/color/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/css_types/horizontal/index.html b/css_types/horizontal/index.html index 74fdc7b897..f6e3c466de 100644 --- a/css_types/horizontal/index.html +++ b/css_types/horizontal/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/css_types/index.html b/css_types/index.html index 7b2ba924f1..f848415058 100644 --- a/css_types/index.html +++ b/css_types/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/css_types/integer/index.html b/css_types/integer/index.html index 146c984662..844736c284 100644 --- a/css_types/integer/index.html +++ b/css_types/integer/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/css_types/name/index.html b/css_types/name/index.html index e65fe68bae..67a1ec9945 100644 --- a/css_types/name/index.html +++ b/css_types/name/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/css_types/number/index.html b/css_types/number/index.html index 8c1bc04f18..9f731fa3b2 100644 --- a/css_types/number/index.html +++ b/css_types/number/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/css_types/overflow/index.html b/css_types/overflow/index.html index eb65d0917e..cfe18f04df 100644 --- a/css_types/overflow/index.html +++ b/css_types/overflow/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/css_types/percentage/index.html b/css_types/percentage/index.html index 38a8a6cfd4..24c5edf231 100644 --- a/css_types/percentage/index.html +++ b/css_types/percentage/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/css_types/scalar/index.html b/css_types/scalar/index.html index 420daec825..9abacceb48 100644 --- a/css_types/scalar/index.html +++ b/css_types/scalar/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/css_types/text_align/index.html b/css_types/text_align/index.html index b5b01d812d..01416ed263 100644 --- a/css_types/text_align/index.html +++ b/css_types/text_align/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/css_types/text_style/index.html b/css_types/text_style/index.html index a6033cb155..c25a80d66f 100644 --- a/css_types/text_style/index.html +++ b/css_types/text_style/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/css_types/vertical/index.html b/css_types/vertical/index.html index e5e2f505aa..8557dcbfb9 100644 --- a/css_types/vertical/index.html +++ b/css_types/vertical/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/events/blur/index.html b/events/blur/index.html index 50336dc00e..c23e241822 100644 --- a/events/blur/index.html +++ b/events/blur/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/events/click/index.html b/events/click/index.html index 7d37842c25..c6c25c5b61 100644 --- a/events/click/index.html +++ b/events/click/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/events/descendant_blur/index.html b/events/descendant_blur/index.html index 9f8bf81b94..2b572578ee 100644 --- a/events/descendant_blur/index.html +++ b/events/descendant_blur/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/events/descendant_focus/index.html b/events/descendant_focus/index.html index 2016784d6c..5e8364dd5f 100644 --- a/events/descendant_focus/index.html +++ b/events/descendant_focus/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/events/enter/index.html b/events/enter/index.html index f5730402be..c10a6eddf0 100644 --- a/events/enter/index.html +++ b/events/enter/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/events/focus/index.html b/events/focus/index.html index c41b46d206..4a54abec74 100644 --- a/events/focus/index.html +++ b/events/focus/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/events/hide/index.html b/events/hide/index.html index 679911ce63..4692a20d82 100644 --- a/events/hide/index.html +++ b/events/hide/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/events/index.html b/events/index.html index ce6f40ea86..9ae11d0a58 100644 --- a/events/index.html +++ b/events/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/events/key/index.html b/events/key/index.html index 5c79bced89..1e3bfef00d 100644 --- a/events/key/index.html +++ b/events/key/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/events/leave/index.html b/events/leave/index.html index c5922526c1..9d4e5cf7de 100644 --- a/events/leave/index.html +++ b/events/leave/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/events/load/index.html b/events/load/index.html index cd4ed36635..94d7d452fc 100644 --- a/events/load/index.html +++ b/events/load/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/events/mount/index.html b/events/mount/index.html index b9e8506930..4dde352891 100644 --- a/events/mount/index.html +++ b/events/mount/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/events/mouse_capture/index.html b/events/mouse_capture/index.html index c3108d25cc..3fc4dbf190 100644 --- a/events/mouse_capture/index.html +++ b/events/mouse_capture/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/events/mouse_down/index.html b/events/mouse_down/index.html index dbe0b4f0ef..1ef7ae8dd6 100644 --- a/events/mouse_down/index.html +++ b/events/mouse_down/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/events/mouse_move/index.html b/events/mouse_move/index.html index 85a0069e17..028760beff 100644 --- a/events/mouse_move/index.html +++ b/events/mouse_move/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/events/mouse_release/index.html b/events/mouse_release/index.html index c160b48597..9538e2bdac 100644 --- a/events/mouse_release/index.html +++ b/events/mouse_release/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/events/mouse_scroll_down/index.html b/events/mouse_scroll_down/index.html index 561a982ac7..9ff80ebb39 100644 --- a/events/mouse_scroll_down/index.html +++ b/events/mouse_scroll_down/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/events/mouse_scroll_up/index.html b/events/mouse_scroll_up/index.html index ef69529970..a190f68626 100644 --- a/events/mouse_scroll_up/index.html +++ b/events/mouse_scroll_up/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/events/mouse_up/index.html b/events/mouse_up/index.html index cb791498af..e2fd6d698c 100644 --- a/events/mouse_up/index.html +++ b/events/mouse_up/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/events/paste/index.html b/events/paste/index.html index da969f8aa2..772fe81d4b 100644 --- a/events/paste/index.html +++ b/events/paste/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/events/resize/index.html b/events/resize/index.html index 49e9900107..3347b0c84d 100644 --- a/events/resize/index.html +++ b/events/resize/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/events/screen_resume/index.html b/events/screen_resume/index.html index 88c2d0223d..c7732b9d4d 100644 --- a/events/screen_resume/index.html +++ b/events/screen_resume/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/events/screen_suspend/index.html b/events/screen_suspend/index.html index 2f2e63b06f..9e82aaf051 100644 --- a/events/screen_suspend/index.html +++ b/events/screen_suspend/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/events/show/index.html b/events/show/index.html index c0d1b72ecf..abd0ea5079 100644 --- a/events/show/index.html +++ b/events/show/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/examples/styles/index.html b/examples/styles/index.html index bd19a068a6..4e3d9a98a3 100644 --- a/examples/styles/index.html +++ b/examples/styles/index.html @@ -246,7 +246,7 @@ - Welcome + Home @@ -504,7 +504,7 @@ - Welcome + Home diff --git a/feed_rss_created.xml b/feed_rss_created.xml index 55bf563245..9f5d73a03a 100644 --- a/feed_rss_created.xml +++ b/feed_rss_created.xml @@ -1 +1 @@ - Textualhttps://textual.textualize.io/https://github.com/textualize/textual/en Thu, 21 Sep 2023 12:27:53 -0000 Thu, 21 Sep 2023 12:27:53 -0000 1440 MkDocs RSS plugin - v1.5.0 Textual 0.38.0 adds a syntax aware TextArea willmcgugan Release <h1>Textual 0.38.0 adds a syntax aware TextArea</h1><p>This is the second big feature release this month after last week's <a href="./release0.37.0.md">command palette</a>.</p><p>&lt;!...</p>https://textual.textualize.io/blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/ Thu, 21 Sep 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/ Things I learned while building Textual's TextArea darrenburns DevLog <h1>Things I learned while building Textual's TextArea</h1><p><code>TextArea</code> is the latest widget to be added to Textual's [growing collection](https://textual.textualiz...</p>https://textual.textualize.io/blog/2023/09/18/things-i-learned-while-building-textuals-textarea/ Mon, 18 Sep 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/09/18/things-i-learned-while-building-textuals-textarea/ Textual 0.37.0 adds a command palette willmcgugan Release <h1>Textual 0.37.0 adds a command palette</h1><p>Textual version 0.37.0 has landed!The highlight of this release is the new command palette.</p><!-- more --><p>A comma...</p>https://textual.textualize.io/blog/2023/09/15/textual-0370-adds-a-command-palette/ Fri, 15 Sep 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/09/15/textual-0370-adds-a-command-palette/ What is Textual Web? willmcgugan News <h1>What is Textual Web?</h1><p>If you know us, you will know that we are the team behind <a href="https://github.com/Textualize/rich">Rich</a> and [Textual](https://github.com...</p>https://textual.textualize.io/blog/2023/09/06/what-is-textual-web/ Wed, 06 Sep 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/09/06/what-is-textual-web/ Pull Requests are cake or puppies willmcgugan DevLog <h1>Pull Requests are cake or puppies</h1><p>Broadly speaking, there are two types of contributions you can make to an Open Source project.</p><!-- more --><p>The first...</p>https://textual.textualize.io/blog/2023/07/29/pull-requests-are-cake-or-puppies/ Sat, 29 Jul 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/07/29/pull-requests-are-cake-or-puppies/ Using Rich Inspect to interrogate Python objects willmcgugan DevLog <h1>Using Rich Inspect to interrogate Python objects</h1><p>The <a href="https://github.com/Textualize/rich">Rich</a> library has a few functions that are admittedly a little o...</p>https://textual.textualize.io/blog/2023/07/27/using-rich-inspect-to-interrogate-python-objects/ Thu, 27 Jul 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/07/27/using-rich-inspect-to-interrogate-python-objects/ Textual 0.30.0 adds desktop-style notifications willmcgugan Release <h1>Textual 0.30.0 adds desktop-style notifications</h1><p>We have a new release of Textual to talk about, but before that I'd like to cover a little Textual news.</p><p>...</p>https://textual.textualize.io/blog/2023/07/17/textual-0300-adds-desktop-style-notifications/ Mon, 17 Jul 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/07/17/textual-0300-adds-desktop-style-notifications/ Textual 0.29.0 refactors dev tools willmcgugan Release <h1>Textual 0.29.0 refactors dev tools</h1><p>It's been a slow week or two at Textualize, with Textual devs taking well-earned annual leave, but we still managed to ...</p>https://textual.textualize.io/blog/2023/07/03/textual-0290-refactors-dev-tools/ Mon, 03 Jul 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/07/03/textual-0290-refactors-dev-tools/ To TUI or not to TUI willmcgugan DevLog <h1>To TUI or not to TUI</h1><p>Tech moves pretty fast.If you don’t stop and look around once in a while, you could miss it.And yet some technology feels like it h...</p>https://textual.textualize.io/blog/2023/06/06/to-tui-or-not-to-tui/ Tue, 06 Jun 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/06/06/to-tui-or-not-to-tui/ Textual adds Sparklines, Selection list, Input validation, and tool tips willmcgugan Release <h1>Textual adds Sparklines, Selection list, Input validation, and tool tips</h1><p>It's been 12 days since the last Textual release, which is longer than our usual ...</p>https://textual.textualize.io/blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/ Thu, 01 Jun 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/ Textual 0.24.0 adds a Select control willmcgugan Release <h1>Textual 0.24.0 adds a Select control</h1><p>Coming just 5 days after the last release, we have version 0.24.0 which we are crowning the King of Textual releases....</p>https://textual.textualize.io/blog/2023/05/08/textual-0240-adds-a-select-control/ Mon, 08 May 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/05/08/textual-0240-adds-a-select-control/ Textual 0.23.0 improves message handling willmcgugan Release <h1>Textual 0.23.0 improves message handling</h1><p>It's been a busy couple of weeks at Textualize.We've been building apps with [Textual](https://github.com/Textua...</p>https://textual.textualize.io/blog/2023/05/03/textual-0230-improves-message-handling/ Wed, 03 May 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/05/03/textual-0230-improves-message-handling/ Textual 0.18.0 adds API for managing concurrent workers willmcgugan Release <h1>Textual 0.18.0 adds API for managing concurrent workers</h1><p>Less than a week since the last release, and we have a new API to show you.</p><!-- more --><p>This r...</p>https://textual.textualize.io/blog/2023/04/04/textual-0180-adds-api-for-managing-concurrent-workers/ Tue, 04 Apr 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/04/04/textual-0180-adds-api-for-managing-concurrent-workers/ Textual 0.17.0 adds translucent screens and Option List willmcgugan Release <h1>Textual 0.17.0 adds translucent screens and Option List</h1><p>This is a surprisingly large release, given it has been just 7 days since the last version (and we...</p>https://textual.textualize.io/blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/ Wed, 29 Mar 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/ Textual 0.16.0 adds TabbedContent and border titles willmcgugan Release <h1>Textual 0.16.0 adds TabbedContent and border titles</h1><p>Textual 0.16.0 lands 9 days after the previous release. We have some new features to show you.</p><p>&lt;!-- m...</p>https://textual.textualize.io/blog/2023/03/22/textual-0160-adds-tabbedcontent-and-border-titles/ Wed, 22 Mar 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/03/22/textual-0160-adds-tabbedcontent-and-border-titles/ No-async async with Python willmcgugan DevLog <h1>No-async async with Python</h1><p>A (reasonable) criticism of async is that it tends to proliferate in your code. In order to <code>await</code> something, your functions m...</p>https://textual.textualize.io/blog/2023/03/15/no-async-async-with-python/ Wed, 15 Mar 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/03/15/no-async-async-with-python/ Textual 0.15.0 adds a tabs widget willmcgugan Release <h1>Textual 0.15.0 adds a tabs widget</h1><p>We've just pushed Textual 0.15.0, only 4 days after the previous version. That's a little faster than our typical releas...</p>https://textual.textualize.io/blog/2023/03/13/textual-0150-adds-a-tabs-widget/ Mon, 13 Mar 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/03/13/textual-0150-adds-a-tabs-widget/ Textual 0.14.0 shakes up posting messages willmcgugan Release <h1>Textual 0.14.0 shakes up posting messages</h1><p>Textual version 0.14.0 has landed just a week after 0.13.0.</p><p>!!! note</p><pre><code>We like fast releases for Textual. Fa...</code></pre>https://textual.textualize.io/blog/2023/03/09/textual-0140-shakes-up-posting-messages/ Thu, 09 Mar 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/03/09/textual-0140-shakes-up-posting-messages/ Overhead of Python Asyncio tasks willmcgugan DevLog <h1>Overhead of Python Asyncio tasks</h1><p>Every widget in Textual, be it a button, tree view, or a text input, runs an [asyncio](https://docs.python.org/3/library/...</p>https://textual.textualize.io/blog/2023/03/08/overhead-of-python-asyncio-tasks/ Wed, 08 Mar 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/03/08/overhead-of-python-asyncio-tasks/ Textual 0.12.0 adds syntactical sugar and batch updates willmcgugan Release <h1>Textual 0.12.0 adds syntactical sugar and batch updates</h1><p>It's been just 9 days since the previous release, but we have a few interesting enhancements to th...</p>https://textual.textualize.io/blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/ Fri, 24 Feb 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/ \ No newline at end of file + Textualhttps://textual.textualize.io/https://github.com/textualize/textual/en Fri, 22 Sep 2023 13:59:27 -0000 Fri, 22 Sep 2023 13:59:27 -0000 1440 MkDocs RSS plugin - v1.5.0 Textual 0.38.0 adds a syntax aware TextArea willmcgugan Release <h1>Textual 0.38.0 adds a syntax aware TextArea</h1><p>This is the second big feature release this month after last week's <a href="./release0.37.0.md">command palette</a>.</p><p>&lt;!...</p>https://textual.textualize.io/blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/ Thu, 21 Sep 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/ Things I learned while building Textual's TextArea darrenburns DevLog <h1>Things I learned while building Textual's TextArea</h1><p><code>TextArea</code> is the latest widget to be added to Textual's [growing collection](https://textual.textualiz...</p>https://textual.textualize.io/blog/2023/09/18/things-i-learned-while-building-textuals-textarea/ Mon, 18 Sep 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/09/18/things-i-learned-while-building-textuals-textarea/ Textual 0.37.0 adds a command palette willmcgugan Release <h1>Textual 0.37.0 adds a command palette</h1><p>Textual version 0.37.0 has landed!The highlight of this release is the new command palette.</p><!-- more --><p>A comma...</p>https://textual.textualize.io/blog/2023/09/15/textual-0370-adds-a-command-palette/ Fri, 15 Sep 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/09/15/textual-0370-adds-a-command-palette/ What is Textual Web? willmcgugan News <h1>What is Textual Web?</h1><p>If you know us, you will know that we are the team behind <a href="https://github.com/Textualize/rich">Rich</a> and [Textual](https://github.com...</p>https://textual.textualize.io/blog/2023/09/06/what-is-textual-web/ Wed, 06 Sep 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/09/06/what-is-textual-web/ Pull Requests are cake or puppies willmcgugan DevLog <h1>Pull Requests are cake or puppies</h1><p>Broadly speaking, there are two types of contributions you can make to an Open Source project.</p><!-- more --><p>The first...</p>https://textual.textualize.io/blog/2023/07/29/pull-requests-are-cake-or-puppies/ Sat, 29 Jul 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/07/29/pull-requests-are-cake-or-puppies/ Using Rich Inspect to interrogate Python objects willmcgugan DevLog <h1>Using Rich Inspect to interrogate Python objects</h1><p>The <a href="https://github.com/Textualize/rich">Rich</a> library has a few functions that are admittedly a little o...</p>https://textual.textualize.io/blog/2023/07/27/using-rich-inspect-to-interrogate-python-objects/ Thu, 27 Jul 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/07/27/using-rich-inspect-to-interrogate-python-objects/ Textual 0.30.0 adds desktop-style notifications willmcgugan Release <h1>Textual 0.30.0 adds desktop-style notifications</h1><p>We have a new release of Textual to talk about, but before that I'd like to cover a little Textual news.</p><p>...</p>https://textual.textualize.io/blog/2023/07/17/textual-0300-adds-desktop-style-notifications/ Mon, 17 Jul 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/07/17/textual-0300-adds-desktop-style-notifications/ Textual 0.29.0 refactors dev tools willmcgugan Release <h1>Textual 0.29.0 refactors dev tools</h1><p>It's been a slow week or two at Textualize, with Textual devs taking well-earned annual leave, but we still managed to ...</p>https://textual.textualize.io/blog/2023/07/03/textual-0290-refactors-dev-tools/ Mon, 03 Jul 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/07/03/textual-0290-refactors-dev-tools/ To TUI or not to TUI willmcgugan DevLog <h1>To TUI or not to TUI</h1><p>Tech moves pretty fast.If you don’t stop and look around once in a while, you could miss it.And yet some technology feels like it h...</p>https://textual.textualize.io/blog/2023/06/06/to-tui-or-not-to-tui/ Tue, 06 Jun 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/06/06/to-tui-or-not-to-tui/ Textual adds Sparklines, Selection list, Input validation, and tool tips willmcgugan Release <h1>Textual adds Sparklines, Selection list, Input validation, and tool tips</h1><p>It's been 12 days since the last Textual release, which is longer than our usual ...</p>https://textual.textualize.io/blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/ Thu, 01 Jun 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/ Textual 0.24.0 adds a Select control willmcgugan Release <h1>Textual 0.24.0 adds a Select control</h1><p>Coming just 5 days after the last release, we have version 0.24.0 which we are crowning the King of Textual releases....</p>https://textual.textualize.io/blog/2023/05/08/textual-0240-adds-a-select-control/ Mon, 08 May 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/05/08/textual-0240-adds-a-select-control/ Textual 0.23.0 improves message handling willmcgugan Release <h1>Textual 0.23.0 improves message handling</h1><p>It's been a busy couple of weeks at Textualize.We've been building apps with [Textual](https://github.com/Textua...</p>https://textual.textualize.io/blog/2023/05/03/textual-0230-improves-message-handling/ Wed, 03 May 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/05/03/textual-0230-improves-message-handling/ Textual 0.18.0 adds API for managing concurrent workers willmcgugan Release <h1>Textual 0.18.0 adds API for managing concurrent workers</h1><p>Less than a week since the last release, and we have a new API to show you.</p><!-- more --><p>This r...</p>https://textual.textualize.io/blog/2023/04/04/textual-0180-adds-api-for-managing-concurrent-workers/ Tue, 04 Apr 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/04/04/textual-0180-adds-api-for-managing-concurrent-workers/ Textual 0.17.0 adds translucent screens and Option List willmcgugan Release <h1>Textual 0.17.0 adds translucent screens and Option List</h1><p>This is a surprisingly large release, given it has been just 7 days since the last version (and we...</p>https://textual.textualize.io/blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/ Wed, 29 Mar 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/ Textual 0.16.0 adds TabbedContent and border titles willmcgugan Release <h1>Textual 0.16.0 adds TabbedContent and border titles</h1><p>Textual 0.16.0 lands 9 days after the previous release. We have some new features to show you.</p><p>&lt;!-- m...</p>https://textual.textualize.io/blog/2023/03/22/textual-0160-adds-tabbedcontent-and-border-titles/ Wed, 22 Mar 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/03/22/textual-0160-adds-tabbedcontent-and-border-titles/ No-async async with Python willmcgugan DevLog <h1>No-async async with Python</h1><p>A (reasonable) criticism of async is that it tends to proliferate in your code. In order to <code>await</code> something, your functions m...</p>https://textual.textualize.io/blog/2023/03/15/no-async-async-with-python/ Wed, 15 Mar 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/03/15/no-async-async-with-python/ Textual 0.15.0 adds a tabs widget willmcgugan Release <h1>Textual 0.15.0 adds a tabs widget</h1><p>We've just pushed Textual 0.15.0, only 4 days after the previous version. That's a little faster than our typical releas...</p>https://textual.textualize.io/blog/2023/03/13/textual-0150-adds-a-tabs-widget/ Mon, 13 Mar 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/03/13/textual-0150-adds-a-tabs-widget/ Textual 0.14.0 shakes up posting messages willmcgugan Release <h1>Textual 0.14.0 shakes up posting messages</h1><p>Textual version 0.14.0 has landed just a week after 0.13.0.</p><p>!!! note</p><pre><code>We like fast releases for Textual. Fa...</code></pre>https://textual.textualize.io/blog/2023/03/09/textual-0140-shakes-up-posting-messages/ Thu, 09 Mar 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/03/09/textual-0140-shakes-up-posting-messages/ Overhead of Python Asyncio tasks willmcgugan DevLog <h1>Overhead of Python Asyncio tasks</h1><p>Every widget in Textual, be it a button, tree view, or a text input, runs an [asyncio](https://docs.python.org/3/library/...</p>https://textual.textualize.io/blog/2023/03/08/overhead-of-python-asyncio-tasks/ Wed, 08 Mar 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/03/08/overhead-of-python-asyncio-tasks/ Textual 0.12.0 adds syntactical sugar and batch updates willmcgugan Release <h1>Textual 0.12.0 adds syntactical sugar and batch updates</h1><p>It's been just 9 days since the previous release, but we have a few interesting enhancements to th...</p>https://textual.textualize.io/blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/ Fri, 24 Feb 2023 00:00:00 +0000Textualhttps://textual.textualize.io/blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/ \ No newline at end of file diff --git a/feed_rss_updated.xml b/feed_rss_updated.xml index d85616d6ba..6bc895016e 100644 --- a/feed_rss_updated.xml +++ b/feed_rss_updated.xml @@ -1 +1 @@ - Textualhttps://textual.textualize.io/https://github.com/textualize/textual/en Thu, 21 Sep 2023 12:27:53 -0000 Thu, 21 Sep 2023 12:27:53 -0000 1440 MkDocs RSS plugin - v1.5.0 Textual 0.38.0 adds a syntax aware TextArea willmcgugan Release <h1>Textual 0.38.0 adds a syntax aware TextArea</h1><p>This is the second big feature release this month after last week's <a href="./release0.37.0.md">command palette</a>.</p><p>&lt;!...</p>https://textual.textualize.io/blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/ Thu, 21 Sep 2023 13:27:43 +0000Textualhttps://textual.textualize.io/blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/ Things I learned while building Textual's TextArea darrenburns DevLog <h1>Things I learned while building Textual's TextArea</h1><p><code>TextArea</code> is the latest widget to be added to Textual's [growing collection](https://textual.textualiz...</p>https://textual.textualize.io/blog/2023/09/18/things-i-learned-while-building-textuals-textarea/ Thu, 21 Sep 2023 11:10:14 +0000Textualhttps://textual.textualize.io/blog/2023/09/18/things-i-learned-while-building-textuals-textarea/ Textual 0.37.0 adds a command palette willmcgugan Release <h1>Textual 0.37.0 adds a command palette</h1><p>Textual version 0.37.0 has landed!The highlight of this release is the new command palette.</p><!-- more --><p>A comma...</p>https://textual.textualize.io/blog/2023/09/15/textual-0370-adds-a-command-palette/ Fri, 15 Sep 2023 17:01:09 +0000Textualhttps://textual.textualize.io/blog/2023/09/15/textual-0370-adds-a-command-palette/ What is Textual Web? willmcgugan News <h1>What is Textual Web?</h1><p>If you know us, you will know that we are the team behind <a href="https://github.com/Textualize/rich">Rich</a> and [Textual](https://github.com...</p>https://textual.textualize.io/blog/2023/09/06/what-is-textual-web/ Wed, 06 Sep 2023 17:53:31 +0000Textualhttps://textual.textualize.io/blog/2023/09/06/what-is-textual-web/ Pull Requests are cake or puppies willmcgugan DevLog <h1>Pull Requests are cake or puppies</h1><p>Broadly speaking, there are two types of contributions you can make to an Open Source project.</p><!-- more --><p>The first...</p>https://textual.textualize.io/blog/2023/07/29/pull-requests-are-cake-or-puppies/ Sat, 29 Jul 2023 17:05:04 +0000Textualhttps://textual.textualize.io/blog/2023/07/29/pull-requests-are-cake-or-puppies/ Using Rich Inspect to interrogate Python objects willmcgugan DevLog <h1>Using Rich Inspect to interrogate Python objects</h1><p>The <a href="https://github.com/Textualize/rich">Rich</a> library has a few functions that are admittedly a little o...</p>https://textual.textualize.io/blog/2023/07/27/using-rich-inspect-to-interrogate-python-objects/ Thu, 27 Jul 2023 12:34:46 +0000Textualhttps://textual.textualize.io/blog/2023/07/27/using-rich-inspect-to-interrogate-python-objects/ Textual 0.30.0 adds desktop-style notifications willmcgugan Release <h1>Textual 0.30.0 adds desktop-style notifications</h1><p>We have a new release of Textual to talk about, but before that I'd like to cover a little Textual news.</p><p>...</p>https://textual.textualize.io/blog/2023/07/17/textual-0300-adds-desktop-style-notifications/ Mon, 17 Jul 2023 14:08:32 +0000Textualhttps://textual.textualize.io/blog/2023/07/17/textual-0300-adds-desktop-style-notifications/ Textual 0.29.0 refactors dev tools willmcgugan Release <h1>Textual 0.29.0 refactors dev tools</h1><p>It's been a slow week or two at Textualize, with Textual devs taking well-earned annual leave, but we still managed to ...</p>https://textual.textualize.io/blog/2023/07/03/textual-0290-refactors-dev-tools/ Mon, 03 Jul 2023 16:09:24 +0000Textualhttps://textual.textualize.io/blog/2023/07/03/textual-0290-refactors-dev-tools/ To TUI or not to TUI willmcgugan DevLog <h1>To TUI or not to TUI</h1><p>Tech moves pretty fast.If you don’t stop and look around once in a while, you could miss it.And yet some technology feels like it h...</p>https://textual.textualize.io/blog/2023/06/06/to-tui-or-not-to-tui/ Mon, 05 Jun 2023 17:51:19 +0000Textualhttps://textual.textualize.io/blog/2023/06/06/to-tui-or-not-to-tui/ Textual adds Sparklines, Selection list, Input validation, and tool tips willmcgugan Release <h1>Textual adds Sparklines, Selection list, Input validation, and tool tips</h1><p>It's been 12 days since the last Textual release, which is longer than our usual ...</p>https://textual.textualize.io/blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/ Thu, 01 Jun 2023 17:41:08 +0000Textualhttps://textual.textualize.io/blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/ Textual 0.24.0 adds a Select control willmcgugan Release <h1>Textual 0.24.0 adds a Select control</h1><p>Coming just 5 days after the last release, we have version 0.24.0 which we are crowning the King of Textual releases....</p>https://textual.textualize.io/blog/2023/05/08/textual-0240-adds-a-select-control/ Thu, 01 Jun 2023 11:33:54 +0000Textualhttps://textual.textualize.io/blog/2023/05/08/textual-0240-adds-a-select-control/ Textual 0.23.0 improves message handling willmcgugan Release <h1>Textual 0.23.0 improves message handling</h1><p>It's been a busy couple of weeks at Textualize.We've been building apps with [Textual](https://github.com/Textua...</p>https://textual.textualize.io/blog/2023/05/03/textual-0230-improves-message-handling/ Wed, 03 May 2023 13:22:22 +0000Textualhttps://textual.textualize.io/blog/2023/05/03/textual-0230-improves-message-handling/ Textual 0.11.0 adds a beautiful Markdown widget willmcgugan Release <h1>Textual 0.11.0 adds a beautiful Markdown widget</h1><p>We released Textual 0.10.0 25 days ago, which is a little longer than our usual release cycle. What have w...</p>https://textual.textualize.io/blog/2023/02/15/textual-0110-adds-a-beautiful-markdown-widget/ Sat, 08 Apr 2023 15:35:49 +0000Textualhttps://textual.textualize.io/blog/2023/02/15/textual-0110-adds-a-beautiful-markdown-widget/ Textual 0.18.0 adds API for managing concurrent workers willmcgugan Release <h1>Textual 0.18.0 adds API for managing concurrent workers</h1><p>Less than a week since the last release, and we have a new API to show you.</p><!-- more --><p>This r...</p>https://textual.textualize.io/blog/2023/04/04/textual-0180-adds-api-for-managing-concurrent-workers/ Tue, 04 Apr 2023 13:12:51 +0000Textualhttps://textual.textualize.io/blog/2023/04/04/textual-0180-adds-api-for-managing-concurrent-workers/ Textual 0.17.0 adds translucent screens and Option List willmcgugan Release <h1>Textual 0.17.0 adds translucent screens and Option List</h1><p>This is a surprisingly large release, given it has been just 7 days since the last version (and we...</p>https://textual.textualize.io/blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/ Wed, 29 Mar 2023 16:29:28 +0000Textualhttps://textual.textualize.io/blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/ Textual 0.16.0 adds TabbedContent and border titles willmcgugan Release <h1>Textual 0.16.0 adds TabbedContent and border titles</h1><p>Textual 0.16.0 lands 9 days after the previous release. We have some new features to show you.</p><p>&lt;!-- m...</p>https://textual.textualize.io/blog/2023/03/22/textual-0160-adds-tabbedcontent-and-border-titles/ Wed, 22 Mar 2023 13:52:31 +0000Textualhttps://textual.textualize.io/blog/2023/03/22/textual-0160-adds-tabbedcontent-and-border-titles/ Stealing Open Source code from Textual willmcgugan DevLog <h1>Stealing Open Source code from Textual</h1><p>I would like to talk about a serious issue in the Free and Open Source software world. Stealing code. You wouldn't ...</p>https://textual.textualize.io/blog/2022/11/20/stealing-open-source-code-from-textual/ Wed, 15 Mar 2023 16:49:12 +0000Textualhttps://textual.textualize.io/blog/2022/11/20/stealing-open-source-code-from-textual/ No-async async with Python willmcgugan DevLog <h1>No-async async with Python</h1><p>A (reasonable) criticism of async is that it tends to proliferate in your code. In order to <code>await</code> something, your functions m...</p>https://textual.textualize.io/blog/2023/03/15/no-async-async-with-python/ Wed, 15 Mar 2023 16:39:05 +0000Textualhttps://textual.textualize.io/blog/2023/03/15/no-async-async-with-python/ Textual 0.14.0 shakes up posting messages willmcgugan Release <h1>Textual 0.14.0 shakes up posting messages</h1><p>Textual version 0.14.0 has landed just a week after 0.13.0.</p><p>!!! note</p><pre><code>We like fast releases for Textual. Fa...</code></pre>https://textual.textualize.io/blog/2023/03/09/textual-0140-shakes-up-posting-messages/ Tue, 14 Mar 2023 09:47:28 +0000Textualhttps://textual.textualize.io/blog/2023/03/09/textual-0140-shakes-up-posting-messages/ Textual 0.15.0 adds a tabs widget willmcgugan Release <h1>Textual 0.15.0 adds a tabs widget</h1><p>We've just pushed Textual 0.15.0, only 4 days after the previous version. That's a little faster than our typical releas...</p>https://textual.textualize.io/blog/2023/03/13/textual-0150-adds-a-tabs-widget/ Mon, 13 Mar 2023 15:48:41 +0000Textualhttps://textual.textualize.io/blog/2023/03/13/textual-0150-adds-a-tabs-widget/ \ No newline at end of file + Textualhttps://textual.textualize.io/https://github.com/textualize/textual/en Fri, 22 Sep 2023 13:59:27 -0000 Fri, 22 Sep 2023 13:59:27 -0000 1440 MkDocs RSS plugin - v1.5.0 Textual 0.38.0 adds a syntax aware TextArea willmcgugan Release <h1>Textual 0.38.0 adds a syntax aware TextArea</h1><p>This is the second big feature release this month after last week's <a href="./release0.37.0.md">command palette</a>.</p><p>&lt;!...</p>https://textual.textualize.io/blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/ Thu, 21 Sep 2023 13:27:43 +0000Textualhttps://textual.textualize.io/blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/ Things I learned while building Textual's TextArea darrenburns DevLog <h1>Things I learned while building Textual's TextArea</h1><p><code>TextArea</code> is the latest widget to be added to Textual's [growing collection](https://textual.textualiz...</p>https://textual.textualize.io/blog/2023/09/18/things-i-learned-while-building-textuals-textarea/ Thu, 21 Sep 2023 11:10:14 +0000Textualhttps://textual.textualize.io/blog/2023/09/18/things-i-learned-while-building-textuals-textarea/ Textual 0.37.0 adds a command palette willmcgugan Release <h1>Textual 0.37.0 adds a command palette</h1><p>Textual version 0.37.0 has landed!The highlight of this release is the new command palette.</p><!-- more --><p>A comma...</p>https://textual.textualize.io/blog/2023/09/15/textual-0370-adds-a-command-palette/ Fri, 15 Sep 2023 17:01:09 +0000Textualhttps://textual.textualize.io/blog/2023/09/15/textual-0370-adds-a-command-palette/ What is Textual Web? willmcgugan News <h1>What is Textual Web?</h1><p>If you know us, you will know that we are the team behind <a href="https://github.com/Textualize/rich">Rich</a> and [Textual](https://github.com...</p>https://textual.textualize.io/blog/2023/09/06/what-is-textual-web/ Wed, 06 Sep 2023 17:53:31 +0000Textualhttps://textual.textualize.io/blog/2023/09/06/what-is-textual-web/ Pull Requests are cake or puppies willmcgugan DevLog <h1>Pull Requests are cake or puppies</h1><p>Broadly speaking, there are two types of contributions you can make to an Open Source project.</p><!-- more --><p>The first...</p>https://textual.textualize.io/blog/2023/07/29/pull-requests-are-cake-or-puppies/ Sat, 29 Jul 2023 17:05:04 +0000Textualhttps://textual.textualize.io/blog/2023/07/29/pull-requests-are-cake-or-puppies/ Using Rich Inspect to interrogate Python objects willmcgugan DevLog <h1>Using Rich Inspect to interrogate Python objects</h1><p>The <a href="https://github.com/Textualize/rich">Rich</a> library has a few functions that are admittedly a little o...</p>https://textual.textualize.io/blog/2023/07/27/using-rich-inspect-to-interrogate-python-objects/ Thu, 27 Jul 2023 12:34:46 +0000Textualhttps://textual.textualize.io/blog/2023/07/27/using-rich-inspect-to-interrogate-python-objects/ Textual 0.30.0 adds desktop-style notifications willmcgugan Release <h1>Textual 0.30.0 adds desktop-style notifications</h1><p>We have a new release of Textual to talk about, but before that I'd like to cover a little Textual news.</p><p>...</p>https://textual.textualize.io/blog/2023/07/17/textual-0300-adds-desktop-style-notifications/ Mon, 17 Jul 2023 14:08:32 +0000Textualhttps://textual.textualize.io/blog/2023/07/17/textual-0300-adds-desktop-style-notifications/ Textual 0.29.0 refactors dev tools willmcgugan Release <h1>Textual 0.29.0 refactors dev tools</h1><p>It's been a slow week or two at Textualize, with Textual devs taking well-earned annual leave, but we still managed to ...</p>https://textual.textualize.io/blog/2023/07/03/textual-0290-refactors-dev-tools/ Mon, 03 Jul 2023 16:09:24 +0000Textualhttps://textual.textualize.io/blog/2023/07/03/textual-0290-refactors-dev-tools/ To TUI or not to TUI willmcgugan DevLog <h1>To TUI or not to TUI</h1><p>Tech moves pretty fast.If you don’t stop and look around once in a while, you could miss it.And yet some technology feels like it h...</p>https://textual.textualize.io/blog/2023/06/06/to-tui-or-not-to-tui/ Mon, 05 Jun 2023 17:51:19 +0000Textualhttps://textual.textualize.io/blog/2023/06/06/to-tui-or-not-to-tui/ Textual adds Sparklines, Selection list, Input validation, and tool tips willmcgugan Release <h1>Textual adds Sparklines, Selection list, Input validation, and tool tips</h1><p>It's been 12 days since the last Textual release, which is longer than our usual ...</p>https://textual.textualize.io/blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/ Thu, 01 Jun 2023 17:41:08 +0000Textualhttps://textual.textualize.io/blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/ Textual 0.24.0 adds a Select control willmcgugan Release <h1>Textual 0.24.0 adds a Select control</h1><p>Coming just 5 days after the last release, we have version 0.24.0 which we are crowning the King of Textual releases....</p>https://textual.textualize.io/blog/2023/05/08/textual-0240-adds-a-select-control/ Thu, 01 Jun 2023 11:33:54 +0000Textualhttps://textual.textualize.io/blog/2023/05/08/textual-0240-adds-a-select-control/ Textual 0.23.0 improves message handling willmcgugan Release <h1>Textual 0.23.0 improves message handling</h1><p>It's been a busy couple of weeks at Textualize.We've been building apps with [Textual](https://github.com/Textua...</p>https://textual.textualize.io/blog/2023/05/03/textual-0230-improves-message-handling/ Wed, 03 May 2023 13:22:22 +0000Textualhttps://textual.textualize.io/blog/2023/05/03/textual-0230-improves-message-handling/ Textual 0.11.0 adds a beautiful Markdown widget willmcgugan Release <h1>Textual 0.11.0 adds a beautiful Markdown widget</h1><p>We released Textual 0.10.0 25 days ago, which is a little longer than our usual release cycle. What have w...</p>https://textual.textualize.io/blog/2023/02/15/textual-0110-adds-a-beautiful-markdown-widget/ Sat, 08 Apr 2023 15:35:49 +0000Textualhttps://textual.textualize.io/blog/2023/02/15/textual-0110-adds-a-beautiful-markdown-widget/ Textual 0.18.0 adds API for managing concurrent workers willmcgugan Release <h1>Textual 0.18.0 adds API for managing concurrent workers</h1><p>Less than a week since the last release, and we have a new API to show you.</p><!-- more --><p>This r...</p>https://textual.textualize.io/blog/2023/04/04/textual-0180-adds-api-for-managing-concurrent-workers/ Tue, 04 Apr 2023 13:12:51 +0000Textualhttps://textual.textualize.io/blog/2023/04/04/textual-0180-adds-api-for-managing-concurrent-workers/ Textual 0.17.0 adds translucent screens and Option List willmcgugan Release <h1>Textual 0.17.0 adds translucent screens and Option List</h1><p>This is a surprisingly large release, given it has been just 7 days since the last version (and we...</p>https://textual.textualize.io/blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/ Wed, 29 Mar 2023 16:29:28 +0000Textualhttps://textual.textualize.io/blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/ Textual 0.16.0 adds TabbedContent and border titles willmcgugan Release <h1>Textual 0.16.0 adds TabbedContent and border titles</h1><p>Textual 0.16.0 lands 9 days after the previous release. We have some new features to show you.</p><p>&lt;!-- m...</p>https://textual.textualize.io/blog/2023/03/22/textual-0160-adds-tabbedcontent-and-border-titles/ Wed, 22 Mar 2023 13:52:31 +0000Textualhttps://textual.textualize.io/blog/2023/03/22/textual-0160-adds-tabbedcontent-and-border-titles/ Stealing Open Source code from Textual willmcgugan DevLog <h1>Stealing Open Source code from Textual</h1><p>I would like to talk about a serious issue in the Free and Open Source software world. Stealing code. You wouldn't ...</p>https://textual.textualize.io/blog/2022/11/20/stealing-open-source-code-from-textual/ Wed, 15 Mar 2023 16:49:12 +0000Textualhttps://textual.textualize.io/blog/2022/11/20/stealing-open-source-code-from-textual/ No-async async with Python willmcgugan DevLog <h1>No-async async with Python</h1><p>A (reasonable) criticism of async is that it tends to proliferate in your code. In order to <code>await</code> something, your functions m...</p>https://textual.textualize.io/blog/2023/03/15/no-async-async-with-python/ Wed, 15 Mar 2023 16:39:05 +0000Textualhttps://textual.textualize.io/blog/2023/03/15/no-async-async-with-python/ Textual 0.14.0 shakes up posting messages willmcgugan Release <h1>Textual 0.14.0 shakes up posting messages</h1><p>Textual version 0.14.0 has landed just a week after 0.13.0.</p><p>!!! note</p><pre><code>We like fast releases for Textual. Fa...</code></pre>https://textual.textualize.io/blog/2023/03/09/textual-0140-shakes-up-posting-messages/ Tue, 14 Mar 2023 09:47:28 +0000Textualhttps://textual.textualize.io/blog/2023/03/09/textual-0140-shakes-up-posting-messages/ Textual 0.15.0 adds a tabs widget willmcgugan Release <h1>Textual 0.15.0 adds a tabs widget</h1><p>We've just pushed Textual 0.15.0, only 4 days after the previous version. That's a little faster than our typical releas...</p>https://textual.textualize.io/blog/2023/03/13/textual-0150-adds-a-tabs-widget/ Mon, 13 Mar 2023 15:48:41 +0000Textualhttps://textual.textualize.io/blog/2023/03/13/textual-0150-adds-a-tabs-widget/ \ No newline at end of file diff --git a/getting_started/index.html b/getting_started/index.html index 68c672bb2c..fea7e9ad2d 100644 --- a/getting_started/index.html +++ b/getting_started/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -7611,7 +7611,7 @@

Need help? - diff --git a/guide/CSS/index.html b/guide/CSS/index.html index bfafe87179..f670cde157 100644 --- a/guide/CSS/index.html +++ b/guide/CSS/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/guide/actions/index.html b/guide/actions/index.html index 9580ac9497..713e86c64e 100644 --- a/guide/actions/index.html +++ b/guide/actions/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/guide/animation/index.html b/guide/animation/index.html index 3036553fed..a2e5b3b943 100644 --- a/guide/animation/index.html +++ b/guide/animation/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -7445,132 +7445,132 @@

Animating styles + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - AnimationApp + AnimationApp - - - - -Hello, World! - - - - - - - - - - - - - - - - - - - - - + + + + +Hello, World! + + + + + + + + + + + + + + + + + + + + + @@ -7599,132 +7599,132 @@

Animating styles + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - AnimationApp + AnimationApp - + - - -Hello, World! - - - - - - - - - - - - - - - - - - - - - + + +Hello, World! + + + + + + + + + + + + + + + + + + + + + diff --git a/guide/app/index.html b/guide/app/index.html index 5bb2e5bff9..284ed2f953 100644 --- a/guide/app/index.html +++ b/guide/app/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/guide/command_palette/index.html b/guide/command_palette/index.html index 540fb7c378..a1a090b73f 100644 --- a/guide/command_palette/index.html +++ b/guide/command_palette/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/guide/design/index.html b/guide/design/index.html index 79800e111b..c63d9ce224 100644 --- a/guide/design/index.html +++ b/guide/design/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/guide/devtools/index.html b/guide/devtools/index.html index a9a6fd307a..1f692fdaa7 100644 --- a/guide/devtools/index.html +++ b/guide/devtools/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/guide/events/index.html b/guide/events/index.html index 255ad1eb7c..1ec63fc53d 100644 --- a/guide/events/index.html +++ b/guide/events/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/guide/index.html b/guide/index.html index 041f60d2ad..eb71f5e67f 100644 --- a/guide/index.html +++ b/guide/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/guide/input/index.html b/guide/input/index.html index a5799ff837..4f0548ad03 100644 --- a/guide/input/index.html +++ b/guide/input/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/guide/layout/index.html b/guide/layout/index.html index 01651a8907..c766691a7c 100644 --- a/guide/layout/index.html +++ b/guide/layout/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/guide/queries/index.html b/guide/queries/index.html index cd6687d071..f093a71c88 100644 --- a/guide/queries/index.html +++ b/guide/queries/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/guide/reactivity/index.html b/guide/reactivity/index.html index 2225f21993..6011498c9b 100644 --- a/guide/reactivity/index.html +++ b/guide/reactivity/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/guide/screens/index.html b/guide/screens/index.html index 8439e158e4..8c55db1285 100644 --- a/guide/screens/index.html +++ b/guide/screens/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/guide/styles/index.html b/guide/styles/index.html index 06a7154462..37397694ef 100644 --- a/guide/styles/index.html +++ b/guide/styles/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/guide/testing/index.html b/guide/testing/index.html index ce8ba17a50..0a41656632 100644 --- a/guide/testing/index.html +++ b/guide/testing/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/guide/widgets/index.html b/guide/widgets/index.html index 08a28a8525..a8710928c6 100644 --- a/guide/widgets/index.html +++ b/guide/widgets/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/guide/workers/index.html b/guide/workers/index.html index e7dbcc5872..f8d68b9dca 100644 --- a/guide/workers/index.html +++ b/guide/workers/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/help/index.html b/help/index.html index 1329ef8f28..965712f8eb 100644 --- a/help/index.html +++ b/help/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/how-to/center-things/index.html b/how-to/center-things/index.html index 8288d0fb64..55ef4d6826 100644 --- a/how-to/center-things/index.html +++ b/how-to/center-things/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/how-to/design-a-layout/index.html b/how-to/design-a-layout/index.html index d7d6232815..745892db65 100644 --- a/how-to/design-a-layout/index.html +++ b/how-to/design-a-layout/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/how-to/index.html b/how-to/index.html index 195d60eab3..67509c9acb 100644 --- a/how-to/index.html +++ b/how-to/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/index.html b/index.html index 65a8096408..29a6391c0c 100644 --- a/index.html +++ b/index.html @@ -71,7 +71,7 @@ - + @@ -147,7 +147,7 @@
- Welcome + Home
@@ -255,7 +255,7 @@ - Welcome + Home @@ -524,7 +524,7 @@ - Welcome + Home @@ -541,7 +541,7 @@ - Welcome + Home @@ -6997,12 +6997,13 @@ -

Welcome

-

Welcome to the Textual framework documentation.

-
+

Tip

-

See the navigation links in the header or side-bars. Click the button (top left) on mobile.

+

See the navigation links in the header or side-bar.

+

Click (top left) on mobile.

+

Welcome

+

Welcome to the Textual framework documentation.

Get started or go straight to the Tutorial

What is Textual?

Textual is a Rapid Application Development framework for Python, built by Textualize.io.

diff --git a/reference/index.html b/reference/index.html index f27b64c21c..71e185239c 100644 --- a/reference/index.html +++ b/reference/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/roadmap/index.html b/roadmap/index.html index 33f56a320c..b471f86505 100644 --- a/roadmap/index.html +++ b/roadmap/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -517,7 +517,7 @@ - Welcome + Home diff --git a/search/search_index.json b/search/search_index.json index 48117adbf1..a3ae81f050 100644 --- a/search/search_index.json +++ b/search/search_index.json @@ -1 +1 @@ -{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"],"fields":{"title":{"boost":1000.0},"text":{"boost":1.0},"tags":{"boost":1000000.0}}},"docs":[{"location":"","title":"Welcome","text":"

Welcome to the Textual framework documentation.

Tip

See the navigation links in the header or side-bars. Click the button (top left) on mobile.

Get started or go straight to the Tutorial

"},{"location":"#what-is-textual","title":"What is Textual?","text":"

Textual is a Rapid Application Development framework for Python, built by Textualize.io.

Build sophisticated user interfaces with a simple Python API. Run your apps in the terminal or a web browser (with textual-web)!

  • Rapid development

    Uses your existing Python skills to build beautiful user interfaces.

  • Low requirements

    Run Textual on a single board computer if you want to.

  • Cross platform

    Textual runs just about everywhere.

  • Remote

    Textual apps can run over SSH.

  • CLI Integration

    Textual apps can be launched and run from the command prompt.

  • Open Source

    Textual is licensed under MIT.

CalculatorApp \u257a\u2501\u2513\u00a0\u00a0\u2513\u00a0\u257b\u00a0\u257b\u250f\u2501\u2578\u250f\u2501\u2513\u257a\u2501\u2513 \u00a0\u2501\u252b\u00a0\u00a0\u2503\u00a0\u2517\u2501\u252b\u2517\u2501\u2513\u2517\u2501\u252b\u250f\u2501\u251b \u257a\u2501\u251b.\u257a\u253b\u2578\u00a0\u00a0\u2579\u257a\u2501\u251b\u257a\u2501\u251b\u2517\u2501\u2578 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0C\u00a0\u00a0+/-\u00a0\u00a0%\u00a0\u00a0\u00f7\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a07\u00a0\u00a08\u00a0\u00a09\u00a0\u00a0\u00d7\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a04\u00a0\u00a05\u00a0\u00a06\u00a0\u00a0-\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a01\u00a0\u00a02\u00a0\u00a03\u00a0\u00a0+\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a00\u00a0\u00a0.\u00a0\u00a0=\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

PrideApp

StopwatchApp \u2b58StopwatchApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a000:00:00.00\u00a0Reset\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a000:00:00.00\u00a0Reset\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a000:00:00.00\u00a0Reset\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0\u00a0A\u00a0\u00a0Add\u00a0\u00a0R\u00a0\u00a0Remove\u00a0

CombiningLayoutsExample \u2b58CombiningLayoutsExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502HorizontallyPositionedChildrenHere\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a00\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a01\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a02\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2586\u2586\u2502\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502Vertical\u00a0layout,\u00a0child\u00a03\u2502\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502Thispanelis\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a04\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a05\u2502\u2502usinggrid\u00a0layout!\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a06\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

WelcomeApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Welcome!\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b Textual\u00a0is\u00a0a\u00a0TUI,\u00a0or\u00a0Text\u00a0User\u00a0Interface,\u00a0framework\u00a0for\u00a0Python\u00a0inspired\u00a0by\u00a0\u00a0 modern\u00a0web\u00a0development.\u00a0We\u00a0hope\u00a0you\u00a0enjoy\u00a0using\u00a0Textual! Dune\u00a0quote \u258c\u00a0\"I\u00a0must\u00a0not\u00a0fear.Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0little-death\u00a0that \u258c\u00a0brings\u00a0total\u00a0obliteration.I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass \u258c\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner \u258c\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only \u258c\u00a0I\u00a0will\u00a0remain.\" \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0OK\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

"},{"location":"FAQ/","title":"FAQ","text":""},{"location":"FAQ/#frequently-asked-questions","title":"Frequently Asked Questions","text":"

Welcome to the Textual FAQ. Here we try and answer any question that comes up frequently. If you can't find what you are looking for here, see our other help channels.

"},{"location":"FAQ/#does-textual-support-images","title":"Does Textual support images?","text":"

Textual doesn't have built-in support for images yet, but it is on the Roadmap.

See also the rich-pixels project for a Rich renderable for images that works with Textual.

"},{"location":"FAQ/#how-can-i-fix-importerror-cannot-import-name-composeresult-from-textualapp","title":"How can I fix ImportError cannot import name ComposeResult from textual.app ?","text":"

You likely have an older version of Textual. You can install the latest version by adding the -U switch which will force pip to upgrade.

The following should do it:

pip install textual-dev -U\n

"},{"location":"FAQ/#how-can-i-select-and-copy-text-in-a-textual-app","title":"How can I select and copy text in a Textual app?","text":"

Running a Textual app puts your terminal in to application mode which disables clicking and dragging to select text. Most terminal emulators offer a modifier key which you can hold while you click and drag to restore the behavior you may expect from the command line. The exact modifier key depends on the terminal and platform you are running on.

  • iTerm Hold the OPTION key.
  • Gnome Terminal Hold the SHIFT key.
  • Windows Terminal Hold the SHIFT key.

Refer to the documentation for your terminal emulator, if it is not listed above.

"},{"location":"FAQ/#how-can-i-set-a-translucent-app-background","title":"How can I set a translucent app background?","text":"

Some terminal emulators have a translucent background feature which allows the desktop underneath to be partially visible.

This feature is unlikely to work with Textual, as the translucency effect requires the use of ANSI background colors, which Textual doesn't use. Textual uses 16.7 million colors where available which enables consistent colors across all platforms and additional effects which aren't possible with ANSI colors.

For more information on ANSI colors in Textual, see Why no Ansi Themes?.

"},{"location":"FAQ/#how-do-i-center-a-widget-in-a-screen","title":"How do I center a widget in a screen?","text":"

Tip

See How To Center Things in the Textual documentation for a more comprensive answer to this question.

To center a widget within a container use align. But remember that align works on the children of a container, it isn't something you use on the child you want centered.

For example, here's an app that shows a Button in the middle of a Screen:

from textual.app import App, ComposeResult\nfrom textual.widgets import Button\nclass ButtonApp(App):\nCSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Button(\"PUSH ME!\")\nif __name__ == \"__main__\":\nButtonApp().run()\n

If you use the above on multiple widgets, you'll find they appear to \"left-align\" in the center of the screen, like this:

+-----+\n|     |\n+-----+\n\n+---------+\n|         |\n+---------+\n\n+---------------+\n|               |\n+---------------+\n

If you want them more like this:

     +-----+\n     |     |\n     +-----+\n\n   +---------+\n   |         |\n   +---------+\n\n+---------------+\n|               |\n+---------------+\n

the best approach is to wrap each widget in a Center container that individually centers it. For example:

from textual.app import App, ComposeResult\nfrom textual.containers import Center\nfrom textual.widgets import Button\nclass ButtonApp(App):\nCSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Center(Button(\"PUSH ME!\"))\nyield Center(Button(\"AND ME!\"))\nyield Center(Button(\"ALSO PLEASE PUSH ME!\"))\nyield Center(Button(\"HEY ME ALSO!!\"))\nif __name__ == \"__main__\":\nButtonApp().run()\n

"},{"location":"FAQ/#how-do-i-fix-workerdeclarationerror","title":"How do I fix WorkerDeclarationError?","text":"

Textual version 0.31.0 requires that you set thread=True on the @work decorator if you want to run a threaded worker.

If you want a threaded worker, you would declare it in the following way:

@work(thread=True)\ndef run_in_background():\n...\n

If you don't want a threaded worker, you should make your work function async:

@work()\nasync def run_in_background():\n...\n

This change was made because it was too easy to accidentally create a threaded worker, which may produce unexpected results.

"},{"location":"FAQ/#how-do-i-pass-arguments-to-an-app","title":"How do I pass arguments to an app?","text":"

When creating your App class, override __init__ as you would when inheriting normally. For example:

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass Greetings(App[None]):\ndef __init__(self, greeting: str=\"Hello\", to_greet: str=\"World\") -> None:\nself.greeting = greeting\nself.to_greet = to_greet\nsuper().__init__()\ndef compose(self) -> ComposeResult:\nyield Static(f\"{self.greeting}, {self.to_greet}\")\n

Then the app can be run, passing in various arguments; for example:

# Running with default arguments.\nGreetings().run()\n# Running with a keyword argument.\nGreetings(to_greet=\"davep\").run()\n# Running with both positional arguments.\nGreetings(\"Well hello\", \"there\").run()\n

"},{"location":"FAQ/#no-widget-called-textlog","title":"No widget called TextLog","text":"

The TextLog widget was renamed to RichLog in Textual 0.32.0. You will need to replace all references to TextLog in your code, with RichLog. Most IDEs will have a search and replace function which will help you do this.

Here's how you should import RichLog:

from textual.widgets import RichLog\n

"},{"location":"FAQ/#why-do-some-key-combinations-never-make-it-to-my-app","title":"Why do some key combinations never make it to my app?","text":"

Textual can only ever support key combinations that are passed on by your terminal application. Which keys get passed on can differ from terminal to terminal, and from operating system to operating system.

Because of this it's best to stick to key combinations that are known to be universally-supported; these include the likes of:

  • Letters
  • Numbers
  • Numbered function keys (especially F1 through F10)
  • Space
  • Return
  • Arrow, home, end and page keys
  • Control
  • Shift

When creating bindings for your application we recommend picking keys and key combinations from the above.

Keys that aren't normally passed through by terminals include Cmd and Option on macOS, and the Windows key on Windows.

If you need to test what key combinations work in different environments you can try them out with textual keys.

"},{"location":"FAQ/#why-doesnt-textual-look-good-on-macos","title":"Why doesn't Textual look good on macOS?","text":"

You may find that the default macOS Terminal.app doesn't render Textual apps (and likely other TUIs) very well, particuarily when it comes to box characters. For instance, you may find it displays misaligned blocks and lines like this:

You can (mostly) fix this by opening settings -> profiles > Text tab, and changing the font settings. We have found that Menlo Regular font, with a character spacing of 1 and line spacing of 0.805 produces reasonable results. If you want to use another font, you may have to tweak the line spacing until you get good results.

With these changes, Textual apps render more as intended:

Even with this fix, Terminal.app has a few limitations. It is limited to 256 colors, and can be a little slow compared to more modern alternatives. Fortunately there are a number of free terminal emulators for macOS which produces high quality results.

We recommend any of the following terminals:

  • iTerm2
  • Kitty
  • WezTerm
"},{"location":"FAQ/#terminalapp-colors","title":"Terminal.app colors","text":""},{"location":"FAQ/#iterm2-colors","title":"iTerm2 colors","text":""},{"location":"FAQ/#why-doesnt-textual-support-ansi-themes","title":"Why doesn't Textual support ANSI themes?","text":"

Textual will not generate escape sequences for the 16 themeable ANSI colors.

This is an intentional design decision we took for for the following reasons:

  • Not everyone has a carefully chosen ANSI color theme. Color combinations which may look fine on your system, may be unreadable on another machine. There is very little an app author or Textual can do to resolve this. Asking users to simply pick a better theme is not a good solution, since not all users will know how.
  • ANSI colors can't be manipulated in the way Textual can do with other colors. Textual can blend colors and produce light and dark shades from an original color, which is used to create more readable text and user interfaces. Color blending will also be used to power future accessibility features.

Textual has a design system which guarantees apps will be readable on all platforms and terminals, and produces better results than ANSI colors.

There is currently a light and dark version of the design system, but more are planned. It will also be possible for users to customize the source colors on a per-app or per-system basis. This means that in the future you will be able to modify the core colors to blend in with your chosen terminal theme.

"},{"location":"FAQ/#why-doesnt-the-datatable-scroll-programmatically","title":"Why doesn't the DataTable scroll programmatically?","text":"

If scrolling in your DataTable is apparently broken, it may be because your DataTable is using the default value of height: auto. This means that the table will be sized to fit its rows without scrolling, which may cause the container (typically the screen) to scroll. If you would like the table itself to scroll, set the height to something other than auto, like 100%.

NOTE: As of Textual v0.31.0 the max-height of a DataTable is set to 100%, this will mean that the above is no longer the default experience.

Generated by FAQtory

"},{"location":"getting_started/","title":"Getting started","text":"

All you need to get started building Textual apps.

"},{"location":"getting_started/#requirements","title":"Requirements","text":"

Textual requires Python 3.7 or later (if you have a choice, pick the most recent Python). Textual runs on Linux, macOS, Windows and probably any OS where Python also runs.

Your platform

"},{"location":"getting_started/#linux-all-distros","title":"Linux (all distros)","text":"

All Linux distros come with a terminal emulator that can run Textual apps.

"},{"location":"getting_started/#macos","title":"macOS","text":"

The default terminal app is limited to 256 colors. We recommend installing a newer terminal such as iterm2, Kitty, or WezTerm.

"},{"location":"getting_started/#windows","title":"Windows","text":"

The new Windows Terminal runs Textual apps beautifully.

"},{"location":"getting_started/#installation","title":"Installation","text":"

Here's how to install Textual.

"},{"location":"getting_started/#from-pypi","title":"From PyPI","text":"

You can install Textual via PyPI, with the following command:

pip install textual\n

If you plan on developing Textual apps, you should also install textual developer tools:

pip install textual-dev\n
"},{"location":"getting_started/#from-conda-forge","title":"From conda-forge","text":"

Textual is also available on conda-forge. The preferred package manager for conda-forge is currently micromamba:

micromamba install -c conda-forge textual\n

And for the textual developer tools:

micromamba install -c conda-forge textual-dev\n
"},{"location":"getting_started/#textual-cli","title":"Textual CLI","text":"

If you installed the developer tools you should have access to the textual command. There are a number of sub-commands available which will aid you in building Textual apps. Run the following for a list of the available commands:

textual --help\n

See devtools for more about the textual command.

"},{"location":"getting_started/#demo","title":"Demo","text":"

Once you have Textual installed, run the following to get an impression of what it can do:

python -m textual\n

If Textual is installed you should see the following:

Textual\u00a0Demo \u2b58Textual\u00a0Demo TOP\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Widgets\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 Textual\u00a0widgets\u00a0are\u00a0powerful\u00a0interactive\u00a0components.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 Widgets Build\u00a0your\u00a0own\u00a0or\u00a0use\u00a0the\u00a0builtin\u00a0widgets.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u2022\u00a0Input\u00a0Text\u00a0/\u00a0Password\u00a0input.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 Rich\u00a0content\u00a0\u2022\u00a0Button\u00a0Clickable\u00a0button\u00a0with\u00a0a\u00a0number\u00a0of\u00a0styles.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u2022\u00a0Switch\u00a0A\u00a0switch\u00a0to\u00a0toggle\u00a0between\u00a0states.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2583\u2583 \u00a0\u2022\u00a0DataTable\u00a0A\u00a0spreadsheet-like\u00a0widget\u00a0for\u00a0navigating\u00a0data.\u00a0Cells\u00a0may\u00a0contain\u00a0text\u00a0or\u00a0Rich\u00a0 renderables.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 CSS\u00a0\u2022\u00a0Tree\u00a0An\u00a0generic\u00a0tree\u00a0with\u00a0expandable\u00a0nodes.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u2022\u00a0DirectoryTree\u00a0A\u00a0tree\u00a0of\u00a0file\u00a0and\u00a0folders.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u2022\u00a0...\u00a0many\u00a0more\u00a0planned\u00a0... \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a \u258eUsername\u258awill\u258e\u258a \u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a \u258e\u258a\u2585\u2585 \u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a \u258ePassword\u258aPassword\u258e\u258a \u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a \u258e\u258a \u258e\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258a \u258e\u00a0Login\u00a0\u258a \u258e\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Foo\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Bar\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Baz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Foo\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(0,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(0,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(0,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(0,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(1,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(1,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(1,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(1,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(2,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(2,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(2,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(2,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(3,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(3,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(3,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(3,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(4,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(4,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(4,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(4,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(5,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(5,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(5,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(5,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(6,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(6,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(6,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(6,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(7,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(7,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(7,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(7,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(8,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(8,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(8,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(8,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(9,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(9,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(9,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(9,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2582\u2582 \u00a0Cell\u00a0(10,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(10,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(10,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(10,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(11,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(11,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(11,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(11,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(12,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(12,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(12,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(12,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(13,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(13,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(13,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(13,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u258f \u00a0CTRL+B\u00a0\u00a0Sidebar\u00a0\u00a0CTRL+T\u00a0\u00a0Toggle\u00a0Dark\u00a0mode\u00a0\u00a0CTRL+S\u00a0\u00a0Screenshot\u00a0\u00a0F1\u00a0\u00a0Notes\u00a0\u00a0CTRL+Q\u00a0\u00a0Quit\u00a0

"},{"location":"getting_started/#examples","title":"Examples","text":"

The Textual repository comes with a number of example apps. To try out the examples, first clone the Textual repository:

HTTPSSSHGitHub CLI
git clone https://github.com/Textualize/textual.git\n
git clone git@github.com:Textualize/textual.git\n
gh repo clone Textualize/textual\n

With the repository cloned, navigate to the /examples/ directory where you will find a number of Python files you can run from the command line:

cd textual/examples/\npython code_browser.py ../\n
"},{"location":"getting_started/#need-help","title":"Need help?","text":"

See the help page for how to get help with Textual, or to report bugs.

"},{"location":"help/","title":"Help","text":"

If you need help with any aspect of Textual, let us know! We would be happy to hear from you.

"},{"location":"help/#bugs-and-feature-requests","title":"Bugs and feature requests","text":"

Report bugs via GitHub on the Textual issues page. You can also post feature requests via GitHub issues, but see the roadmap first.

"},{"location":"help/#help-with-using-textual","title":"Help with using Textual","text":"

You can seek help with using Textual in the discussion area on GitHub.

"},{"location":"help/#discord-server","title":"Discord Server","text":"

For more realtime feedback or chat, join our discord server to connect with the Textual community.

"},{"location":"roadmap/","title":"Roadmap","text":"

We (textualize.io) are actively building and maintaining Textual.

We have many new features in the pipeline. This page will keep track of that work.

"},{"location":"roadmap/#features","title":"Features","text":"

High-level features we plan on implementing.

  • Accessibility
    • Integration with screen readers
    • Monochrome mode
    • High contrast theme
    • Color-blind themes
  • Command palette
    • Fuzzy search
  • Configuration (.toml based extensible configuration format)
  • Console
  • Devtools
    • Integrated log
    • DOM tree view
    • REPL
  • Reactive state abstraction
  • Themes
    • Customize via config
    • Builtin theme editor
"},{"location":"roadmap/#widgets","title":"Widgets","text":"

Widgets are key to making user-friendly interfaces. The builtin widgets should cover many common (and some uncommon) use-cases. The following is a list of the widgets we have built or are planning to build.

  • Buttons
    • Error / warning variants
  • Color picker
  • Checkbox
  • Content switcher
  • DataTable
    • Cell select
    • Row / Column select
    • API to update cells / rows
    • Lazy loading API
  • Date picker
  • Drop-down menus
  • Form Widget
    • Serialization / Deserialization
    • Export to attrs objects
    • Export to PyDantic objects
  • Image support
    • Half block
    • Braille
    • Sixels, and other image extensions
  • Input
    • Validation
    • Error / warning states
    • Template types: IP address, physical units (weight, volume), currency, credit card etc
  • Select control (pull-down)
  • Markdown viewer
    • Collapsible sections
    • Custom widgets
  • Plots
    • bar chart
    • line chart
    • Candlestick chars
  • Progress bars
    • Style variants (solid, thin etc)
  • Radio boxes
  • Spark-lines
  • Switch
  • Tabs
  • TextArea (multi-line input)
    • Basic controls
    • Indentation guides
    • Smart features for various languages
    • Syntax highlighting
"},{"location":"tutorial/","title":"Tutorial","text":"

Welcome to the Textual Tutorial!

By the end of this page you should have a solid understanding of app development with Textual.

Quote

If you want people to build things, make it fun.

\u2014 Will McGugan (creator of Rich and Textual)

"},{"location":"tutorial/#video-series","title":"Video series","text":"

This tutorial has an accompanying video series which covers the same content.

"},{"location":"tutorial/#stopwatch-application","title":"Stopwatch Application","text":"

We're going to build a stopwatch application. This application should show a list of stopwatches with buttons to start, stop, and reset the stopwatches. We also want the user to be able to add and remove stopwatches as required.

This will be a simple yet fully featured app \u2014 you could distribute this app if you wanted to!

Here's what the finished app will look like:

stopwatch.py \u2b58StopwatchApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Stop\u00a000:00:02.02 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Stop\u00a000:00:09.17 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Stop\u00a000:00:06.04 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0\u00a0A\u00a0\u00a0Add\u00a0\u00a0R\u00a0\u00a0Remove\u00a0

"},{"location":"tutorial/#get-the-code","title":"Get the code","text":"

If you want to try the finished Stopwatch app and follow along with the code, first make sure you have Textual installed then check out the Textual repository:

HTTPSSSHGitHub CLI
git clone https://github.com/Textualize/textual.git\n
git clone git@github.com:Textualize/textual.git\n
gh repo clone Textualize/textual\n

With the repository cloned, navigate to docs/examples/tutorial and run stopwatch.py.

cd textual/docs/examples/tutorial\npython stopwatch.py\n
"},{"location":"tutorial/#type-hints-in-brief","title":"Type hints (in brief)","text":"

Tip

Type hints are entirely optional in Textual. We've included them in the example code but it's up to you whether you add them to your own projects.

We're a big fan of Python type hints at Textualize. If you haven't encountered type hinting, it's a way to express the types of your data, parameters, and return values. Type hinting allows tools like mypy to catch bugs before your code runs.

The following function contains type hints:

def repeat(text: str, count: int) -> str:\n\"\"\"Repeat a string a given number of times.\"\"\"\nreturn text * count\n

Parameter types follow a colon. So text: str indicates that text requires a string and count: int means that count requires an integer.

Return types follow ->. So -> str: indicates this method returns a string.

"},{"location":"tutorial/#the-app-class","title":"The App class","text":"

The first step in building a Textual app is to import and extend the App class. Here's a basic app class we will use as a starting point for the stopwatch app.

stopwatch01.py
from textual.app import App, ComposeResult\nfrom textual.widgets import Header, Footer\nclass StopwatchApp(App):\n\"\"\"A Textual app to manage stopwatches.\"\"\"\nBINDINGS = [(\"d\", \"toggle_dark\", \"Toggle dark mode\")]\ndef compose(self) -> ComposeResult:\n\"\"\"Create child widgets for the app.\"\"\"\nyield Header()\nyield Footer()\ndef action_toggle_dark(self) -> None:\n\"\"\"An action to toggle dark mode.\"\"\"\nself.dark = not self.dark\nif __name__ == \"__main__\":\napp = StopwatchApp()\napp.run()\n

If you run this code, you should see something like the following:

stopwatch01.py \u2b58StopwatchApp \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0

Hit the D key to toggle between light and dark mode.

stopwatch01.py \u2b58StopwatchApp \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0

Hit Ctrl+C to exit the app and return to the command prompt.

"},{"location":"tutorial/#a-closer-look-at-the-app-class","title":"A closer look at the App class","text":"

Let's examine stopwatch01.py in more detail.

stopwatch01.py
from textual.app import App, ComposeResult\nfrom textual.widgets import Header, Footer\nclass StopwatchApp(App):\n\"\"\"A Textual app to manage stopwatches.\"\"\"\nBINDINGS = [(\"d\", \"toggle_dark\", \"Toggle dark mode\")]\ndef compose(self) -> ComposeResult:\n\"\"\"Create child widgets for the app.\"\"\"\nyield Header()\nyield Footer()\ndef action_toggle_dark(self) -> None:\n\"\"\"An action to toggle dark mode.\"\"\"\nself.dark = not self.dark\nif __name__ == \"__main__\":\napp = StopwatchApp()\napp.run()\n

The first line imports the Textual App class, which we will use as the base class for our App. The second line imports two builtin widgets: Footer which shows a bar at the bottom of the screen with bound keys, and Header which shows a title at the top of the screen. Widgets are re-usable components responsible for managing a part of the screen. We will cover how to build widgets in this tutorial.

The following lines define the app itself:

stopwatch01.py
from textual.app import App, ComposeResult\nfrom textual.widgets import Header, Footer\nclass StopwatchApp(App):\n\"\"\"A Textual app to manage stopwatches.\"\"\"\nBINDINGS = [(\"d\", \"toggle_dark\", \"Toggle dark mode\")]\ndef compose(self) -> ComposeResult:\n\"\"\"Create child widgets for the app.\"\"\"\nyield Header()\nyield Footer()\ndef action_toggle_dark(self) -> None:\n\"\"\"An action to toggle dark mode.\"\"\"\nself.dark = not self.dark\nif __name__ == \"__main__\":\napp = StopwatchApp()\napp.run()\n

The App class is where most of the logic of Textual apps is written. It is responsible for loading configuration, setting up widgets, handling keys, and more.

Here's what the above app defines:

  • BINDINGS is a list of tuples that maps (or binds) keys to actions in your app. The first value in the tuple is the key; the second value is the name of the action; the final value is a short description. We have a single binding which maps the D key on to the \"toggle_dark\" action. See key bindings in the guide for details.

  • compose() is where we construct a user interface with widgets. The compose() method may return a list of widgets, but it is generally easier to yield them (making this method a generator). In the example code we yield an instance of each of the widget classes we imported, i.e. Header() and Footer().

  • action_toggle_dark() defines an action method. Actions are methods beginning with action_ followed by the name of the action. The BINDINGS list above tells Textual to run this action when the user hits the D key. See actions in the guide for details.

stopwatch01.py
from textual.app import App, ComposeResult\nfrom textual.widgets import Header, Footer\nclass StopwatchApp(App):\n\"\"\"A Textual app to manage stopwatches.\"\"\"\nBINDINGS = [(\"d\", \"toggle_dark\", \"Toggle dark mode\")]\ndef compose(self) -> ComposeResult:\n\"\"\"Create child widgets for the app.\"\"\"\nyield Header()\nyield Footer()\ndef action_toggle_dark(self) -> None:\n\"\"\"An action to toggle dark mode.\"\"\"\nself.dark = not self.dark\nif __name__ == \"__main__\":\napp = StopwatchApp()\napp.run()\n

The final three lines create an instance of the app and calls the run() method which puts your terminal in to application mode and runs the app until you exit with Ctrl+C. This happens within a __name__ == \"__main__\" block so we could run the app with python stopwatch01.py or import it as part of a larger project.

"},{"location":"tutorial/#designing-a-ui-with-widgets","title":"Designing a UI with widgets","text":"

Textual comes with a number of builtin widgets, like Header and Footer, which are versatile and re-usable. We will need to build some custom widgets for the stopwatch. Before we dive in to that, let's first sketch a design for the app \u2014 so we know what we're aiming for.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nNVcXGlP40pcdTAwMTb93r9cdTAwMDIxX2akxq/2paXRqFx1MDAwM4R9XHUwMDBiW8PMXHUwMDEzMrFDXGbxguNA4Kn/+5SddOwktuM4S/tFLVx1MDAxYVxcjuu66tx77lJVf33Z2NhcZj48c/PbxqbZb+pcdTAwMWTL8PX3za/h9TfT71quo5pQ9HfX7fnN6M52XHUwMDEweN1vf/xh6/6LXHUwMDE5eFx1MDAxZL1pam9Wt6d3ukHPsFxcrenaf1iBaXf/XHUwMDEz/jzVbfPfnmtcdTAwMWKBr8WdbJmGXHUwMDE1uP6gL7Nj2qZcdTAwMTN01dP/q/7e2Pgr+pmQzjebge48dczoXHUwMDBiUVMsIFx1MDAwNmzy6qnrRMJcbi5cdTAwMDWFXGaR0VxyVndHdVx1MDAxN5iGam0pkc24Jby0+aizxo/G/VPr6uX83Hu/ueqSPo97bVmdzmXw0Ymk6rrqZeK2buC7L+atZVx1MDAwNG3VXG4nrmd9y3d7T23H7HbHvuN6etNcbj7Ca1x1MDAwMIyuXHUwMDBlhuDbRnylXHUwMDFmTlx1MDAxMCdcdTAwMWFkXHUwMDAwUoboqCH8KpJcXCOEYpi4Plx1MDAxMGbb7ajBV8L8XHUwMDAzRJ9YnEe9+fKkZHKM+Fx1MDAxZVx1MDAwMpqAJlx1MDAwNuF9+IqqQ1xyy4lcdTAwMGXapvXUXHUwMDBlXHUwMDA2gmtcdTAwMWNcbp7o24yGXHUwMDFkXCLEOUFcYqNRS9ijd2BEXGL4c3Lg2rrvXHJcdTAwMDdos1x1MDAxYv6RkDZcdTAwMTR0d1x1MDAxMj5JXGIlpvag2/C2m5+HRzdcdTAwMTf1t7uL8114vnc4etZcdTAwMTjedN933zdHLT+Hv8Wi9TxDXHUwMDFmgFxiMlx1MDAwNiRcdTAwMTaSXHUwMDEwgmNcdTAwMWN2LOdFNTq9Tie+5jZfYtxFV39+LYF3gmBcdTAwMTbeJVx1MDAxNYJQSorj3XrEnty37P7JsSlcdTAwMWT97NV7/VGvON5cdTAwMDXRXHUwMDA0wXxcdTAwMWPsXHUwMDE4YlxyYyqT18uAvaVTRNE02JWGTWOcsSlwc8FcdTAwMDRUUCDrXHUwMDAy9y/MXHUwMDA0Zj9cdTAwMThH82CG67KPb+3+/bFcclx1MDAwZnZunFx1MDAwN3v/sL01XHUwMDE3tlx1MDAxOVx1MDAwNVxigWVhe0zOYmZcdTAwMWMqXHUwMDExXHUwMDE4R1x1MDAxY4nCuE5/63RcXLf1Zrvnm1VAtkxDNlZ4X1x1MDAxY9mBrztdT/dcdTAwMTWaUtBNU9CN8LTpJlAqky9WgO5lXHUwMDAyMJ5n11x0Lq3PcKhcdTAwMTNcdTAwMGZcYq/WddvqfIxNVYRMJell4CZcdTAwMDXVu6bqMcIhXHUwMDFmu/d7x3pcbpG72VTvYPpjoFx1MDAwZSzl6oxusC3DSFx1MDAxYfOmXHUwMDEyQFfP9Fx1MDAwZopcdTAwMThh17eeLEfvXFwl5SvPXHUwMDFmjMlM/lAug0SCo8J65lx1MDAxY2/3t06R6Fx1MDAxY8BG42brXHUwMDAzXFw1/I9q81x1MDAwN2NEXHUwMDAzWIJJd4lgpPRcZoGF/aWmaVx1MDAxMEMvSiFiSskgR8q1XHUwMDEyZG1cdTAwMTQymMpeXHUwMDFmn+zsideL9zuj9tK6sE9/bDfS/aNIU2Jcbvma/thZzJTeYXFmXHUwMDEyWPmXWMaTtVwiZqJcdOM4yUycXHUwMDAzyVxiT8zsLI3JXHUwMDFm5ooyk7JcdTAwMTnpOkOp8rpcdTAwMTbXmaWQXHUwMDEzXHUwMDA3XGIoXHUwMDFkS7jhqyenXHUwMDEyXHUwMDE4XFyMnFx1MDAxYWbXXGbWyk4zTPwkO1xyXHUwMDA0LE9PMGFcdTAwMTEntI1wXGKQXHUwMDE0rHh4c/zs9smja183ru5Ojp73jWtcdTAwMTI0q01PhCtVo2Lc34v8QMQ1oMhhPKYuXHUwMDEz4kSfND3DXHUwMDFhnExcdTAwMTiMXHUwMDE0XHUwMDBlU02OO6dDveMq5FRcdTAwMTaArkDtyvHKx0n/ljx8Xlx1MDAwN96xW/NZv+18st1cbkY8kmRcdTAwMDFcdTAwMWQyjpVZ4ax4wJP+0lx1MDAxNadcdTAwMTVcdTAwMTVIZGBd+WJ0knCWzitcdTAwMDRPwzyFV6hcdTAwMDQq5GGr8MeqXHUwMDEz9Fx1MDAwMPAt/Mc1XHUwMDA018ouMyz0JLskxSzPMVhmptCgQFxicVxuOSusende//LkZGf39uiqVoeg3Ttv+8e/k2RwkZQxXHUwMDA1krOpnDGBQmOccrGqIIgypnHO8Vx1MDAxOJOMJY1cdTAwMDVcdTAwMWRPZY/CXCJcdTAwMDRcdTAwMTX/XHUwMDEwsVYtXHUwMDE0XHUwMDEwYonm0MLyoKRcdTAwMTRngpJcdTAwMTPJkVx1MDAwNKQ4XHUwMDFmyGOhO/unXHKf1M97bbttXl1cdTAwMWZUvZAhqFx1MDAwNlx1MDAwNaWEXHQ+XHUwMDE5mlx1MDAxM00uI7ubVcoomt1ccrP8gvLEq6wlNP++3Xpr2O32XHUwMDE2Mz6fejY4erg74uOuz3JD8/RcdTAwMGWr50IpK5KlM0x5XHUwMDBmXHUwMDAwwDlcXKj8Ua6oXHUwMDBipaxCltJQppElKM1yYnMsXHUwMDAwXHUwMDEwfFx1MDAxNTW/6vhQl4Hurzc2n2HlpzPHoYBcdTAwMGL4TSiborCQiOE5XHUwMDEyYW7t5NW8QPzItnuNeqv/dnyB33+vuvFcIsE5XHUwMDAxqZkwXCKVT1xuVqxspVx1MDAwMnRKwuhFrrve/iDfL8+O/Ju9k7bVMPZcdTAwMGZcdTAwMDS+P2uvkrTSO6xcdTAwMWVpIZpZgFGBP8CcUFA8xZU/zFx1MDAxNVUjXHUwMDE192eoXHUwMDExpVx1MDAxYV9xOrlY2C9cdFx1MDAxMpJSuM5s8lx1MDAxMIFwXHUwMDBlXHUwMDA0LsZYw3BcdTAwMWFo41x1MDAwM7py3pph+zOi/kjM8uyFWKazKFx1MDAxOEZcdTAwMDBcdTAwMTFQ3Fl8RkivfaKzXHUwMDFh3vGI34JXd82b/d9cdTAwMTlfkdlBP9Mog1x1MDAxNOGpJVx1MDAwNsroaVx1MDAwYkf8Lf1cdTAwMTFcdTAwMDCaXHUwMDE28XNccipdXHUwMDFh0/WR4il9R1igsPeoXHUwMDBmOKmHXGJcdTAwMTBlXHUwMDEwMV1fOXRcdTAwMTbP6PpcdTAwMTXbu7o/OFx1MDAwMSe39nZw+eRcdTAwMWScXs63WkxIXHRji7NcIp6BIDvBLJByJOZYXHUwMDE3mf7O6XBv+m63u9XWg2b794NewEzQh1x1MDAwZVx1MDAxM4aMrDTJTCmfXHUwMDA2fUqAxCRcdTAwMDJUKcl6k8xzXHUwMDAzcTG62Td1I0lcdTAwMWJrYJpcdTAwMTl2epJphlx1MDAxMpYnXHUwMDE5XG4zSVx1MDAwNlwiyKOca2Gtc+y3k8tcdTAwMTf48Irk+71OwFx1MDAxYjx9LpVZRkvSNzpb34CGXGKcjIRcIq9cdTAwMTAqfWNkXCJEKUE0XGKJR5OlXHUwMDExXHKZVjWcsohcciNcdTAwMTLSUWVcYuU2QPrdzsOWj/pta5/dP+3hXHUwMDA3ez5CkYrd4/dZVeDCM0vzkGEogUCgePyf/ta/nVJcbkCcZkJcXPlRy4D4XGZGSYF5SvxcdTAwMTKuhyFcdTAwMDCtmVDmXHUwMDA14mKEUnfdYM2EMsMmT1x1MDAxMspQwkVcYiUn56ZAiOZSujP60NxvdY1cdTAwMWYt1Kyf39qn/aB5U/FaJdOYXHUwMDEwJKVYSTHXplLf1ShWXCLMJcOYraBamc4xw1x1MDAxMsZcdTAwMWU7O3uxrf7lNjk6291BXHUwMDFl+Vx1MDAxMOk5t1x1MDAxMntcXFx1MDAxMGF8PbVQxrM3XHUwMDAzIE64ilx1MDAwN+fINNvPp/a2d3x0dF4zOvt35lPt8sWpei2Ua5hcdTAwMTE0UYn/XHUwMDE42H9ccirVXzhgX7RcdTAwMTgqiFRTQcSai6E7XHI9eDyy2s9cdTAwMTdcdTAwMTf711x1MDAwZl3y2q5jP1x1MDAxZOPF8soreuwsry+9w3lcdTAwMTRS4DCHumqvj+akq7lcdTAwMDBcdTAwMTBcdFx1MDAwNouHNPnDXFzZXCIrytRGTjW2XHUwMDA0bVxcSpVcdTAwMTVcYkokoGtdXHUwMDAxXVx1MDAwMoaL+Xzrr7LO4I9lV1lcdMkmP4wogpTC4jt0asfXzkO7Z+yC163vV/XWNdjln5WvXHUwMDBmQY1yXHRTNjVTJrRVr1x1MDAwYi1XZoWIXHUwMDAxiClfRVxuL4+4LpqH6PFwz3Vfnlxm9/Ni79Opi93F+XDpj53Fh+lcdTAwMWRWj1x1MDAwZrHIXFw8ijBcdTAwMDVcXMGmeFx1MDAxOSl/lKuqnSxTOznRhFgxXHUwMDE5XHUwMDE2q98qXHUwMDE2RGqq1rxVdc1cXPi76rczSKV0/TYz84hpls5cdTAwMTGBJZtrb3jt7HnLe39/dPmLrd95dYs9YiND59aSVZ/tgEpGNSogSFnmR2G4LUjmXHUwMDFm84FcdNZJ2W2rXHUwMDAwpejbXHUwMDE0/SGiXGaCSM5D1TNcdTAwMWW59Cdr37dcdTAwMGbPXHUwMDFmxPfDmjh8faa017r/sbRMXG6lXHUwMDAwr6/sXHUwMDE2blx1MDAxOH9cdTAwMWbPnSdthFx1MDAxOPvCyFx1MDAwNHTMVpBjIcY2yY+bh7FcdTAwMTdJ270+XHUwMDEwJtdcdTAwMTJcZlx1MDAwNjWNfrOPP1FcdTAwMDAkmPB5zvvJn+Zq2lx1MDAwMlxuNFxmXHUwMDA1Y1x1MDAwMmMsMJw0XHUwMDA3QoOYhVx1MDAwYiZouFtyNTaBizBcdTAwMDWFZbhcdTAwMWSR8OSBXHUwMDAx8cJcdTAwMGWoQYSEomGGpVx1MDAwMvykvVAuNCfhSq/57UUkZNn6hGCiVH2iXHUwMDFiRnU1yzEs50k1xibj1/lVXHUwMDA3XHUwMDA1yCXS1WYvlHJcdTAwMGJoTKrRXHUwMDEzMFxc8IeT5yGFY6F7USSiodCLYThcdTAwMWNGQeTwhpHp2jRcdTAwMWQjlmn8NfRusO3atlx1MDAxNahcdTAwMDE4dy0nmLwjeqPvoZ61TX1Kb9WTk22TXG7phU9cdTAwMWO3zvFvXHUwMDFiMWSjP0a///k19e6tTDxFrZNQilx1MDAxZvcl+f/cplx1MDAwNKHszJZEUir9wsUzW/lMVFFTwjV1V9pcdTAwMGVMisP1zFxmQ45BaFNXYkaksmScyF9cdTAwMWaeslSGKFx0XHUwMDE5XHUwMDAyXHUwMDEw8TCyXHUwMDAyaOq4XGbVICVgskTOa1x1MDAxMTtCldqW8vOXbUeApiw9UapcdTAwMDEg41xckZ9I3DQwI0IjXHUwMDAyRJ5jvv3IkiW/aDgmXHUwMDBilzzccqIgxaREdFpcdTAwMTaENUSHO1x1MDAwZqek+VvZrEzwhp8p2M5pszKTXHUwMDBmXHS+nNzwhChnhOHiqcGjz7u911Zw8456jkktv9WA9duKWywsNJZusZQnpFx0wfLPR1gsXHUwMDEwimWO3ZxYvtFSPlx1MDAwZVx1MDAxMFwiZM1lsVx1MDAwNVx1MDAwMpbcQGhVXHUwMDAx1mqPY1x1MDAxNJSBeUrVS1xusP7n/DOyUKbxr9RYK5G6mi9cdTAwMWazcLSVlKyct4SzXHUwMDE3okjCwpNw5ti1kj/7XHUwMDE1NT1UxV2/llx1MDAxMidXqlx1MDAwZk6skJpiWIakcth5cqf2Mk1cdTAwMTBcdTAwMTOaXHUwMDE4nLhcdTAwMWFcdTAwMTGPiD2QODOjUVxuOFx1MDAwZuXkTFwiknagXHUwMDA1XHUwMDA2UGBaJlVT5dArn87G3Fx1MDAxNFxiiVQ2XHUwMDFhcCYoJ3FcYj1yU5SXMtixkO8x/X0jrkwkhZ9pXGbN6b7k1jd5zuJcdTAwMWWoaF1IJIpnc9+E3fQ+3G6999yRXHUwMDA3dv2cg+1SlmSdp6lRXHJcdTAwMDCkQszpbC5ccjclrGg5W9FcdTAwMDNcYjFV1mE1S0fzPILT21OvfaVcdTAwMDb70rm+wVcnwdbxoZ3uXHUwMDExzFPIXFz6Y2dcdTAwMTUy0zss7r4oJlxy/cd5dlx1MDAwMeYqY1YskdyJMJX+4FhKguY4ai1/mCu6sEe56pmayFXYXFyBo1x1MDAxM1x1MDAxMMRcZofnXHUwMDExrfPgm1x1MDAxMlx1MDAxMFxczINe/7GGM3gj71jDL0Pl3dQ97zJQ4zZyStTUWMbw5eOx2nyzzPda9kl8X4a6XHUwMDFiKolcdTAwMTlOzF8/v/z8P1xiXCJcdTAwMWT6In0= StopReset00:00:07.21Start00:00:00.00HeaderFooterStart00:00:00.00StopwatchStopwatch(started)Reset"},{"location":"tutorial/#custom-widgets","title":"Custom widgets","text":"

We need a Stopwatch widget composed of the following child widgets:

  • A \"Start\" button
  • A \"Stop\" button
  • A \"Reset\" button
  • A time display

Textual has a builtin Button widget which takes care of the first three components. All we need to build is the time display widget which will show the elapsed time and the stopwatch widget itself.

Let's add those to the app. Just a skeleton for now, we will add the rest of the features as we go.

stopwatch02.py
from textual.app import App, ComposeResult\nfrom textual.containers import ScrollableContainer\nfrom textual.widgets import Button, Footer, Header, Static\nclass TimeDisplay(Static):\n\"\"\"A widget to display elapsed time.\"\"\"\nclass Stopwatch(Static):\n\"\"\"A stopwatch widget.\"\"\"\ndef compose(self) -> ComposeResult:\n\"\"\"Create child widgets of a stopwatch.\"\"\"\nyield Button(\"Start\", id=\"start\", variant=\"success\")\nyield Button(\"Stop\", id=\"stop\", variant=\"error\")\nyield Button(\"Reset\", id=\"reset\")\nyield TimeDisplay(\"00:00:00.00\")\nclass StopwatchApp(App):\n\"\"\"A Textual app to manage stopwatches.\"\"\"\nBINDINGS = [(\"d\", \"toggle_dark\", \"Toggle dark mode\")]\ndef compose(self) -> ComposeResult:\n\"\"\"Create child widgets for the app.\"\"\"\nyield Header()\nyield Footer()\nyield ScrollableContainer(Stopwatch(), Stopwatch(), Stopwatch())\ndef action_toggle_dark(self) -> None:\n\"\"\"An action to toggle dark mode.\"\"\"\nself.dark = not self.dark\nif __name__ == \"__main__\":\napp = StopwatchApp()\napp.run()\n

We've imported two new widgets in this code: Button, which creates a clickable button, and Static which is a base class for a simple control. We've also imported ScrollableContainer from textual.containers which (as the name suggests) is a Widget which contains other widgets.

We've defined an empty TimeDisplay widget by extending Static. We will flesh this out later.

The Stopwatch widget class also extends Static. This class has a compose() method which yields child widgets, consisting of three Button objects and a single TimeDisplay object. These widgets will form the stopwatch in our sketch.

"},{"location":"tutorial/#the-buttons","title":"The buttons","text":"

The Button constructor takes a label to be displayed in the button (\"Start\", \"Stop\", or \"Reset\"). Additionally, some of the buttons set the following parameters:

  • id is an identifier we can use to tell the buttons apart in code and apply styles. More on that later.
  • variant is a string which selects a default style. The \"success\" variant makes the button green, and the \"error\" variant makes it red.
"},{"location":"tutorial/#composing-the-widgets","title":"Composing the widgets","text":"

To add widgets to our application we first need to yield them from the app's compose() method:

The new line in StopwatchApp.compose() yields a single ScrollableContainer object which will create a scrolling list of stopwatches. When classes contain other widgets (like ScrollableContainer) they will typically accept their child widgets as positional arguments. We want to start the app with three stopwatches, so we construct three Stopwatch instances and pass them to the container's constructor.

"},{"location":"tutorial/#the-unstyled-app","title":"The unstyled app","text":"

Let's see what happens when we run stopwatch02.py.

stopwatch02.py \u2b58StopwatchApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Stop\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Reset\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 00:00:00.00 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Stop\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2586\u2586 \u00a0Reset\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 00:00:00.00 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a0 \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0

The elements of the stopwatch application are there. The buttons are clickable and you can scroll the container but it doesn't look like the sketch. This is because we have yet to apply any styles to our new widgets.

"},{"location":"tutorial/#writing-textual-css","title":"Writing Textual CSS","text":"

Every widget has a styles object with a number of attributes that impact how the widget will appear. Here's how you might set white text and a blue background for a widget:

self.styles.background = \"blue\"\nself.styles.color = \"white\"\n

While it's possible to set all styles for an app this way, it is rarely necessary. Textual has support for CSS (Cascading Style Sheets), a technology used by web browsers. CSS files are data files loaded by your app which contain information about styles to apply to your widgets.

Info

The dialect of CSS used in Textual is greatly simplified over web based CSS and much easier to learn.

CSS makes it easy to iterate on the design of your app and enables live-editing \u2014 you can edit CSS and see the changes without restarting the app!

Let's add a CSS file to our application.

stopwatch03.py
from textual.app import App, ComposeResult\nfrom textual.containers import ScrollableContainer\nfrom textual.widgets import Button, Footer, Header, Static\nclass TimeDisplay(Static):\n\"\"\"A widget to display elapsed time.\"\"\"\nclass Stopwatch(Static):\n\"\"\"A stopwatch widget.\"\"\"\ndef compose(self) -> ComposeResult:\n\"\"\"Create child widgets of a stopwatch.\"\"\"\nyield Button(\"Start\", id=\"start\", variant=\"success\")\nyield Button(\"Stop\", id=\"stop\", variant=\"error\")\nyield Button(\"Reset\", id=\"reset\")\nyield TimeDisplay(\"00:00:00.00\")\nclass StopwatchApp(App):\n\"\"\"A Textual app to manage stopwatches.\"\"\"\nCSS_PATH = \"stopwatch03.tcss\"\nBINDINGS = [(\"d\", \"toggle_dark\", \"Toggle dark mode\")]\ndef compose(self) -> ComposeResult:\n\"\"\"Create child widgets for the app.\"\"\"\nyield Header()\nyield Footer()\nyield ScrollableContainer(Stopwatch(), Stopwatch(), Stopwatch())\ndef action_toggle_dark(self) -> None:\n\"\"\"An action to toggle dark mode.\"\"\"\nself.dark = not self.dark\nif __name__ == \"__main__\":\napp = StopwatchApp()\napp.run()\n

Adding the CSS_PATH class variable tells Textual to load the following file when the app starts:

stopwatch03.tcss
Stopwatch {\nlayout: horizontal;\nbackground: $boost;\nheight: 5;\nmargin: 1;\nmin-width: 50;\npadding: 1;\n}\nTimeDisplay {\ncontent-align: center middle;\ntext-opacity: 60%;\nheight: 3;\n}\nButton {\nwidth: 16;\n}\n#start {\ndock: left;\n}\n#stop {\ndock: left;\ndisplay: none;\n}\n#reset {\ndock: right;\n}\n

If we run the app now, it will look very different.

stopwatch03.py \u2b58StopwatchApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a000:00:00.00\u00a0Reset\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a000:00:00.00\u00a0Reset\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a000:00:00.00\u00a0Reset\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0

This app looks much more like our sketch. Let's look at how Textual uses stopwatch03.tcss to apply styles.

"},{"location":"tutorial/#css-basics","title":"CSS basics","text":"

CSS files contain a number of declaration blocks. Here's the first such block from stopwatch03.tcss again:

Stopwatch {\nlayout: horizontal;\nbackground: $boost;\nheight: 5;\nmargin: 1;\nmin-width: 50;\npadding: 1;\n}\n

The first line tells Textual that the styles should apply to the Stopwatch widget. The lines between the curly brackets contain the styles themselves.

Here's how this CSS code changes how the Stopwatch widget is displayed.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXGlT20pcdTAwMTb9nl/hYuZj3Ol9SdXUXHUwMDE0mLCEsIUlycx7lVx1MDAxMpKwXHUwMDE1ZMuxZFx1MDAxNr/Kf58rQazd2MZcdTAwMDYz9VRcdTAwMTRcdTAwMTi1lqu+5/Q993bLf71pNNaiu7679r6x5t7alu85XHUwMDAz62btbbz/2lx1MDAxZIRe0IMmmvxcdTAwMWZcdTAwMDbDgZ1cdTAwMWPZiaJ++P7du641uHKjvm/ZLrr2wqHlh9HQ8Vx1MDAwMmRcdTAwMDfdd17kdsN/x79cdTAwMGasrvuvftB1olx1MDAwMUpv0nRcdTAwMWQvXG5cdTAwMDb393J9t+v2olx1MDAxMK7+X/i/0fgr+Z2xbuDakdVr+25yQtKUXHUwMDFhKDUt7j1cYnqJsUxcdTAwMWLKJNVkfIBcdTAwMTduwu1cIteB1ksw2U1b4l1rp+fnV527b8cnrfbt94D7XHUwMDFi3uczkt710vP9k+jOT6xcblx1MDAwM3iYtC2MXHUwMDA2wZX7xXOiXHUwMDBltJLC/rqzXHUwMDA2wbDd6blhmDsn6Fu2XHUwMDE33cWPgMc773vgfSPdc1x1MDAxYvvHMIQ1M5ozJcYt8amUcyR1Zue9Ja3Ah55cdTAwMDdL/oGTLbXlwrKv2mBQz0mPsV2HO1Z6zM3D81x0KZFSiuXu2nG9dieKn1x1MDAwNGOkXHUwMDA1XHUwMDExkmbu7ia9ToRmUnKJ06eN79nfdVx1MDAxMlx1MDAwMPyZ7Zme89AzvaHvp2bGXHJcdTAwMWaKoMlcdTAwMDIn41Duj1pHN44z3Fx1MDAxMteYbOtmv+tvjVx1MDAxZieHMmswXGJu1sYtv1x1MDAxZT6lXHUwMDE2XHL7jnVcdTAwMGZcdTAwMWRcIiU2WmAhXHUwMDE11eN23+tdXHUwMDE1jfVcdTAwMDP7KkVbsvfX2zlQrjNAKKCcXHUwMDFiXGZcdTAwMDBQxkyN8rsvx1x1MDAxZlpmt7l7sVx1MDAxZH36XHUwMDFlXHUwMDA1m+vfv929JMpcdTAwMDEvj8CcYYaUMErmMJXAXFxiZLhghD5cculcdTAwMWPbWKgy0onEZYBLWcJ1XGZcYqYwXHUwMDExz4LrT+vOZcft2ups+1x1MDAwN93fOT883j2+rcZ15N5GL1xi6+T2XHUwMDE1iFaE1CFagT9h5FJ8akRP7o48ojuW3Vx1MDAxOVx1MDAwZdxcdTAwMTXAtOCIVmNaU8SfjuloYPXCvjVcdTAwMDBAVYzgplxma8pKsNaEY8IlN1x1MDAwYoN1NfI0JopzXHUwMDA2XHUwMDAxxcyAvNTDQS868UZxJ1Oc27tldT3/LuekXHUwMDA0ktA/J5E1iLI9XHUwMDE4unDL+Fx1MDAxYUTlXHUwMDBlXve9dozZNVx1MDAxYlx1MDAxZcJcdTAwMWTk4Fx1MDAxY3mgasZcdTAwMDd0PcfJjuA2WGDBNVx1MDAwN7vTjLzBwGt7Pcs/zVx1MDAxOTh/1JC4NmpcdTAwMDA8qVx1MDAxNJRjNTXJLs9bR82D/Vx1MDAxZnZcdTAwMTieXvMvrattdui9LMnUY1x1MDAxY+NGIaNAbpiSPFJcdTAwMDJRSvPkWzTJiGCIXHUwMDE0mTymXHUwMDFiXHUwMDEzqKDbXHUwMDFlWCdcdTAwMDWEXHUwMDEyLtXiSDcplvxs/Tw7bNODjf6BXGZcdTAwMGZYf32fd9ZfVSxcdTAwMTGM1eFcXCjCmOQzxJLJ3bGaMFx1MDAxN1x1MDAwNNfBXFxcdTAwMTOkXGIvwGzRMKeqjO6KYIKxXHUwMDExhDC6OO3/SDDheFx1MDAwNug9LZhg/D75QfmOXFx6SHlkVC6GlKyZ81x1MDAwN1x1MDAxNsNqxZvkXFxRo9X06VxiiUbHwaXeXHUwMDEyx9d476K5c9lum9FqpyNcdTAwMTJYVZlzS4qUNE/PRury7upsRJeYJsBcYmJohoPLjCBk3TlmXHUwMDAz22/rI4b90Z41+mzvT1x1MDAxNUHeTrrsK0re61wik8a6jijGKEO1Znp6okzs5lx1MDAxNc1yXHUwMDE0q6GKXHUwMDA2/bVcdTAwMDCqTFxmTFVsqYhLRIPWXHUwMDEykv8/xqXPbug+b5LzyHhejEj3XHUwMDA2zsUurmrDkCaKMqxcdJ6aXba5acqbj6PNnVx1MDAxZGNv9Jtfty82/VVnl2FcdTAwMDblKXTPLYPMMlx1MDAwNZ+syGbKtV5cbj4gYpFBaHVoJVx1MDAxYTGLwmpi6TmJXHUwMDE1XHUwMDA1/TpW5Vx1MDAxZaRIod/GTCTRfYisYpFcdTAwMTF1LDKGXG4lKZ9ezIU6tH7Y/fObs30pdLBcdTAwMWZcdTAwMWVccjtm5VlEXGZSxVpAQiRjXHUwMDEwgU4gT5xHmUgmjLjGPDdVMyZcdTAwMTU3SFx1MDAwYpFv/M0uzDCnRlxuNVx1MDAwM71SlfVcdTAwMWJcdTAwMTf0Yc+vWVk3n4rK9KI1iDa8nuP12sVT3J5T0+JbYdRcbrpdL1x1MDAwMjOOXHUwMDAyr1x1MDAxN1x1MDAxNY9IrrtcdTAwMWVjveNaJebAlbNtRVL04yvm5W76qZGiJvln/PnPt5VHN8uOTXZnfJpe4k3278xcdTAwMTRcdTAwMTZEXHUwMDE2945cdTAwMGJ9XHUwMDFjc1x1MDAwZVx1MDAxNJ9+XHUwMDEyNNjb/EG2etf+9o9j94BcdTAwMGbZXHUwMDA3vi9Xn8JcdTAwMTKp4pzj/VxmqkHw/MulMEFwXHUwMDAzrFx1MDAxNIa7S1xuv1N3pFTGSFx1MDAxMlx1MDAwM4iQ4H2IjCVcdTAwMTXKXHUwMDE4xEqhZyq1/83n5+NzvZPjrejeXHUwMDE5qZ1Ih1xuZoO2q2W2XHUwMDExJp5Mz1x1MDAxND9cdTAwMWaj9lx1MDAwNiNcdTAwMWJH+ESwy+1u6+Y/ncNR3/+48tRcdTAwMDZcdTAwMDZcdTAwMGLMXGYuRWcuXHUwMDExlpLjZZbwKYabK11ZwIemglnjSifXXHUwMDE0XHUwMDAw8vJcXJ5rNm0luJxrWyiRyy5NTvvtzFx1MDAwNTGXitpJXHSipJEwWkwvq+Uu3zsyh7fru4e7QeeL+DB0jtXqXHUwMDEzVyFVZEhcdTAwMTKTXHUwMDA1RphcdTAwMTZWXHUwMDBmLZy4ulxcX0qZW2IsXHUwMDAxLa20kS+upv9mbPnoXG5fXHUwMDE2zpuKqlx1MDAxM+c0uObFveNcdTAwMDUpkFx1MDAwNzKq9fRz5XpcdTAwMDN3j/btT9s3XHUwMDE3n4eR/rBtt1uXXHUwMDBi5qtjhVx1MDAxZHehhKVKIa6q8mAqXHUwMDEwK+Soi6/VUiSK0TxdUshcZsJJ0sRNsqW+eGCwwdhcdTAwMTBN8PMswFx1MDAxYfV1Z7RPvu58O1xuNvpdtqPWeVrkzKGuODfxdtJ1N7+FV2fOQXvnzmtcdTAwMWXuaOc6PO21XHUwMDE2Oucxy/gykU11VVmKa4lcdTAwMDTeoVxcSZmpOD3GpD1ztH973lx1MDAxYlx1MDAxZFx1MDAwN1tcdTAwMTY+3W3tnUaOs/pMMlx1MDAxYVx1MDAxOVMxXHUwMDFmXHUwMDBmXGJFy5asRFat7SqLVCmwoYw/05LFJzDmcWRcdTAwMTOuNGczIDtcdTAwMDXQPNXfTjDwRnF91m/41l0wrJlgqalcdTAwMDP77mWeOIupXHUwMDAyl42aSN/aYlx1MDAxMnRnXHUwMDFkf6VcdTAwMTJcdTAwMWNcdTAwMGJNplx1MDAwZoSTvb6iwpVcdTAwMWKGaHHRTHKqXHUwMDExyFx1MDAxMEKZXHUwMDAwclPOl8hhXCKRllx1MDAxNFBccsZcdTAwMDDCVVxu7kz6yVxihERcdTAwMDKySGtwSibbTFclXHUwMDBiaJJslpnNRata+EPFXFwzM1x1MDAwNVWbMezhpZHdKcRWwmZ7XHUwMDE4W9kkiEOPXHUwMDE4JrHmMZS1zlx1MDAxY9W2+snIjZhhXFxcdTAwMTCBwftClFx1MDAxZT0np59sXHUwMDEyQUxjqZigQihcdTAwMDXAqrZIYKyEhFxcimNOy+54VWW0WmTHW7NcZupcdTAwMTlFfq0sMfVcdTAwMGLOMSdEsVx1MDAxOfLxw+DO4k54ff1ldHeuw7NbbM72Vn1Yo0IjTngpXHUwMDFk58QgoYtLUVx1MDAxNz6gVa1cdTAwMTEsi1x1MDAxMiaJxmyRazFej9x+mighXHLb9f1G11x1MDAxYYAsWFx1MDAwNUGSN2g+MVwiMolegbWUKqownaH8PdnbK8paRlx1MDAxOeKCK+CEXHUwMDAxksr0ce+5S5ApTlx1MDAxZi/8XVx1MDAxMYyIVFxuXHUwMDEzXHUwMDA16TdE81xuLcI5wsVcdTAwMWH9mNLUyDjZmGVcdTAwMWTIypbW6lwi/uSIkFx1MDAxNSFcdTAwMThRKiBcdTAwMDVUgsVcdTAwMDE9M3c7XHUwMDBl+Vxu3aeOS1x1MDAxNlx1MDAxZnGsNYZwJjVcdTAwMDX8UJa+XHIyNoUhSVx1MDAxNNFcdTAwMTW2vCbRUYvgeGtmwLsgscFVffGfU1x1MDAxMtdcdTAwMTPN9Fx1MDAwYj+DT/3t06/d1sneh83RlzY3gm0uupq4+IWfXFwhSD1UbuI9XHUwMDE5skDQXHUwMDEyTJb84k06bTpcdTAwMWWjMmsgxiVcdTAwMTCl43l38zxqY1lcdTAwMGKa01x1MDAwNXDpczyT2uhbTjxcdTAwMWH90bOSLmpcXFxmoyjoVa+Ly1x1MDAxNGieZ13cIzbOp0iMqZ+Rh5QyjjczvFQ3XHUwMDE5XHUwMDEyK0ptjTWIXHUwMDBlI0g8y22yq3DvJYlGy13WTeM8XHUwMDFlayZULHziZVx1MDAxMGWySzBcdTAwMTGyXGKBQVx1MDAxOUllZEX5XHUwMDEz0ktqlHjR2lxi5UYuYr1NnVx1MDAxNphcdTAwMWM68rJcdTAwMDRzoaRkQlx1MDAwYkY1XHUwMDE1qVvHYoBgXHUwMDA0fVx1MDAxZft2PmUy+fs18taA3OVKa2ygg1x1MDAxNElfXHUwMDBiXHUwMDFlXHUwMDFiXHUwMDAzQZvGqVwieJFRzMTrLos060GdNJfwvCClQln9bI2iWlxiQXG6mu2x4cy/utz+aX8ynYNd7Xe3xGZw5lxmVn0441qh4mqEZCBjXHUwMDE0SUrwUpfSXHUwMDEzOZVS0VhcdTAwMTlccu5/nlx1MDAxN7q+hV/7srP58/bC73V+3uy3XHUwMDA33Y81Xy8xh1Dh0NnPJ1RShzTs2CN/9Lyw8c+LIFxiq2dtnl2lTDJwPolCSP1SXHUwMDA2oWFw4Wz696EnY2FFKS2kQFLG31x1MDAwYsMwXHUwMDAxzFx1MDAxNTNcdTAwMTCOlvviv0RExyNcdTAwMDcxXHUwMDFhXCKpIFx1MDAxNStcdTAwMWGMRIWv/lx1MDAxOL+8aSBlXHUwMDAyXHUwMDEx+ULC5IkknVKYTI5cdTAwMTRcdTAwMDVhXCJcdTAwMTTTWihKuMBgXHUwMDFlz1x1MDAxY/a7YFx1MDAwMsExKWBPXHUwMDE2Jq928XEtoOKtOcZSnSh483DhNavfP4nA1WPPXHUwMDAwtjznYVBNn27t2nNvNipeTL5MtnhcXEp6OKa/m+Dv15tf/1x1MDAwM1x1MDAxY9s3yyJ9 Start00:00:00.00Reset5 lineshorizontal layout1 cell margin1 cell paddingaround buttonsbackground coloris $boost
  • layout: horizontal aligns child widgets horizontally from left to right.
  • background: $boost sets the background color to $boost. The $ prefix picks a pre-defined color from the builtin theme. There are other ways to specify colors such as \"blue\" or rgb(20,46,210).
  • height: 5 sets the height of our widget to 5 lines of text.
  • margin: 1 sets a margin of 1 cell around the Stopwatch widget to create a little space between widgets in the list.
  • min-width: 50 sets the minimum width of our widget to 50 cells.
  • padding: 1 sets a padding of 1 cell around the child widgets.

Here's the rest of stopwatch03.tcss which contains further declaration blocks:

TimeDisplay {\ncontent-align: center middle;\nopacity: 60%;\nheight: 3;\n}\nButton {\nwidth: 16;\n}\n#start {\ndock: left;\n}\n#stop {\ndock: left;\ndisplay: none;\n}\n#reset {\ndock: right;\n}\n

The TimeDisplay block aligns text to the center (content-align), fades it slightly (opacity), and sets its height (height) to 3 lines.

The Button block sets the width (width) of buttons to 16 cells (character widths).

The last 3 blocks have a slightly different format. When the declaration begins with a # then the styles will be applied to widgets with a matching \"id\" attribute. We've set an ID on the Button widgets we yielded in compose. For instance the first button has id=\"start\" which matches #start in the CSS.

The buttons have a dock style which aligns the widget to a given edge. The start and stop buttons are docked to the left edge, while the reset button is docked to the right edge.

You may have noticed that the stop button (#stop in the CSS) has display: none;. This tells Textual to not show the button. We do this because we don't want to display the stop button when the timer is not running. Similarly, we don't want to show the start button when the timer is running. We will cover how to manage such dynamic user interfaces in the next section.

"},{"location":"tutorial/#dynamic-css","title":"Dynamic CSS","text":"

We want our Stopwatch widget to have two states: a default state with a Start and Reset button; and a started state with a Stop button. When a stopwatch is started it should also have a green background and bold text.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nNVaa1PiSFx1MDAxNP3ur7CYr5rp98OqrS1fOO4oXCLq6ri1ZYWkIZFAMFx0XHUwMDAzOOV/305wJYFEXHUwMDEwXHUwMDExXHUwMDE5Synp7vS96b6nz+nb/Wtjc7NcdTAwMTRccruqtLNZUlx1MDAwM8v0XFw7MPulrbj8p1xuQtfv6CqUfFx1MDAwZv1eYCUtnSjqhjtfv7bNoKWirmdayvjphj3TXHUwMDBio57t+oblt7+6kWqHf8afXHUwMDE1s63+6PptO1xujLGRbWW7kVx1MDAxZoxsKU+1VSdcbnXv/+jvm5u/ks+Ud4GyXCKz0/RU8kBSNXZcdTAwMTBTNlla8TuJs1x1MDAxMENcdTAwMDExxlx1MDAwMr20cMNcdTAwMDNtL1K2rm5on9W4Ji4qoW23o/reoSjzk6FPh+JQXHUwMDBl62OzXHLX8y6ioZe4XHUwMDE1+vptxnVhXHUwMDE0+C117dqRXHUwMDEz254oL3oq8HtNp6PCMPOM3zUtN1x1MDAxYcZlXHUwMDAwvJSOxmBnc1xcMohniFx1MDAwMlx1MDAwM0GJ9GtcbojoS138NELSoIhiXHRYqmbk0b7v6SnQXHUwMDFlfVx1MDAwMcnP2Ke6abWa2rGOPW5DgFx1MDAwNShcdTAwMWa36T+/J2XIwJJQnDbtKLfpRCPvXHJcdTAwMGVcdTAwMDVP2VbJ2ENcdTAwMDRcdTAwMDRHXHUwMDEwkfHUxFx1MDAxNrvHdlx1MDAxMlx1MDAwN/9Ojp5jXHUwMDA23edRKoXxl5S3saOHk0GUXHUwMDBlpNT87pFcdTAwMTCE/UFj96T5sCtxXHUwMDEz/+Wdf3/pK1x1MDAxM3VmXHUwMDEw+P3SS83T839j13pd21x1MDAxY0VcdTAwMTJkXGZxyKCk+u+l3nM7LV3Z6XneuMy3WuPgS0qftlx1MDAxNoh6wkFh1EtcdTAwMGUklZSLuaM+qNTR7vZcdL4/sitn1UrtR3hb21vzqGfAkFx1MDAwMFx0jFx1MDAwMJ6KekJcclx1MDAwMlx1MDAxOYD0vVHfMDV60HTU686ng52xqSiHUlxuXHUwMDAyQGqyVlx1MDAxMuWHu4d30VEzVJWWffvQOFx1MDAwNPBuu5dcdTAwMWblkVx1MDAxYUSpIN/K7zbTemteg5+HnYyfXHUwMDE5siBFsEFcYnKs10syN2peXHUwMDFm5SxqXHUwMDFj03J6gVpcdTAwMDfc0GLcMGbIZeAmXG7MTtg1XHUwMDAzXHUwMDFkqznYoTnYQXhcbjtSYqqdoWL52FlmXHUwMDFjjufb70RcdTAwMTfuY1x1MDAxMksgU1o22643zExZXHUwMDEyoNrTi8hPO2qGSltMVnGeabvruc04gEuWflx1MDAwN1x1MDAxNWRiO3K1rnpp0HZtO81cdTAwMTmWdsDUfVx1MDAwNsfzLPV+4Dbdjuldpv1bnKZQ6jUmaUpALUpcdTAwMDCZX5v1XHUwMDBlr1x1MDAwM+fS/SZJvyo6dXl601x1MDAxMt/Wm6VcYqVcdTAwMDaEnHHNxlNoo9JcdTAwMDBcdTAwMTJyyTL6aFx1MDAxMZZKfvKQhlxySIq0XHUwMDE51lx1MDAwYoEgOEebMY6JXlxiXHRbPvJeY63WXfPQbLdcdTAwMWQk78hjXHUwMDE5n4fSuyrQZsthrXyD68dakMhCXHUwMDE0SUxcdTAwMDSjLKUwZsHo9WFeU9pcIlruXHUwMDE1XHUwMDAySVNcdTAwMWFcdTAwMTV4MpiXTVtcdTAwMDRPY2iatqhcdTAwMDZcdTAwMWRcdTAwMTP4I7CzPqxcdTAwMDXATvJrZFx1MDAwN/TDyWtcdTAwMDZcdTAwMDNMklfazcU5jMvirVx1MDAxNlx1MDAxMZzogYfzi0bYP6uehFF9t7ZrXHUwMDFmPJRZ11xu67X1JjGGpcFFNoVcdTAwMTA/uc1cclx1MDAxZOlUvlx1MDAxM3VfLGVcdTAwMTPbnHeTJSZcdTAwMTHHXHUwMDA0xVxcolXvsS49//6x45fvavVqZ7/Svq5W9uvvYavfq9tZ3Jpv8Fxy3Eo5kzKlXHUwMDBlP4hbXHUwMDE5K1aoVCAuxVx1MDAxYsD9+iivKbUyQnLhXHKJISAj781cdTAwMWMuZy9IuVx1MDAxNrJcdTAwMWN/XHUwMDAwxpdcdTAwMTmB72PVmlxuVbRSPp1BRpN8OnJwcSalQFx1MDAxNGGNx0hcdTAwMTNcZr9cdTAwMDFrXHUwMDAz+O3+7j4kofqhRexpMPjR3f5MXCLFcyXqqeSMTjEpgsYyXHUwMDA0bCGVUsZcZs71ti5tIJOlXHUwMDE3dFwi6/N/nlx1MDAxZSMtcEBqXHUwMDExXFyJno1XXHUwMDFl+Fx1MDAwNuQtXHUwMDFllIyjoqDEXHUwMDE4MoTwXHUwMDFiUlx1MDAxNOft87NK1ynT/burRlx1MDAxN3lcdTAwMTXsW0frre6SRDqmlGTSXHUwMDEwSVTG26qJNOEyj47mTaJTXHUwMDBl9NKPwYrTXHUwMDExe41WebD7/Vie3J62L09cdTAwMDfgxqyU36/EfpduZ1x0vHyD6yfwaHHKX6+4klxuXCLnz528PsprKvCSlH8uwqGuWEJcdTAwMDJyXHUwMDE5XHUwMDEyXHUwMDBmaX2nXaFopYmTVUu8i8hcZlYr8WYw0nS+P3ZwcTYlpJBNIcNcdTAwMTJLXHUwMDFkb/NrvL9r29fSqdVOq4KU/dtcdTAwMDPn4LzZ/FxcuPG5Uv5cdTAwMTTg6VxylTCYpO9l01x1MDAxOVhbLOFPmdB2Kf6Ao7bXOOvCeri+8/f/dlx1MDAwNvXHq+F5w2pcdTAwMGWCo/dT4e/S7SyGzTe4flxmi3nhXHIsxiiIz3Hnh/zro7ymkI9cdTAwMGYnciGvXHRcdTAwMTZ+KLnOdyghpN5eM/BcdTAwMTH4Xlx1MDAxZm79rEOJXHUwMDE5JLXwoURcdTAwMTHcXGIovsNCMJGI0vlcdTAwMDXtzZk68Fx1MDAxYex+31x1MDAxOYKa1+Ku6Fx1MDAwNnTdXHUwMDA1LYlcdTAwMTMpOVlcdTAwMTQujUmVu3TAXHUwMDAxlFx1MDAwM7hpRiWAUckkXm3aXHUwMDA0QcRTm+pVXFxe6ZuR5eTjTeTjzVON6Fx1MDAxNbRlLsRkoZZ5kbybKiNnXHUwMDE2Qlx1MDAxNS1GXHUwMDE1RHqXKKVEb4BVs/5Ar1x1MDAxYSdcdTAwMDdu9fjspkrCI+ZcdTAwMDRrXHUwMDBmK6hcdTAwMTUqZtn7usmjnFx1MDAxOIxcIvyx5+tcdTAwMTLOhSwmXHUwMDEwkECI1e1cdTAwMTOJ5jLGqcSrRJbehil7czGEzWS0xTE26dZcYmtcdTAwMWLPKrRkdru6TVx1MDAxNDs3Qp6eXHUwMDFj135+/XHXpZ+u6u9cdTAwMTXfZNp4xm9cZlx1MDAxNFx1MDAxNU/Nr6eNp/9cdTAwMDDD6SGzIn0= Stop00:00:00.00ResetStart00:00:00.00StopwatchStarted Stopwatch

We can accomplish this with a CSS class. Not to be confused with a Python class, a CSS class is like a tag you can add to a widget to modify its styles.

Here's the new CSS:

stopwatch04.tcss
Stopwatch {\nlayout: horizontal;\nbackground: $boost;\nheight: 5;\nmargin: 1;\nmin-width: 50;\npadding: 1;\n}\nTimeDisplay {\ncontent-align: center middle;\ntext-opacity: 60%;\nheight: 3;\n}\nButton {\nwidth: 16;\n}\n#start {\ndock: left;\n}\n#stop {\ndock: left;\ndisplay: none;\n}\n#reset {\ndock: right;\n}\n.started {\ntext-style: bold;\nbackground: $success;\ncolor: $text;\n}\n.started TimeDisplay {\ntext-opacity: 100%;\n}\n.started #start {\ndisplay: none\n}\n.started #stop {\ndisplay: block\n}\n.started #reset {\nvisibility: hidden\n}\n

These new rules are prefixed with .started. The . indicates that .started refers to a CSS class called \"started\". The new styles will be applied only to widgets that have this CSS class.

Some of the new styles have more than one selector separated by a space. The space indicates that the rule should match the second selector if it is a child of the first. Let's look at one of these styles:

.started #start {\ndisplay: none\n}\n

The .started selector matches any widget with a \"started\" CSS class. While #start matches a child widget with an ID of \"start\". So it matches the Start button only for Stopwatches in a started state.

The rule is \"display: none\" which tells Textual to hide the button.

"},{"location":"tutorial/#manipulating-classes","title":"Manipulating classes","text":"

Modifying a widget's CSS classes is a convenient way to update visuals without introducing a lot of messy display related code.

You can add and remove CSS classes with the add_class() and remove_class() methods. We will use these methods to connect the started state to the Start / Stop buttons.

The following code will start or stop the stopwatches in response to clicking a button.

stopwatch04.py
from textual.app import App, ComposeResult\nfrom textual.containers import ScrollableContainer\nfrom textual.widgets import Button, Footer, Header, Static\nclass TimeDisplay(Static):\n\"\"\"A widget to display elapsed time.\"\"\"\nclass Stopwatch(Static):\n\"\"\"A stopwatch widget.\"\"\"\ndef on_button_pressed(self, event: Button.Pressed) -> None:\n\"\"\"Event handler called when a button is pressed.\"\"\"\nif event.button.id == \"start\":\nself.add_class(\"started\")\nelif event.button.id == \"stop\":\nself.remove_class(\"started\")\ndef compose(self) -> ComposeResult:\n\"\"\"Create child widgets of a stopwatch.\"\"\"\nyield Button(\"Start\", id=\"start\", variant=\"success\")\nyield Button(\"Stop\", id=\"stop\", variant=\"error\")\nyield Button(\"Reset\", id=\"reset\")\nyield TimeDisplay(\"00:00:00.00\")\nclass StopwatchApp(App):\n\"\"\"A Textual app to manage stopwatches.\"\"\"\nCSS_PATH = \"stopwatch04.tcss\"\nBINDINGS = [(\"d\", \"toggle_dark\", \"Toggle dark mode\")]\ndef compose(self) -> ComposeResult:\n\"\"\"Create child widgets for the app.\"\"\"\nyield Header()\nyield Footer()\nyield ScrollableContainer(Stopwatch(), Stopwatch(), Stopwatch())\ndef action_toggle_dark(self) -> None:\n\"\"\"An action to toggle dark mode.\"\"\"\nself.dark = not self.dark\nif __name__ == \"__main__\":\napp = StopwatchApp()\napp.run()\n

The on_button_pressed method is an event handler. Event handlers are methods called by Textual in response to an event such as a key press, mouse click, etc. Event handlers begin with on_ followed by the name of the event they will handle. Hence on_button_pressed will handle the button pressed event.

If you run stopwatch04.py now you will be able to toggle between the two states by clicking the first button:

stopwatch04.py \u2b58StopwatchApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a000:00:00.00\u00a0Reset\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a000:00:00.00\u00a0Reset\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a000:00:00.00\u00a0Reset\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0

"},{"location":"tutorial/#reactive-attributes","title":"Reactive attributes","text":"

A recurring theme in Textual is that you rarely need to explicitly update a widget. It is possible: you can call refresh() to display new data. However, Textual prefers to do this automatically via reactive attributes.

You can declare a reactive attribute with reactive. Let's use this feature to create a timer that displays elapsed time and keeps it updated.

stopwatch05.py
from time import monotonic\nfrom textual.app import App, ComposeResult\nfrom textual.containers import ScrollableContainer\nfrom textual.reactive import reactive\nfrom textual.widgets import Button, Footer, Header, Static\nclass TimeDisplay(Static):\n\"\"\"A widget to display elapsed time.\"\"\"\nstart_time = reactive(monotonic)\ntime = reactive(0.0)\ndef on_mount(self) -> None:\n\"\"\"Event handler called when widget is added to the app.\"\"\"\nself.set_interval(1 / 60, self.update_time)\ndef update_time(self) -> None:\n\"\"\"Method to update the time to the current time.\"\"\"\nself.time = monotonic() - self.start_time\ndef watch_time(self, time: float) -> None:\n\"\"\"Called when the time attribute changes.\"\"\"\nminutes, seconds = divmod(time, 60)\nhours, minutes = divmod(minutes, 60)\nself.update(f\"{hours:02,.0f}:{minutes:02.0f}:{seconds:05.2f}\")\nclass Stopwatch(Static):\n\"\"\"A stopwatch widget.\"\"\"\ndef on_button_pressed(self, event: Button.Pressed) -> None:\n\"\"\"Event handler called when a button is pressed.\"\"\"\nif event.button.id == \"start\":\nself.add_class(\"started\")\nelif event.button.id == \"stop\":\nself.remove_class(\"started\")\ndef compose(self) -> ComposeResult:\n\"\"\"Create child widgets of a stopwatch.\"\"\"\nyield Button(\"Start\", id=\"start\", variant=\"success\")\nyield Button(\"Stop\", id=\"stop\", variant=\"error\")\nyield Button(\"Reset\", id=\"reset\")\nyield TimeDisplay()\nclass StopwatchApp(App):\n\"\"\"A Textual app to manage stopwatches.\"\"\"\nCSS_PATH = \"stopwatch04.tcss\"\nBINDINGS = [(\"d\", \"toggle_dark\", \"Toggle dark mode\")]\ndef compose(self) -> ComposeResult:\n\"\"\"Create child widgets for the app.\"\"\"\nyield Header()\nyield Footer()\nyield ScrollableContainer(Stopwatch(), Stopwatch(), Stopwatch())\ndef action_toggle_dark(self) -> None:\n\"\"\"An action to toggle dark mode.\"\"\"\nself.dark = not self.dark\nif __name__ == \"__main__\":\napp = StopwatchApp()\napp.run()\n

We have added two reactive attributes to the TimeDisplay widget: start_time will contain the time (in seconds) the stopwatch was started, and time will contain the time to be displayed on the Stopwatch.

Both attributes will be available on self as if you had assigned them in __init__. If you write to either of these attributes the widget will update automatically.

Info

The monotonic function in this example is imported from the standard library time module. It is similar to time.time but won't go backwards if the system clock is changed.

The first argument to reactive may be a default value for the attribute or a callable that returns a default value. We set the default for start_time to the monotonic function which will be called to initialize the attribute with the current time when the TimeDisplay is added to the app. The time attribute has a simple float as the default, so self.time will be initialized to 0.

The on_mount method is an event handler called when the widget is first added to the application (or mounted in Textual terminology). In this method we call set_interval() to create a timer which calls self.update_time sixty times a second. This update_time method calculates the time elapsed since the widget started and assigns it to self.time \u2014 which brings us to one of Reactive's super-powers.

If you implement a method that begins with watch_ followed by the name of a reactive attribute, then the method will be called when the attribute is modified. Such methods are known as watch methods.

Because watch_time watches the time attribute, when we update self.time 60 times a second we also implicitly call watch_time which converts the elapsed time to a string and updates the widget with a call to self.update. Because this happens automatically, we don't need to pass in an initial argument to TimeDisplay.

The end result is that the Stopwatch widgets show the time elapsed since the widget was created:

stopwatch05.py \u2b58StopwatchApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a000:00:01.03\u00a0Reset\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a000:00:01.03\u00a0Reset\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a000:00:01.03\u00a0Reset\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0

We've seen how we can update widgets with a timer, but we still need to wire up the buttons so we can operate stopwatches independently.

"},{"location":"tutorial/#wiring-buttons","title":"Wiring buttons","text":"

We need to be able to start, stop, and reset each stopwatch independently. We can do this by adding a few more methods to the TimeDisplay class.

stopwatch06.py
from time import monotonic\nfrom textual.app import App, ComposeResult\nfrom textual.containers import ScrollableContainer\nfrom textual.reactive import reactive\nfrom textual.widgets import Button, Footer, Header, Static\nclass TimeDisplay(Static):\n\"\"\"A widget to display elapsed time.\"\"\"\nstart_time = reactive(monotonic)\ntime = reactive(0.0)\ntotal = reactive(0.0)\ndef on_mount(self) -> None:\n\"\"\"Event handler called when widget is added to the app.\"\"\"\nself.update_timer = self.set_interval(1 / 60, self.update_time, pause=True)\ndef update_time(self) -> None:\n\"\"\"Method to update time to current.\"\"\"\nself.time = self.total + (monotonic() - self.start_time)\ndef watch_time(self, time: float) -> None:\n\"\"\"Called when the time attribute changes.\"\"\"\nminutes, seconds = divmod(time, 60)\nhours, minutes = divmod(minutes, 60)\nself.update(f\"{hours:02,.0f}:{minutes:02.0f}:{seconds:05.2f}\")\ndef start(self) -> None:\n\"\"\"Method to start (or resume) time updating.\"\"\"\nself.start_time = monotonic()\nself.update_timer.resume()\ndef stop(self) -> None:\n\"\"\"Method to stop the time display updating.\"\"\"\nself.update_timer.pause()\nself.total += monotonic() - self.start_time\nself.time = self.total\ndef reset(self) -> None:\n\"\"\"Method to reset the time display to zero.\"\"\"\nself.total = 0\nself.time = 0\nclass Stopwatch(Static):\n\"\"\"A stopwatch widget.\"\"\"\ndef on_button_pressed(self, event: Button.Pressed) -> None:\n\"\"\"Event handler called when a button is pressed.\"\"\"\nbutton_id = event.button.id\ntime_display = self.query_one(TimeDisplay)\nif button_id == \"start\":\ntime_display.start()\nself.add_class(\"started\")\nelif button_id == \"stop\":\ntime_display.stop()\nself.remove_class(\"started\")\nelif button_id == \"reset\":\ntime_display.reset()\ndef compose(self) -> ComposeResult:\n\"\"\"Create child widgets of a stopwatch.\"\"\"\nyield Button(\"Start\", id=\"start\", variant=\"success\")\nyield Button(\"Stop\", id=\"stop\", variant=\"error\")\nyield Button(\"Reset\", id=\"reset\")\nyield TimeDisplay()\nclass StopwatchApp(App):\n\"\"\"A Textual app to manage stopwatches.\"\"\"\nCSS_PATH = \"stopwatch04.tcss\"\nBINDINGS = [(\"d\", \"toggle_dark\", \"Toggle dark mode\")]\ndef compose(self) -> ComposeResult:\n\"\"\"Called to add widgets to the app.\"\"\"\nyield Header()\nyield Footer()\nyield ScrollableContainer(Stopwatch(), Stopwatch(), Stopwatch())\ndef action_toggle_dark(self) -> None:\n\"\"\"An action to toggle dark mode.\"\"\"\nself.dark = not self.dark\nif __name__ == \"__main__\":\napp = StopwatchApp()\napp.run()\n

Here's a summary of the changes made to TimeDisplay.

  • We've added a total reactive attribute to store the total time elapsed between clicking the start and stop buttons.
  • The call to set_interval has grown a pause=True argument which starts the timer in pause mode (when a timer is paused it won't run until resume() is called). This is because we don't want the time to update until the user hits the start button.
  • The update_time method now adds total to the current time to account for the time between any previous clicks of the start and stop buttons.
  • We've stored the result of set_interval which returns a Timer object. We will use this later to resume the timer when we start the Stopwatch.
  • We've added start(), stop(), and reset() methods.

In addition, the on_button_pressed method on Stopwatch has grown some code to manage the time display when the user clicks a button. Let's look at that in detail:

    def on_button_pressed(self, event: Button.Pressed) -> None:\n\"\"\"Event handler called when a button is pressed.\"\"\"\nbutton_id = event.button.id\ntime_display = self.query_one(TimeDisplay)\nif button_id == \"start\":\ntime_display.start()\nself.add_class(\"started\")\nelif button_id == \"stop\":\ntime_display.stop()\nself.remove_class(\"started\")\nelif button_id == \"reset\":\ntime_display.reset()\n

This code supplies missing features and makes our app useful. We've made the following changes.

  • The first line retrieves id attribute of the button that was pressed. We can use this to decide what to do in response.
  • The second line calls query_one to get a reference to the TimeDisplay widget.
  • We call the method on TimeDisplay that matches the pressed button.
  • We add the \"started\" class when the Stopwatch is started (self.add_class(\"started\")), and remove it (self.remove_class(\"started\")) when it is stopped. This will update the Stopwatch visuals via CSS.

If you run stopwatch06.py you will be able to use the stopwatches independently.

stopwatch06.py \u2b58StopwatchApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a000:00:00.00\u00a0Reset\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Stop\u00a000:00:04.04 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a000:00:00.00\u00a0Reset\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0

The only remaining feature of the Stopwatch app left to implement is the ability to add and remove stopwatches.

"},{"location":"tutorial/#dynamic-widgets","title":"Dynamic widgets","text":"

The Stopwatch app creates widgets when it starts via the compose method. We will also need to create new widgets while the app is running, and remove widgets we no longer need. We can do this by calling mount() to add a widget, and remove() to remove a widget.

Let's use these methods to implement adding and removing stopwatches to our app.

stopwatch.py
from time import monotonic\nfrom textual.app import App, ComposeResult\nfrom textual.containers import ScrollableContainer\nfrom textual.reactive import reactive\nfrom textual.widgets import Button, Footer, Header, Static\nclass TimeDisplay(Static):\n\"\"\"A widget to display elapsed time.\"\"\"\nstart_time = reactive(monotonic)\ntime = reactive(0.0)\ntotal = reactive(0.0)\ndef on_mount(self) -> None:\n\"\"\"Event handler called when widget is added to the app.\"\"\"\nself.update_timer = self.set_interval(1 / 60, self.update_time, pause=True)\ndef update_time(self) -> None:\n\"\"\"Method to update time to current.\"\"\"\nself.time = self.total + (monotonic() - self.start_time)\ndef watch_time(self, time: float) -> None:\n\"\"\"Called when the time attribute changes.\"\"\"\nminutes, seconds = divmod(time, 60)\nhours, minutes = divmod(minutes, 60)\nself.update(f\"{hours:02,.0f}:{minutes:02.0f}:{seconds:05.2f}\")\ndef start(self) -> None:\n\"\"\"Method to start (or resume) time updating.\"\"\"\nself.start_time = monotonic()\nself.update_timer.resume()\ndef stop(self):\n\"\"\"Method to stop the time display updating.\"\"\"\nself.update_timer.pause()\nself.total += monotonic() - self.start_time\nself.time = self.total\ndef reset(self):\n\"\"\"Method to reset the time display to zero.\"\"\"\nself.total = 0\nself.time = 0\nclass Stopwatch(Static):\n\"\"\"A stopwatch widget.\"\"\"\ndef on_button_pressed(self, event: Button.Pressed) -> None:\n\"\"\"Event handler called when a button is pressed.\"\"\"\nbutton_id = event.button.id\ntime_display = self.query_one(TimeDisplay)\nif button_id == \"start\":\ntime_display.start()\nself.add_class(\"started\")\nelif button_id == \"stop\":\ntime_display.stop()\nself.remove_class(\"started\")\nelif button_id == \"reset\":\ntime_display.reset()\ndef compose(self) -> ComposeResult:\n\"\"\"Create child widgets of a stopwatch.\"\"\"\nyield Button(\"Start\", id=\"start\", variant=\"success\")\nyield Button(\"Stop\", id=\"stop\", variant=\"error\")\nyield Button(\"Reset\", id=\"reset\")\nyield TimeDisplay()\nclass StopwatchApp(App):\n\"\"\"A Textual app to manage stopwatches.\"\"\"\nCSS_PATH = \"stopwatch.tcss\"\nBINDINGS = [\n(\"d\", \"toggle_dark\", \"Toggle dark mode\"),\n(\"a\", \"add_stopwatch\", \"Add\"),\n(\"r\", \"remove_stopwatch\", \"Remove\"),\n]\ndef compose(self) -> ComposeResult:\n\"\"\"Called to add widgets to the app.\"\"\"\nyield Header()\nyield Footer()\nyield ScrollableContainer(Stopwatch(), Stopwatch(), Stopwatch(), id=\"timers\")\ndef action_add_stopwatch(self) -> None:\n\"\"\"An action to add a timer.\"\"\"\nnew_stopwatch = Stopwatch()\nself.query_one(\"#timers\").mount(new_stopwatch)\nnew_stopwatch.scroll_visible()\ndef action_remove_stopwatch(self) -> None:\n\"\"\"Called to remove a timer.\"\"\"\ntimers = self.query(\"Stopwatch\")\nif timers:\ntimers.last().remove()\ndef action_toggle_dark(self) -> None:\n\"\"\"An action to toggle dark mode.\"\"\"\nself.dark = not self.dark\nif __name__ == \"__main__\":\napp = StopwatchApp()\napp.run()\n

Here's a summary of the changes:

  • The ScrollableContainer object in StopWatchApp grew a \"timers\" ID.
  • Added action_add_stopwatch to add a new stopwatch.
  • Added action_remove_stopwatch to remove a stopwatch.
  • Added keybindings for the actions.

The action_add_stopwatch method creates and mounts a new stopwatch. Note the call to query_one() with a CSS selector of \"#timers\" which gets the timer's container via its ID. Once mounted, the new Stopwatch will appear in the terminal. That last line in action_add_stopwatch calls scroll_visible() which will scroll the container to make the new Stopwatch visible (if required).

The action_remove_stopwatch function calls query() with a CSS selector of \"Stopwatch\" which gets all the Stopwatch widgets. If there are stopwatches then the action calls last() to get the last stopwatch, and remove() to remove it.

If you run stopwatch.py now you can add a new stopwatch with the A key and remove a stopwatch with R.

stopwatch.py \u2b58StopwatchApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a000:00:00.00\u00a0Reset\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a000:00:00.00\u00a0Reset\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a000:00:00.00\u00a0Reset\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a000:00:00.00\u00a0Reset\u00a0 \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0\u00a0A\u00a0\u00a0Add\u00a0\u00a0R\u00a0\u00a0Remove\u00a0

"},{"location":"tutorial/#what-next","title":"What next?","text":"

Congratulations on building your first Textual application! This tutorial has covered a lot of ground. If you are the type that prefers to learn a framework by coding, feel free. You could tweak stopwatch.py or look through the examples.

Read the guide for the full details on how to build sophisticated TUI applications with Textual.

"},{"location":"widget_gallery/","title":"Widgets","text":"

Welcome to the Textual widget gallery.

We have many more widgets planned, or you can build your own.

Info

Textual is a TUI framework. Everything below runs in the terminal.

"},{"location":"widget_gallery/#button","title":"Button","text":"

A simple button with a variety of semantic styles.

Button reference

ButtonsApp Standard\u00a0ButtonsDisabled\u00a0Buttons \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Default\u00a0\u00a0Default\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Primary!\u00a0\u00a0Primary!\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Success!\u00a0\u00a0Success!\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Warning!\u00a0\u00a0Warning!\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Error!\u00a0\u00a0Error!\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

"},{"location":"widget_gallery/#checkbox","title":"Checkbox","text":"

A classic checkbox control.

Checkbox reference

CheckboxApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258c\u00a0Arrakis\u00a0\ud83d\ude13\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258c\u00a0Caladan\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258c\u00a0Chusuk\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258cGiedi\u00a0Prime\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258cGinaz\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258c\u00a0Grumman\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2583\u2583 \u258a\u2590X\u258cKaitain\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e

"},{"location":"widget_gallery/#collapsible","title":"Collapsible","text":"

Content that may be toggled on and off by clicking a title.

Collapsible reference

CollapsibleApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25bc\u00a0Leto #\u00a0Duke\u00a0Leto\u00a0I\u00a0Atreides Head\u00a0of\u00a0House\u00a0Atreides. \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25bc\u00a0Jessica \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eLady\u00a0Jessica\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Bene\u00a0Gesserit\u00a0and\u00a0concubine\u00a0of\u00a0Leto,\u00a0and\u00a0mother\u00a0of\u00a0Paul\u00a0and\u00a0Alia. \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25b6\u00a0Paul \u00a0C\u00a0\u00a0Collapse\u00a0All\u00a0\u00a0E\u00a0\u00a0Expand\u00a0All\u00a0

"},{"location":"widget_gallery/#contentswitcher","title":"ContentSwitcher","text":"

A widget for containing and switching display between multiple child widgets.

ContentSwitcher reference

"},{"location":"widget_gallery/#datatable","title":"DataTable","text":"

A powerful data table, with configurable cursors.

DataTable reference

TableApp \u00a0lane\u00a0\u00a0swimmer\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0country\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0time\u00a0\u00a0 \u00a04\u00a0\u00a0\u00a0\u00a0\u00a0Joseph\u00a0Schooling\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Singapore\u00a0\u00a0\u00a0\u00a0\u00a0\u00a050.39\u00a0 \u00a02\u00a0\u00a0\u00a0\u00a0\u00a0Michael\u00a0Phelps\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.14\u00a0 \u00a05\u00a0\u00a0\u00a0\u00a0\u00a0Chad\u00a0le\u00a0Clos\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0South\u00a0Africa\u00a0\u00a0\u00a051.14\u00a0 \u00a06\u00a0\u00a0\u00a0\u00a0\u00a0L\u00e1szl\u00f3\u00a0Cseh\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Hungary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.14\u00a0 \u00a03\u00a0\u00a0\u00a0\u00a0\u00a0Li\u00a0Zhuhao\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0China\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.26\u00a0 \u00a08\u00a0\u00a0\u00a0\u00a0\u00a0Mehdy\u00a0Metella\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0France\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.58\u00a0 \u00a07\u00a0\u00a0\u00a0\u00a0\u00a0Tom\u00a0Shields\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.73\u00a0 \u00a01\u00a0\u00a0\u00a0\u00a0\u00a0Aleksandr\u00a0Sadovnikov\u00a0\u00a0Russia\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0 \u00a010\u00a0\u00a0\u00a0\u00a0Darren\u00a0Burns\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Scotland\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0

"},{"location":"widget_gallery/#digits","title":"Digits","text":"

Display numbers in tall characters.

Digits reference

DigitApp \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557 \u2551\u257a\u2501\u2513\u00a0\u00a0\u2513\u00a0\u257b\u00a0\u257b\u00a0\u2513\u00a0\u00a0\u250f\u2501\u2578\u250f\u2501\u2513\u257a\u2501\u2513\u00a0\u250f\u2501\u2578\u250f\u2501\u2578\u257a\u2501\u2513\u00a0\u250f\u2501\u2578\u250f\u2501\u2513\u250f\u2501\u2513\u257a\u2501\u2513\u2551 \u2551\u00a0\u2501\u252b\u00a0\u00a0\u2503\u00a0\u2517\u2501\u252b\u00a0\u2503\u00a0\u00a0\u2517\u2501\u2513\u2517\u2501\u252b\u250f\u2501\u251b\u00a0\u2523\u2501\u2513\u2517\u2501\u2513\u00a0\u2501\u252b\u00a0\u2517\u2501\u2513\u2523\u2501\u252b\u2517\u2501\u252b\u00a0\u00a0\u2503\u2551 \u2551\u257a\u2501\u251b.\u257a\u253b\u2578\u00a0\u00a0\u2579\u257a\u253b\u2578,\u257a\u2501\u251b\u257a\u2501\u251b\u2517\u2501\u2578,\u2517\u2501\u251b\u257a\u2501\u251b\u257a\u2501\u251b,\u257a\u2501\u251b\u2517\u2501\u251b\u257a\u2501\u251b\u00a0\u00a0\u2579\u2551 \u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d

"},{"location":"widget_gallery/#directorytree","title":"DirectoryTree","text":"

A tree view of files and folders.

DirectoryTree reference

DirectoryTreeApp \ud83d\udcc2\u00a0. \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0.cache \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0.faq \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0.git \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0.github \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0.ipynb_checkpoints \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0.mypy_cache \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0.pytest_cache \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0.screenshot_cache \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0.vscode \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0__pycache__ \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0dist\u2583\u2583 \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0docs \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0examples \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0imgs \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0notes \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0questions \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0reference \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0sandbox \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0site \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0src \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0tests \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0tools \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0tree-sitter

"},{"location":"widget_gallery/#footer","title":"Footer","text":"

A footer to display and interact with key bindings.

Footer reference

FooterApp \u00a0Q\u00a0\u00a0Quit\u00a0the\u00a0app\u00a0\u00a0?\u00a0\u00a0Show\u00a0help\u00a0screen\u00a0\u00a0DELETE\u00a0\u00a0Delete\u00a0the\u00a0thing\u00a0

"},{"location":"widget_gallery/#header","title":"Header","text":"

A header to display the app's title and subtitle.

Header reference

HeaderApp \u2b58HeaderApp

"},{"location":"widget_gallery/#input","title":"Input","text":"

A control to enter text.

Input reference

InputApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aDarren\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aLast\u00a0Name\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

"},{"location":"widget_gallery/#label","title":"Label","text":"

A simple text label.

Label reference

"},{"location":"widget_gallery/#listview","title":"ListView","text":"

Display a list of items (items may be other widgets).

ListView reference

ListViewExample One Two Three

"},{"location":"widget_gallery/#loadingindicator","title":"LoadingIndicator","text":"

Display an animation while data is loading.

LoadingIndicator reference

LoadingApp \u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf

"},{"location":"widget_gallery/#log","title":"Log","text":"

Display and update lines of text (such as from a file).

Log reference

LogApp And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain. I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain. I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\u2584\u2584 I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.

"},{"location":"widget_gallery/#markdownviewer","title":"MarkdownViewer","text":"

Display and interact with a Markdown document (adds a table of contents and browser-like navigation to Markdown).

MarkdownViewer reference

MarkdownExampleApp \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u25bc\u00a0\u2160\u00a0Markdown\u00a0Viewer\u258a\u258e\u258a \u251c\u2500\u2500\u00a0\u2161\u00a0Features\u258a\u258eMarkdown\u00a0Viewer\u258a \u251c\u2500\u2500\u00a0\u2161\u00a0Tables\u258a\u258e\u258a \u2514\u2500\u2500\u00a0\u2161\u00a0Code\u00a0Blocks\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258aThis\u00a0is\u00a0an\u00a0example\u00a0of\u00a0Textual's\u00a0MarkdownViewer\u00a0widget. \u258a \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258a\u258e\u258a \u258a\u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Features\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258e\u258a \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258aMarkdown\u00a0syntax\u00a0and\u00a0extensions\u00a0are\u00a0supported. \u258a \u258a\u25cf\u00a0Typography\u00a0emphasis,\u00a0strong,\u00a0inline\u00a0code\u00a0etc. \u258a\u25cf\u00a0Headers \u258a\u25cf\u00a0Lists\u00a0(bullet\u00a0and\u00a0ordered) \u258a\u25cf\u00a0Syntax\u00a0highlighted\u00a0code\u00a0blocks \u258a\u25cf\u00a0Tables! \u258a \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258a\u258e\u258a \u258a\u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Tables\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258e\u258a \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258aTables\u00a0are\u00a0displayed\u00a0in\u00a0a\u00a0DataTable\u00a0widget. \u258a \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258a\u258e\u258a \u258a\u258eName\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0TypeDefaultDescription\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258e\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0\u258a \u258a\u258eshow_headerboolTrueShow\u00a0the\u00a0table\u00a0header\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258efixed_rowsint0Number\u00a0of\u00a0fixed\u00a0rows\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258efixed_columnsint0Number\u00a0of\u00a0fixed\u00a0columns\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258ezebra_stripesboolFalseDisplay\u00a0alternating\u00a0colors\u00a0on\u00a0rows\u258a \u258a\u258eheader_heightint1Height\u00a0of\u00a0header\u00a0row\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258eshow_cursorboolTrueShow\u00a0a\u00a0cell\u00a0cursor\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258e\u258a \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258a \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258a\u258e\u258a

"},{"location":"widget_gallery/#markdown","title":"Markdown","text":"

Display a markdown document.

Markdown reference

MarkdownExampleApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eMarkdown\u00a0Document\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 This\u00a0is\u00a0an\u00a0example\u00a0of\u00a0Textual's\u00a0Markdown\u00a0widget. \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Features\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Markdown\u00a0syntax\u00a0and\u00a0extensions\u00a0are\u00a0supported. \u25cf\u00a0Typography\u00a0emphasis,\u00a0strong,\u00a0inline\u00a0code\u00a0etc. \u25cf\u00a0Headers \u25cf\u00a0Lists\u00a0(bullet\u00a0and\u00a0ordered) \u25cf\u00a0Syntax\u00a0highlighted\u00a0code\u00a0blocks \u25cf\u00a0Tables!

"},{"location":"widget_gallery/#optionlist","title":"OptionList","text":"

Display a vertical list of options (options may be Rich renderables).

OptionList reference

OptionListApp \u2b58OptionListApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aAerilon\u258e \u258aAquaria\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aCanceron\u258e \u258aCaprica\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aGemenon\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aLeonis\u258e \u258aLibran\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aPicon\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aSagittaron\u2584\u2584\u258e \u258aScorpia\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

"},{"location":"widget_gallery/#placeholder","title":"Placeholder","text":"

Display placeholder content while you are designing a UI.

Placeholder reference

PlaceholderApp Placeholder\u00a0p2\u00a0here! This\u00a0is\u00a0a\u00a0custom\u00a0label\u00a0for\u00a0p1. #p4 #p3#p5Placeholde r Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0 26\u00a0x\u00a06amet,\u00a0consectetur\u00a027\u00a0x\u00a06 adipiscing\u00a0elit.\u00a0Etiam\u00a0 feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0 Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0 consectetur\u00a0adipiscing\u00a0elit.\u00a0Etiam\u00a040\u00a0x\u00a06 feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0accumsan.\u00a0 Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis\u00a0 gravida.\u00a0Phasellus\u00a0id\u00a0eleifend\u00a0ligula. Nullam\u00a0imperdiet\u00a0sem\u00a0tellus,\u00a0sed\u00a0 vehicula\u00a0nisl\u00a0faucibus\u00a0sit\u00a0amet.\u00a0Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0 Praesent\u00a0iaculis\u00a0tempor\u00a0ultricies.\u00a0Sedconsectetur\u00a0adipiscing\u00a0elit.\u00a0Etiam\u00a0 lacinia,\u00a0tellus\u00a0id\u00a0rutrum\u00a0lacinia,\u00a0feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0accumsan.\u00a0 sapien\u00a0sapien\u00a0congue\u00a0mauris,\u00a0sit\u00a0amet\u00a0Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis\u00a0

"},{"location":"widget_gallery/#pretty","title":"Pretty","text":"

Display a pretty-formatted Rich renderable.

Pretty reference

PrettyExample { 'title':\u00a0'Back\u00a0to\u00a0the\u00a0Future', 'releaseYear':\u00a01985, 'director':\u00a0'Robert\u00a0Zemeckis', 'genre':\u00a0'Adventure,\u00a0Comedy,\u00a0Sci-Fi', 'cast':\u00a0[ {'actor':\u00a0'Michael\u00a0J.\u00a0Fox',\u00a0'character':\u00a0'Marty\u00a0McFly'}, {'actor':\u00a0'Christopher\u00a0Lloyd',\u00a0'character':\u00a0'Dr.\u00a0Emmett\u00a0Brown'} ] }

"},{"location":"widget_gallery/#progressbar","title":"ProgressBar","text":"

A configurable progress bar with ETA and percentage complete.

ProgressBar reference

Funding\u00a0tracking \u2b58Funding\u00a0tracking Funding:\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u250150% \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258a$$$\u258e\u00a0Donate\u00a0 \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 Donation\u00a0for\u00a0$50\u00a0received!

"},{"location":"widget_gallery/#radiobutton","title":"RadioButton","text":"

A simple radio button.

RadioButton reference

RadioChoicesApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590\u25cf\u258cBattlestar\u00a0Galactica\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a01984\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a02021\u258e \u258a\u2590\u25cf\u258c\u00a0Serenity\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Trek:\u00a0The\u00a0Motion\u00a0Picture\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Wars:\u00a0A\u00a0New\u00a0Hope\u258e \u258a\u2590\u25cf\u258c\u00a0The\u00a0Last\u00a0Starfighter\u258e \u258a\u2590\u25cf\u258c\u00a0Total\u00a0Recall\u00a0\ud83d\udc49\u00a0\ud83d\udd34\u258e \u258a\u2590\u25cf\u258c\u00a0Wing\u00a0Commander\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

"},{"location":"widget_gallery/#radioset","title":"RadioSet","text":"

A collection of radio buttons, that enforces uniqueness.

RadioSet reference

RadioChoicesApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590\u25cf\u258cBattlestar\u00a0Galactica\u258e\u258a\u2590\u25cf\u258c\u00a0Amanda\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a01984\u258e\u258a\u2590\u25cf\u258c\u00a0Connor\u00a0MacLeod\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a02021\u258e\u258a\u2590\u25cf\u258c\u00a0Duncan\u00a0MacLeod\u258e \u258a\u2590\u25cf\u258c\u00a0Serenity\u258e\u258a\u2590\u25cf\u258c\u00a0Heather\u00a0MacLeod\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Trek:\u00a0The\u00a0Motion\u00a0Picture\u258e\u258a\u2590\u25cf\u258c\u00a0Joe\u00a0Dawson\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Wars:\u00a0A\u00a0New\u00a0Hope\u258e\u258a\u2590\u25cf\u258c\u00a0Kurgan,\u00a0The\u258e \u258a\u2590\u25cf\u258c\u00a0The\u00a0Last\u00a0Starfighter\u258e\u258a\u2590\u25cf\u258c\u00a0Methos\u258e \u258a\u2590\u25cf\u258c\u00a0Total\u00a0Recall\u00a0\ud83d\udc49\u00a0\ud83d\udd34\u258e\u258a\u2590\u25cf\u258c\u00a0Rachel\u00a0Ellenstein\u258e \u258a\u2590\u25cf\u258c\u00a0Wing\u00a0Commander\u258e\u258a\u2590\u25cf\u258c\u00a0Ram\u00edrez\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

"},{"location":"widget_gallery/#richlog","title":"RichLog","text":"

Display and update text in a scrolling panel.

RichLog reference

RichLogApp \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0previous_value=next(iter_values) \u2502\u00a0\u00a0\u00a0exceptStopIteration: \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0return \u2502\u00a0\u00a0\u00a0first=True\u2585\u2585 \u2502\u00a0\u00a0\u00a0forvalueiniter_values: \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0yieldfirst,False,previous_value \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0first=False \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0previous_value=value \u2502\u00a0\u00a0\u00a0yieldfirst,True,previous_value \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503lane\u2503swimmer\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503country\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503time\u00a0\u2503 \u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529 \u25024\u00a0\u00a0\u00a0\u2502Joseph\u00a0Schooling\u00a0\u00a0\u00a0\u00a0\u2502Singapore\u00a0\u00a0\u00a0\u00a0\u250250.39\u2502 \u25022\u00a0\u00a0\u00a0\u2502Michael\u00a0Phelps\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502United\u00a0States\u250251.14\u2502 \u25025\u00a0\u00a0\u00a0\u2502Chad\u00a0le\u00a0Clos\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502South\u00a0Africa\u00a0\u250251.14\u2502 \u25026\u00a0\u00a0\u00a0\u2502L\u00e1szl\u00f3\u00a0Cseh\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502Hungary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250251.14\u2502 \u25023\u00a0\u00a0\u00a0\u2502Li\u00a0Zhuhao\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502China\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250251.26\u2502 \u25028\u00a0\u00a0\u00a0\u2502Mehdy\u00a0Metella\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502France\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250251.58\u2502 \u25027\u00a0\u00a0\u00a0\u2502Tom\u00a0Shields\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502United\u00a0States\u250251.73\u2502 \u25021\u00a0\u00a0\u00a0\u2502Aleksandr\u00a0Sadovnikov\u2502Russia\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250251.84\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 Write\u00a0text\u00a0or\u00a0any\u00a0Rich\u00a0renderable! Key(key='H',\u00a0character='H',\u00a0name='upper_h',\u00a0is_printable=True) Key(key='i',\u00a0character='i',\u00a0name='i',\u00a0is_printable=True)

"},{"location":"widget_gallery/#rule","title":"Rule","text":"

A rule widget to separate content, similar to a <hr> HTML tag.

Rule reference

HorizontalRulesApp \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0solid\u00a0(default)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0heavy\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0thick\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0dashed\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0double\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0ascii\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 ----------------------------------------------------------------

"},{"location":"widget_gallery/#select","title":"Select","text":"

Select from a number of possible options.

Select reference

SelectApp \u2b58SelectApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aSelect\u25b2\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aSelect\u258e \u258aI\u00a0must\u00a0not\u00a0fear.\u258e \u258aFear\u00a0is\u00a0the\u00a0mind-killer.\u258e \u258aFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0\u258e \u258aobliteration.\u258e \u258aI\u00a0will\u00a0face\u00a0my\u00a0fear.\u258e \u258aI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

"},{"location":"widget_gallery/#selectionlist","title":"SelectionList","text":"

Select multiple values from a list of options.

SelectionList reference

SelectionListApp \u2b58SelectionListApp \u250c\u2500\u00a0Shall\u00a0we\u00a0play\u00a0some\u00a0games?\u00a0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502 \u2502\u2590X\u258cFalken's\u00a0Maze\u2502 \u2502\u2590X\u258cBlack\u00a0Jack\u2502 \u2502\u2590X\u258cGin\u00a0Rummy\u2502 \u2502\u2590X\u258cHearts\u2502 \u2502\u2590X\u258cBridge\u2502 \u2502\u2590X\u258cCheckers\u2502 \u2502\u2590X\u258cChess\u2502 \u2502\u2590X\u258cPoker\u2502 \u2502\u2590X\u258cFighter\u00a0Combat\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

"},{"location":"widget_gallery/#sparkline","title":"Sparkline","text":"

Display numerical data.

Sparkline reference

SparklineSummaryFunctionApp \u2582\u2584\u2582\u2584\u2583\u2583\u2586\u2585\u2583\u2582\u2583\u2582\u2583\u2582\u2584\u2587\u2583\u2583\u2587\u2585\u2584\u2583\u2584\u2584\u2583\u2582\u2583\u2582\u2583\u2584\u2584\u2588\u2586\u2582\u2583\u2583\u2585\u2583\u2583\u2584\u2583\u2587\u2583\u2583\u2583\u2584\u2584\u2586\u2583\u2583\u2585\u2582\u2585\u2583\u2584\u2583\u2583\u2584\u2583\u2585\u2586\u2582\u2582\u2583\u2586\u2582\u2583\u2584\u2585\u2584\u2583\u2584\u2584\u2581\u2583\u2582 \u2581\u2582\u2581\u2582\u2581\u2581\u2582\u2582\u2581\u2581\u2581\u2581\u2582\u2581\u2581\u2582\u2581\u2581\u2582\u2582\u2582\u2582\u2582\u2582\u2581\u2581\u2581\u2581\u2581\u2582\u2581\u2582\u2582\u2581\u2581\u2581\u2582\u2581\u2581\u2582\u2581\u2582\u2581\u2581\u2581\u2581\u2582\u2582\u2581\u2581\u2581\u2581\u2582\u2581\u2581\u2582\u2581\u2582\u2581\u2581\u2582\u2581\u2581\u2581\u2581\u2581\u2581\u2582\u2582\u2582\u2581\u2582\u2581\u2581\u2581\u2581 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

"},{"location":"widget_gallery/#static","title":"Static","text":"

Displays simple static content. Typically used as a base class.

Static reference

"},{"location":"widget_gallery/#switch","title":"Switch","text":"

An on / off control, inspired by toggle buttons.

Switch reference

SwitchApp Example\u00a0switches \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e off:\u00a0\u00a0\u00a0\u00a0\u00a0\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e on:\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e focused:\u00a0\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e custom:\u00a0\u00a0\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

"},{"location":"widget_gallery/#tabs","title":"Tabs","text":"

A row of tabs you can select with the mouse or navigate with keys.

Tabs reference

TabsApp \u00a0AtreidiesDuke\u00a0Leto\u00a0AtreidesLady\u00a0JessicaGurney\u00a0HalleckBaron\u00a0Vladimir \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258aLady\u00a0Jessica\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u00a0A\u00a0\u00a0Add\u00a0tab\u00a0\u00a0R\u00a0\u00a0Remove\u00a0active\u00a0tab\u00a0\u00a0C\u00a0\u00a0Clear\u00a0tabs\u00a0

"},{"location":"widget_gallery/#tabbedcontent","title":"TabbedContent","text":"

A Combination of Tabs and ContentSwitcher to navigate static content.

TabbedContent reference

TabbedApp LetoJessicaPaul \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eLady\u00a0Jessica\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Bene\u00a0Gesserit\u00a0and\u00a0concubine\u00a0of\u00a0Leto,\u00a0and\u00a0mother\u00a0of\u00a0Paul\u00a0and\u00a0Alia. PaulAlia \u2501\u2578\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 First\u00a0child \u00a0L\u00a0\u00a0Leto\u00a0\u00a0J\u00a0\u00a0Jessica\u00a0\u00a0P\u00a0\u00a0Paul\u00a0

"},{"location":"widget_gallery/#textarea","title":"TextArea","text":"

A multi-line text area which supports syntax highlighting various languages.

TextArea reference

TextAreaExample 1\u00a0\u00a0defhello(name):\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 2\u00a0\u00a0print(\"hello\"+\u00a0name)\u00a0\u00a0\u00a0 3\u00a0\u00a0 4\u00a0\u00a0defgoodbye(name):\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 5\u00a0\u00a0print(\"goodbye\"+\u00a0name)\u00a0 6\u00a0\u00a0

"},{"location":"widget_gallery/#tree","title":"Tree","text":"

A tree control with expandable nodes.

Tree reference

TreeApp \u25bc\u00a0Dune \u2517\u2501\u2501\u00a0\u25bc\u00a0Characters \u2523\u2501\u2501\u00a0Paul \u2523\u2501\u2501\u00a0Jessica \u2517\u2501\u2501\u00a0Chani

"},{"location":"api/","title":"API","text":"

This is a API-level reference to the Textual API. Click the links to your left (or in the burger menu) to open a reference for each module.

If you are new to Textual, you may want to read the tutorial or guide first.

"},{"location":"api/app/","title":"App","text":"

Here you will find the App class, which is the base class for Textual apps.

See app basics for how to build Textual apps.

"},{"location":"api/app/#textual.app.AutopilotCallbackType","title":"AutopilotCallbackType module-attribute","text":"
AutopilotCallbackType: TypeAlias = (\n\"Callable[[Pilot[object]], Coroutine[Any, Any, None]]\"\n)\n

Signature for valid callbacks that can be used to control apps.

"},{"location":"api/app/#textual.app.CSSPathType","title":"CSSPathType module-attribute","text":"
CSSPathType = Union[\nstr, PurePath, List[Union[str, PurePath]]\n]\n

Valid ways of specifying paths to CSS files.

"},{"location":"api/app/#textual.app.ActionError","title":"ActionError class","text":"

Bases: Exception

Base class for exceptions relating to actions.

"},{"location":"api/app/#textual.app.ActiveModeError","title":"ActiveModeError class","text":"

Bases: ModeError

Raised when attempting to remove the currently active mode.

"},{"location":"api/app/#textual.app.App","title":"App class","text":"
def __init__(\nself, driver_class=None, css_path=None, watch_css=False\n):\n

Bases: Generic[ReturnType], DOMNode

The base class for Textual Applications.

Parameters Name Type Description Default driver_class Type[Driver] | None

Driver class or None to auto-detect. This will be used by some Textual tools.

None css_path CSSPathType | None

Path to CSS or None to use the CSS_PATH class variable. To load multiple CSS files, pass a list of strings or paths which will be loaded in order.

None watch_css bool

Reload CSS if the files changed. This is set automatically if you are using textual run with the dev switch.

False Raises Type Description CssPathError

When the supplied CSS path(s) are an unexpected type.

"},{"location":"api/app/#textual.app.App.AUTO_FOCUS","title":"AUTO_FOCUS class-attribute","text":"
AUTO_FOCUS: str | None = '*'\n

A selector to determine what to focus automatically when a screen is activated.

The widget focused is the first that matches the given CSS selector. Setting to None or \"\" disables auto focus.

"},{"location":"api/app/#textual.app.App.COMMANDS","title":"COMMANDS class-attribute","text":"
COMMANDS: set[type[Provider]] = {SystemCommands}\n

Command providers used by the command palette.

Should be a set of command.Provider classes.

"},{"location":"api/app/#textual.app.App.CSS","title":"CSS class-attribute","text":"
CSS: str = ''\n

Inline CSS, useful for quick scripts. This is loaded after CSS_PATH, and therefore takes priority in the event of a specificity clash.

"},{"location":"api/app/#textual.app.App.CSS_PATH","title":"CSS_PATH class-attribute","text":"
CSS_PATH: CSSPathType | None = None\n

File paths to load CSS from.

"},{"location":"api/app/#textual.app.App.ENABLE_COMMAND_PALETTE","title":"ENABLE_COMMAND_PALETTE class-attribute","text":"
ENABLE_COMMAND_PALETTE: bool = True\n

Should the command palette be enabled for the application?

"},{"location":"api/app/#textual.app.App.MODES","title":"MODES class-attribute","text":"
MODES: dict[str, str | Screen | Callable[[], Screen]] = {}\n

Modes associated with the app and their base screens.

The base screen is the screen at the bottom of the mode stack. You can think of it as the default screen for that stack. The base screens can be names of screens listed in SCREENS, Screen instances, or callables that return screens.

Example
class HelpScreen(Screen[None]):\n...\nclass MainAppScreen(Screen[None]):\n...\nclass MyApp(App[None]):\nMODES = {\n\"default\": \"main\",\n\"help\": HelpScreen,\n}\nSCREENS = {\n\"main\": MainAppScreen,\n}\n...\n
"},{"location":"api/app/#textual.app.App.SCREENS","title":"SCREENS class-attribute","text":"
SCREENS: dict[str, Screen | Callable[[], Screen]] = {}\n

Screens associated with the app for the lifetime of the app.

"},{"location":"api/app/#textual.app.App.SUB_TITLE","title":"SUB_TITLE class-attribute instance-attribute","text":"
SUB_TITLE: str | None = None\n

A class variable to set the default sub-title for the application.

To update the sub-title while the app is running, you can set the sub_title attribute. See also the Screen.SUB_TITLE attribute.

"},{"location":"api/app/#textual.app.App.TITLE","title":"TITLE class-attribute instance-attribute","text":"
TITLE: str | None = None\n

A class variable to set the default title for the application.

To update the title while the app is running, you can set the title attribute. See also the Screen.TITLE attribute.

"},{"location":"api/app/#textual.app.App.children","title":"children property","text":"
children: Sequence['Widget']\n

A view onto the app's immediate children.

This attribute exists on all widgets. In the case of the App, it will only ever contain a single child, which will be the currently active screen.

Returns Type Description Sequence['Widget']

A sequence of widgets.

"},{"location":"api/app/#textual.app.App.current_mode","title":"current_mode property","text":"
current_mode: str\n

The name of the currently active mode.

"},{"location":"api/app/#textual.app.App.dark","title":"dark class-attribute instance-attribute","text":"
dark: Reactive[bool] = Reactive(True, compute=False)\n

Use a dark theme if True, otherwise use a light theme.

Modify this attribute to switch between light and dark themes.

Example
self.app.dark = not self.app.dark  # Toggle dark mode\n
"},{"location":"api/app/#textual.app.App.debug","title":"debug property","text":"
debug: bool\n

Is debug mode enabled?

"},{"location":"api/app/#textual.app.App.focused","title":"focused property","text":"
focused: Widget | None\n

The widget that is focused on the currently active screen, or None.

Focused widgets receive keyboard input.

Returns Type Description Widget | None

The currently focused widget, or None if nothing is focused.

"},{"location":"api/app/#textual.app.App.is_headless","title":"is_headless property","text":"
is_headless: bool\n

Is the driver running in 'headless' mode?

Headless mode is used when running tests with run_test.

"},{"location":"api/app/#textual.app.App.log","title":"log property","text":"
log: Logger\n

The textual logger.

Example
self.log(\"Hello, World!\")\nself.log(self.tree)\n
Returns Type Description Logger

A Textual logger.

"},{"location":"api/app/#textual.app.App.namespace_bindings","title":"namespace_bindings property","text":"
namespace_bindings: dict[str, tuple[DOMNode, Binding]]\n

Get currently active bindings.

If no widget is focused, then app-level bindings are returned. If a widget is focused, then any bindings present in the active screen and app are merged and returned.

This property may be used to inspect current bindings.

Returns Type Description dict[str, tuple[DOMNode, Binding]]

A mapping of keys onto pairs of nodes and bindings.

"},{"location":"api/app/#textual.app.App.return_code","title":"return_code property","text":"
return_code: int | None\n

The return code with which the app exited.

Non-zero codes indicate errors. A value of 1 means the app exited with a fatal error. If the app wasn't exited yet, this will be None.

Example

The return code can be used to exit the process via sys.exit.

my_app.run()\nsys.exit(my_app.return_code)\n

"},{"location":"api/app/#textual.app.App.return_value","title":"return_value property","text":"
return_value: ReturnType | None\n

The return value of the app, or None if it has not yet been set.

The return value is set when calling exit.

"},{"location":"api/app/#textual.app.App.screen","title":"screen property","text":"
screen: Screen[object]\n

The current active screen.

Returns Type Description Screen[object]

The currently active (visible) screen.

Raises Type Description ScreenStackError

If there are no screens on the stack.

"},{"location":"api/app/#textual.app.App.screen_stack","title":"screen_stack property","text":"
screen_stack: Sequence[Screen]\n

A snapshot of the current screen stack.

Returns Type Description Sequence[Screen]

A snapshot of the current state of the screen stack.

"},{"location":"api/app/#textual.app.App.scroll_sensitivity_x","title":"scroll_sensitivity_x instance-attribute","text":"
scroll_sensitivity_x: float = 4.0\n

Number of columns to scroll in the X direction with wheel or trackpad.

"},{"location":"api/app/#textual.app.App.scroll_sensitivity_y","title":"scroll_sensitivity_y instance-attribute","text":"
scroll_sensitivity_y: float = 2.0\n

Number of lines to scroll in the Y direction with wheel or trackpad.

"},{"location":"api/app/#textual.app.App.size","title":"size property","text":"
size: Size\n

The size of the terminal.

Returns Type Description Size

Size of the terminal.

"},{"location":"api/app/#textual.app.App.sub_title","title":"sub_title class-attribute instance-attribute","text":"
sub_title: Reactive[str] = (\nself.SUB_TITLE if self.SUB_TITLE is not None else \"\"\n)\n

The sub-title for the application.

The initial value for sub_title will be set to the SUB_TITLE class variable if it exists, or an empty string if it doesn't.

Sub-titles are typically used to show the high-level state of the app, such as the current mode, or path to the file being worked on.

Assign a new value to this attribute to change the sub-title. The new value is always converted to string.

"},{"location":"api/app/#textual.app.App.title","title":"title class-attribute instance-attribute","text":"
title: Reactive[str] = (\nself.TITLE\nif self.TITLE is not None\nelse f\"{self.__class__.__name__}\"\n)\n

The title for the application.

The initial value for title will be set to the TITLE class variable if it exists, or the name of the app if it doesn't.

Assign a new value to this attribute to change the title. The new value is always converted to string.

"},{"location":"api/app/#textual.app.App.use_command_palette","title":"use_command_palette instance-attribute","text":"
use_command_palette: bool = self.ENABLE_COMMAND_PALETTE\n

A flag to say if the application should use the command palette.

If set to False any call to action_command_palette will be ignored.

"},{"location":"api/app/#textual.app.App.workers","title":"workers property","text":"
workers: WorkerManager\n

The worker manager.

Returns Type Description WorkerManager

An object to manage workers.

"},{"location":"api/app/#textual.app.App.action_add_class","title":"action_add_class async","text":"
def action_add_class(self, selector, class_name):\n

An action to add a CSS class to the selected widget.

Parameters Name Type Description Default selector str

Selects the widget to add the class to.

required class_name str

The class to add to the selected widget.

required"},{"location":"api/app/#textual.app.App.action_back","title":"action_back async","text":"
def action_back(self):\n

An action to go back to the previous screen (pop the current screen).

Note

If there is no screen to go back to, this is a non-operation (in other words it's safe to call even if there are no other screens on the stack.)

"},{"location":"api/app/#textual.app.App.action_bell","title":"action_bell async","text":"
def action_bell(self):\n

An action to play the terminal 'bell'.

"},{"location":"api/app/#textual.app.App.action_check_bindings","title":"action_check_bindings async","text":"
def action_check_bindings(self, key):\n

An action to handle a key press using the binding system.

Parameters Name Type Description Default key str

The key to process.

required"},{"location":"api/app/#textual.app.App.action_command_palette","title":"action_command_palette method","text":"
def action_command_palette(self):\n

Show the Textual command palette.

"},{"location":"api/app/#textual.app.App.action_focus","title":"action_focus async","text":"
def action_focus(self, widget_id):\n

An action to focus the given widget.

Parameters Name Type Description Default widget_id str

ID of widget to focus.

required"},{"location":"api/app/#textual.app.App.action_focus_next","title":"action_focus_next method","text":"
def action_focus_next(self):\n

An action to focus the next widget.

"},{"location":"api/app/#textual.app.App.action_focus_previous","title":"action_focus_previous method","text":"
def action_focus_previous(self):\n

An action to focus the previous widget.

"},{"location":"api/app/#textual.app.App.action_pop_screen","title":"action_pop_screen async","text":"
def action_pop_screen(self):\n

An action to remove the topmost screen and makes the new topmost screen active.

"},{"location":"api/app/#textual.app.App.action_push_screen","title":"action_push_screen async","text":"
def action_push_screen(self, screen):\n

An action to push a new screen on to the stack and make it active.

Parameters Name Type Description Default screen str

Name of the screen.

required"},{"location":"api/app/#textual.app.App.action_quit","title":"action_quit async","text":"
def action_quit(self):\n

An action to quit the app as soon as possible.

"},{"location":"api/app/#textual.app.App.action_remove_class","title":"action_remove_class async","text":"
def action_remove_class(self, selector, class_name):\n

An action to remove a CSS class from the selected widget.

Parameters Name Type Description Default selector str

Selects the widget to remove the class from.

required class_name str

The class to remove from the selected widget.

required"},{"location":"api/app/#textual.app.App.action_screenshot","title":"action_screenshot method","text":"
def action_screenshot(self, filename=None, path='./'):\n

This action will save an SVG file containing the current contents of the screen.

Parameters Name Type Description Default filename str | None

Filename of screenshot, or None to auto-generate.

None path str

Path to directory. Defaults to current working directory.

'./'"},{"location":"api/app/#textual.app.App.action_switch_mode","title":"action_switch_mode async","text":"
def action_switch_mode(self, mode):\n

An action that switches to the given mode..

"},{"location":"api/app/#textual.app.App.action_switch_screen","title":"action_switch_screen async","text":"
def action_switch_screen(self, screen):\n

An action to switch screens.

Parameters Name Type Description Default screen str

Name of the screen.

required"},{"location":"api/app/#textual.app.App.action_toggle_class","title":"action_toggle_class async","text":"
def action_toggle_class(self, selector, class_name):\n

An action to toggle a CSS class on the selected widget.

Parameters Name Type Description Default selector str

Selects the widget to toggle the class on.

required class_name str

The class to toggle on the selected widget.

required"},{"location":"api/app/#textual.app.App.action_toggle_dark","title":"action_toggle_dark method","text":"
def action_toggle_dark(self):\n

An action to toggle dark mode.

"},{"location":"api/app/#textual.app.App.add_mode","title":"add_mode method","text":"
def add_mode(self, mode, base_screen):\n

Adds a mode and its corresponding base screen to the app.

Parameters Name Type Description Default mode str

The new mode.

required base_screen str | Screen | Callable[[], Screen]

The base screen associated with the given mode.

required Raises Type Description InvalidModeError

If the name of the mode is not valid/duplicated.

"},{"location":"api/app/#textual.app.App.animate","title":"animate method","text":"
def animate(\nself,\nattribute,\nvalue,\n*,\nfinal_value=Ellipsis,\nduration=None,\nspeed=None,\ndelay=0.0,\neasing=DEFAULT_EASING,\non_complete=None\n):\n

Animate an attribute.

See the guide for how to use the animation system.

Parameters Name Type Description Default attribute str

Name of the attribute to animate.

required value float | Animatable

The value to animate to.

required final_value object

The final value of the animation.

Ellipsis duration float | None

The duration of the animate.

None speed float | None

The speed of the animation.

None delay float

A delay (in seconds) before the animation starts.

0.0 easing EasingFunction | str

An easing method.

DEFAULT_EASING on_complete CallbackType | None

A callable to invoke when the animation is finished.

None"},{"location":"api/app/#textual.app.App.batch_update","title":"batch_update method","text":"
def batch_update(self):\n

A context manager to suspend all repaints until the end of the batch.

"},{"location":"api/app/#textual.app.App.begin_capture_print","title":"begin_capture_print method","text":"
def begin_capture_print(self, target, stdout=True, stderr=True):\n

Capture content that is printed (or written to stdout / stderr).

If printing is captured, the target will be send an events.Print message.

Parameters Name Type Description Default target MessageTarget

The widget where print content will be sent.

required stdout bool

Capture stdout.

True stderr bool

Capture stderr.

True"},{"location":"api/app/#textual.app.App.bell","title":"bell method","text":"
def bell(self):\n

Play the console 'bell'.

For terminals that support a bell, this typically makes a notification or error sound. Some terminals may make no sound or display a visual bell indicator, depending on configuration.

"},{"location":"api/app/#textual.app.App.bind","title":"bind method","text":"
def bind(\nself,\nkeys,\naction,\n*,\ndescription=\"\",\nshow=True,\nkey_display=None\n):\n

Bind a key to an action.

Parameters Name Type Description Default keys str

A comma separated list of keys, i.e.

required action str

Action to bind to.

required description str

Short description of action.

'' show bool

Show key in UI.

True key_display str | None

Replacement text for key, or None to use default.

None"},{"location":"api/app/#textual.app.App.call_from_thread","title":"call_from_thread method","text":"
def call_from_thread(self, callback, *args, **kwargs):\n

Run a callable from another thread, and return the result.

Like asyncio apps in general, Textual apps are not thread-safe. If you call methods or set attributes on Textual objects from a thread, you may get unpredictable results.

This method will ensure that your code runs within the correct context.

Tip

Consider using post_message which is also thread-safe.

Parameters Name Type Description Default callback Callable[..., CallThreadReturnType | Awaitable[CallThreadReturnType]]

A callable to run.

required *args object

Arguments to the callback.

() **kwargs object

Keyword arguments for the callback.

{} Raises Type Description RuntimeError

If the app isn't running or if this method is called from the same thread where the app is running.

Returns Type Description CallThreadReturnType

The result of the callback.

"},{"location":"api/app/#textual.app.App.capture_mouse","title":"capture_mouse method","text":"
def capture_mouse(self, widget):\n

Send all mouse events to the given widget or disable mouse capture.

Parameters Name Type Description Default widget Widget | None

If a widget, capture mouse event, or None to end mouse capture.

required"},{"location":"api/app/#textual.app.App.check_bindings","title":"check_bindings async","text":"
def check_bindings(self, key, priority=False):\n

Handle a key press.

This method is used internally by the bindings system, but may be called directly if you wish to simulate a key being pressed.

Parameters Name Type Description Default key str

A key.

required priority bool

If True check from App down, otherwise from focused up.

False Returns Type Description bool

True if the key was handled by a binding, otherwise False

"},{"location":"api/app/#textual.app.App.clear_notifications","title":"clear_notifications method","text":"
def clear_notifications(self):\n

Clear all the current notifications.

"},{"location":"api/app/#textual.app.App.end_capture_print","title":"end_capture_print method","text":"
def end_capture_print(self, target):\n

End capturing of prints.

Parameters Name Type Description Default target MessageTarget

The widget that was capturing prints.

required"},{"location":"api/app/#textual.app.App.exit","title":"exit method","text":"
def exit(self, result=None, return_code=0, message=None):\n

Exit the app, and return the supplied result.

Parameters Name Type Description Default result ReturnType | None

Return value.

None return_code int

The return code. Use non-zero values for error codes.

0 message RenderableType | None

Optional message to display on exit.

None"},{"location":"api/app/#textual.app.App.export_screenshot","title":"export_screenshot method","text":"
def export_screenshot(self, *, title=None):\n

Export an SVG screenshot of the current screen.

See also save_screenshot which writes the screenshot to a file.

Parameters Name Type Description Default title str | None

The title of the exported screenshot or None to use app title.

None"},{"location":"api/app/#textual.app.App.get_child_by_id","title":"get_child_by_id method","text":"
def get_child_by_id(self, id, expect_type=None):\n

Get the first child (immediate descendent) of this DOMNode with the given ID.

Parameters Name Type Description Default id str

The ID of the node to search for.

required expect_type type[ExpectType] | None

Require the object be of the supplied type, or use None to apply no type restriction.

None Returns Type Description ExpectType | Widget

The first child of this node with the specified ID.

Raises Type Description NoMatches

If no children could be found for this ID.

WrongType

If the wrong type was found.

"},{"location":"api/app/#textual.app.App.get_child_by_type","title":"get_child_by_type method","text":"
def get_child_by_type(self, expect_type):\n

Get a child of a give type.

Parameters Name Type Description Default expect_type type[ExpectType]

The type of the expected child.

required Raises Type Description NoMatches

If no valid child is found.

Returns Type Description ExpectType

A widget.

"},{"location":"api/app/#textual.app.App.get_css_variables","title":"get_css_variables method","text":"
def get_css_variables(self):\n

Get a mapping of variables used to pre-populate CSS.

May be implemented in a subclass to add new CSS variables.

Returns Type Description dict[str, str]

A mapping of variable name to value.

"},{"location":"api/app/#textual.app.App.get_driver_class","title":"get_driver_class method","text":"
def get_driver_class(self):\n

Get a driver class for this platform.

This method is called by the constructor, and unlikely to be required when building a Textual app.

Returns Type Description Type[Driver]

A Driver class which manages input and display.

"},{"location":"api/app/#textual.app.App.get_key_display","title":"get_key_display method","text":"
def get_key_display(self, key):\n

For a given key, return how it should be displayed in an app (e.g. in the Footer widget). By key, we refer to the string used in the \"key\" argument for a Binding instance. By overriding this method, you can ensure that keys are displayed consistently throughout your app, without needing to add a key_display to every binding.

Parameters Name Type Description Default key str

The binding key string.

required Returns Type Description str

The display string for the input key.

"},{"location":"api/app/#textual.app.App.get_screen","title":"get_screen method","text":"
def get_screen(self, screen):\n

Get an installed screen.

Parameters Name Type Description Default screen Screen | str

Either a Screen object or screen name (the name argument when installed).

required Raises Type Description KeyError

If the named screen doesn't exist.

Returns Type Description Screen

A screen instance.

"},{"location":"api/app/#textual.app.App.get_widget_at","title":"get_widget_at method","text":"
def get_widget_at(self, x, y):\n

Get the widget under the given coordinates.

Parameters Name Type Description Default x int

X coordinate.

required y int

Y coordinate.

required Returns Type Description tuple[Widget, Region]

The widget and the widget's screen region.

"},{"location":"api/app/#textual.app.App.get_widget_by_id","title":"get_widget_by_id method","text":"
def get_widget_by_id(self, id, expect_type=None):\n

Get the first descendant widget with the given ID.

Performs a breadth-first search rooted at the current screen. It will not return the Screen if that matches the ID. To get the screen, use self.screen.

Parameters Name Type Description Default id str

The ID to search for in the subtree

required expect_type type[ExpectType] | None

Require the object be of the supplied type, or None for any type. Defaults to None.

None Returns Type Description ExpectType | Widget

The first descendant encountered with this ID.

Raises Type Description NoMatches

if no children could be found for this ID

WrongType

if the wrong type was found.

"},{"location":"api/app/#textual.app.App.install_screen","title":"install_screen method","text":"
def install_screen(self, screen, name):\n

Install a screen.

Installing a screen prevents Textual from destroying it when it is no longer on the screen stack. Note that you don't need to install a screen to use it. See push_screen or switch_screen to make a new screen current.

Parameters Name Type Description Default screen Screen

Screen to install.

required name str

Unique name to identify the screen.

required Raises Type Description ScreenError

If the screen can't be installed.

Returns Type Description None

An awaitable that awaits the mounting of the screen and its children.

"},{"location":"api/app/#textual.app.App.is_mounted","title":"is_mounted method","text":"
def is_mounted(self, widget):\n

Check if a widget is mounted.

Parameters Name Type Description Default widget Widget

A widget.

required Returns Type Description bool

True of the widget is mounted.

"},{"location":"api/app/#textual.app.App.is_screen_installed","title":"is_screen_installed method","text":"
def is_screen_installed(self, screen):\n

Check if a given screen has been installed.

Parameters Name Type Description Default screen Screen | str

Either a Screen object or screen name (the name argument when installed).

required Returns Type Description bool

True if the screen is currently installed,

"},{"location":"api/app/#textual.app.App.mount","title":"mount method","text":"
def mount(self, *widgets, before=None, after=None):\n

Mount the given widgets relative to the app's screen.

Parameters Name Type Description Default *widgets Widget

The widget(s) to mount.

() before int | str | Widget | None

Optional location to mount before. An int is the index of the child to mount before, a str is a query_one query to find the widget to mount before.

None after int | str | Widget | None

Optional location to mount after. An int is the index of the child to mount after, a str is a query_one query to find the widget to mount after.

None Returns Type Description AwaitMount

An awaitable object that waits for widgets to be mounted.

Raises Type Description MountError

If there is a problem with the mount request.

Note

Only one of before or after can be provided. If both are provided a MountError will be raised.

"},{"location":"api/app/#textual.app.App.mount_all","title":"mount_all method","text":"
def mount_all(self, widgets, *, before=None, after=None):\n

Mount widgets from an iterable.

Parameters Name Type Description Default widgets Iterable[Widget]

An iterable of widgets.

required before int | str | Widget | None

Optional location to mount before. An int is the index of the child to mount before, a str is a query_one query to find the widget to mount before.

None after int | str | Widget | None

Optional location to mount after. An int is the index of the child to mount after, a str is a query_one query to find the widget to mount after.

None Returns Type Description AwaitMount

An awaitable object that waits for widgets to be mounted.

Raises Type Description MountError

If there is a problem with the mount request.

Note

Only one of before or after can be provided. If both are provided a MountError will be raised.

"},{"location":"api/app/#textual.app.App.notify","title":"notify method","text":"
def notify(\nself,\nmessage,\n*,\ntitle=\"\",\nseverity=\"information\",\ntimeout=Notification.timeout\n):\n

Create a notification.

Tip

This method is thread-safe.

Parameters Name Type Description Default message str

The message for the notification.

required title str

The title for the notification.

'' severity SeverityLevel

The severity of the notification.

'information' timeout float

The timeout for the notification.

Notification.timeout

The notify method is used to create an application-wide notification, shown in a Toast, normally originating in the bottom right corner of the display.

Notifications can have the following severity levels:

  • information
  • warning
  • error

The default is information.

Example
# Show an information notification.\nself.notify(\"It's an older code, sir, but it checks out.\")\n# Show a warning. Note that Textual's notification system allows\n# for the use of Rich console markup.\nself.notify(\n\"Now witness the firepower of this fully \"\n\"[b]ARMED[/b] and [i][b]OPERATIONAL[/b][/i] battle station!\",\ntitle=\"Possible trap detected\",\nseverity=\"warning\",\n)\n# Show an error. Set a longer timeout so it's noticed.\nself.notify(\"It's a trap!\", severity=\"error\", timeout=10)\n# Show an information notification, but without any sort of title.\nself.notify(\"It's against my programming to impersonate a deity.\", title=\"\")\n
"},{"location":"api/app/#textual.app.App.panic","title":"panic method","text":"
def panic(self, *renderables):\n

Exits the app and display error message(s).

Used in response to unexpected errors. For a more graceful exit, see the exit method.

Parameters Name Type Description Default *renderables RenderableType

Text or Rich renderable(s) to display on exit.

()"},{"location":"api/app/#textual.app.App.pop_screen","title":"pop_screen method","text":"
def pop_screen(self):\n

Pop the current screen from the stack, and switch to the previous screen.

Returns Type Description Screen[object]

The screen that was replaced.

"},{"location":"api/app/#textual.app.App.post_display_hook","title":"post_display_hook method","text":"
def post_display_hook(self):\n

Called immediately after a display is done. Used in tests.

"},{"location":"api/app/#textual.app.App.push_screen","title":"push_screen method","text":"
def push_screen(self, screen, callback=None):\n

Push a new screen on the screen stack, making it the current screen.

Parameters Name Type Description Default screen Screen[ScreenResultType] | str

A Screen instance or the name of an installed screen.

required callback ScreenResultCallbackType[ScreenResultType] | None

An optional callback function that will be called if the screen is dismissed with a result.

None Returns Type Description AwaitMount

An optional awaitable that awaits the mounting of the screen and its children.

"},{"location":"api/app/#textual.app.App.refresh_css","title":"refresh_css method","text":"
def refresh_css(self, animate=True):\n

Refresh CSS.

Parameters Name Type Description Default animate bool

Also execute CSS animations.

True"},{"location":"api/app/#textual.app.App.remove_mode","title":"remove_mode method","text":"
def remove_mode(self, mode):\n

Removes a mode from the app.

Screens that are running in the stack of that mode are scheduled for pruning.

Parameters Name Type Description Default mode str

The mode to remove. It can't be the active mode.

required Raises Type Description ActiveModeError

If trying to remove the active mode.

UnknownModeError

If trying to remove an unknown mode.

"},{"location":"api/app/#textual.app.App.run","title":"run method","text":"
def run(self, *, headless=False, size=None, auto_pilot=None):\n

Run the app.

Parameters Name Type Description Default headless bool

Run in headless mode (no output).

False size tuple[int, int] | None

Force terminal size to (WIDTH, HEIGHT), or None to auto-detect.

None auto_pilot AutopilotCallbackType | None

An auto pilot coroutine.

None Returns Type Description ReturnType | None

App return value.

"},{"location":"api/app/#textual.app.App.run_action","title":"run_action async","text":"
def run_action(self, action, default_namespace=None):\n

Perform an action.

Actions are typically associated with key bindings, where you wouldn't need to call this method manually.

Parameters Name Type Description Default action str | ActionParseResult

Action encoded in a string.

required default_namespace object | None

Namespace to use if not provided in the action, or None to use app.

None Returns Type Description bool

True if the event has been handled.

"},{"location":"api/app/#textual.app.App.run_async","title":"run_async async","text":"
def run_async(\nself, *, headless=False, size=None, auto_pilot=None\n):\n

Run the app asynchronously.

Parameters Name Type Description Default headless bool

Run in headless mode (no output).

False size tuple[int, int] | None

Force terminal size to (WIDTH, HEIGHT), or None to auto-detect.

None auto_pilot AutopilotCallbackType | None

An auto pilot coroutine.

None Returns Type Description ReturnType | None

App return value.

"},{"location":"api/app/#textual.app.App.run_test","title":"run_test async","text":"
def run_test(\nself,\n*,\nheadless=True,\nsize=(80, 24),\ntooltips=False,\nnotifications=False,\nmessage_hook=None\n):\n

An asynchronous context manager for testing apps.

Tip

See the guide for testing Textual apps.

Use this to run your app in \"headless\" mode (no output) and drive the app via a Pilot object.

Example
async with app.run_test() as pilot:\nawait pilot.click(\"#Button.ok\")\nassert ...\n
Parameters Name Type Description Default headless bool

Run in headless mode (no output or input).

True size tuple[int, int] | None

Force terminal size to (WIDTH, HEIGHT), or None to auto-detect.

(80, 24) tooltips bool

Enable tooltips when testing.

False notifications bool

Enable notifications when testing.

False message_hook Callable[[Message], None] | None

An optional callback that will called with every message going through the app.

None"},{"location":"api/app/#textual.app.App.save_screenshot","title":"save_screenshot method","text":"
def save_screenshot(\nself, filename=None, path=\"./\", time_format=None\n):\n

Save an SVG screenshot of the current screen.

Parameters Name Type Description Default filename str | None

Filename of SVG screenshot, or None to auto-generate a filename with the date and time.

None path str

Path to directory for output. Defaults to current working directory.

'./' time_format str | None

Date and time format to use if filename is None. Defaults to a format like ISO 8601 with some reserved characters replaced with underscores.

None Returns Type Description str

Filename of screenshot.

"},{"location":"api/app/#textual.app.App.set_focus","title":"set_focus method","text":"
def set_focus(self, widget, scroll_visible=True):\n

Focus (or unfocus) a widget. A focused widget will receive key events first.

Parameters Name Type Description Default widget Widget | None

Widget to focus.

required scroll_visible bool

Scroll widget in to view.

True"},{"location":"api/app/#textual.app.App.stop_animation","title":"stop_animation async","text":"
def stop_animation(self, attribute, complete=True):\n

Stop an animation on an attribute.

Parameters Name Type Description Default attribute str

Name of the attribute whose animation should be stopped.

required complete bool

Should the animation be set to its final value?

True Note

If there is no animation scheduled or running, this is a no-op.

"},{"location":"api/app/#textual.app.App.switch_mode","title":"switch_mode method","text":"
def switch_mode(self, mode):\n

Switch to a given mode.

Parameters Name Type Description Default mode str

The mode to switch to.

required Raises Type Description UnknownModeError

If trying to switch to an unknown mode.

"},{"location":"api/app/#textual.app.App.switch_screen","title":"switch_screen method","text":"
def switch_screen(self, screen):\n

Switch to another screen by replacing the top of the screen stack with a new screen.

Parameters Name Type Description Default screen Screen | str

Either a Screen object or screen name (the name argument when installed).

required"},{"location":"api/app/#textual.app.App.uninstall_screen","title":"uninstall_screen method","text":"
def uninstall_screen(self, screen):\n

Uninstall a screen.

If the screen was not previously installed then this method is a null-op. Uninstalling a screen allows Textual to delete it when it is popped or switched. Note that uninstalling a screen is only required if you have previously installed it with install_screen. Textual will also uninstall screens automatically on exit.

Parameters Name Type Description Default screen Screen | str

The screen to uninstall or the name of a installed screen.

required Returns Type Description str | None

The name of the screen that was uninstalled, or None if no screen was uninstalled.

"},{"location":"api/app/#textual.app.App.update_styles","title":"update_styles method","text":"
def update_styles(self, node):\n

Immediately update the styles of this node and all descendant nodes.

Should be called whenever CSS classes / pseudo classes change. For example, when you hover over a button, the :hover pseudo class will be added, and this method is called to apply the corresponding :hover styles.

"},{"location":"api/app/#textual.app.App.validate_sub_title","title":"validate_sub_title method","text":"
def validate_sub_title(self, sub_title):\n

Make sure the sub-title is set to a string.

"},{"location":"api/app/#textual.app.App.validate_title","title":"validate_title method","text":"
def validate_title(self, title):\n

Make sure the title is set to a string.

"},{"location":"api/app/#textual.app.App.watch_dark","title":"watch_dark method","text":"
def watch_dark(self, dark):\n

Watches the dark bool.

This method handles the transition between light and dark mode when you change the dark attribute.

"},{"location":"api/app/#textual.app.AppError","title":"AppError class","text":"

Bases: Exception

Base class for general App related exceptions.

"},{"location":"api/app/#textual.app.InvalidModeError","title":"InvalidModeError class","text":"

Bases: ModeError

Raised if there is an issue with a mode name.

"},{"location":"api/app/#textual.app.ModeError","title":"ModeError class","text":"

Bases: Exception

Base class for exceptions related to modes.

"},{"location":"api/app/#textual.app.ScreenError","title":"ScreenError class","text":"

Bases: Exception

Base class for exceptions that relate to screens.

"},{"location":"api/app/#textual.app.ScreenStackError","title":"ScreenStackError class","text":"

Bases: ScreenError

Raised when trying to manipulate the screen stack incorrectly.

"},{"location":"api/app/#textual.app.UnknownModeError","title":"UnknownModeError class","text":"

Bases: ModeError

Raised when attempting to use a mode that is not known.

"},{"location":"api/await_remove/","title":"Await remove","text":"

An optionally awaitable object returned by methods that remove widgets.

"},{"location":"api/await_remove/#textual.await_remove.AwaitRemove","title":"AwaitRemove class","text":"
def __init__(self, finished_flag, task):\n

An awaitable returned by a method that removes DOM nodes.

Returned by Widget.remove and DOMQuery.remove.

Parameters Name Type Description Default finished_flag Event

The asyncio event to wait on.

required task Task

The task which does the remove (required to keep a reference).

required"},{"location":"api/binding/","title":"Binding","text":"

A binding maps a key press on to an action.

See bindings in the guide for details.

"},{"location":"api/binding/#textual.binding.Binding","title":"Binding class","text":"

The configuration of a key binding.

"},{"location":"api/binding/#textual.binding.Binding.action","title":"action instance-attribute","text":"
action: str\n

Action to bind to.

"},{"location":"api/binding/#textual.binding.Binding.description","title":"description class-attribute instance-attribute","text":"
description: str = ''\n

Description of action.

"},{"location":"api/binding/#textual.binding.Binding.key","title":"key instance-attribute","text":"
key: str\n

Key to bind. This can also be a comma-separated list of keys to map multiple keys to a single action.

"},{"location":"api/binding/#textual.binding.Binding.key_display","title":"key_display class-attribute instance-attribute","text":"
key_display: str | None = None\n

How the key should be shown in footer.

"},{"location":"api/binding/#textual.binding.Binding.priority","title":"priority class-attribute instance-attribute","text":"
priority: bool = False\n

Enable priority binding for this key.

"},{"location":"api/binding/#textual.binding.Binding.show","title":"show class-attribute instance-attribute","text":"
show: bool = True\n

Show the action in Footer, or False to hide.

"},{"location":"api/binding/#textual.binding.BindingError","title":"BindingError class","text":"

Bases: Exception

A binding related error.

"},{"location":"api/binding/#textual.binding.InvalidBinding","title":"InvalidBinding class","text":"

Bases: Exception

Binding key is in an invalid format.

"},{"location":"api/binding/#textual.binding.NoBinding","title":"NoBinding class","text":"

Bases: Exception

A binding was not found.

"},{"location":"api/color/","title":"Color","text":"

This module contains a powerful Color class which Textual uses to manipulate colors.

"},{"location":"api/color/#textual.color--named-colors","title":"Named colors","text":"

The following named colors are used by the parse method.

colors \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Name\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503hex\u00a0\u00a0\u00a0\u00a0\u2503RGB\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Color\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503 \u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529 \u2502\"aliceblue\"\u2502#F0F8FF\u2502rgb(240,\u00a0248,\u00a0255)\u2502\u2502 \u2502\"ansi_black\"\u2502#000000\u2502rgb(0,\u00a00,\u00a00)\u2502\u2502 \u2502\"ansi_blue\"\u2502#000080\u2502rgb(0,\u00a00,\u00a0128)\u2502\u2502 \u2502\"ansi_bright_black\"\u2502#808080\u2502rgb(128,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"ansi_bright_blue\"\u2502#0000FF\u2502rgb(0,\u00a00,\u00a0255)\u2502\u2502 \u2502\"ansi_bright_cyan\"\u2502#00FFFF\u2502rgb(0,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"ansi_bright_green\"\u2502#00FF00\u2502rgb(0,\u00a0255,\u00a00)\u2502\u2502 \u2502\"ansi_bright_magenta\"\u2502#FF00FF\u2502rgb(255,\u00a00,\u00a0255)\u2502\u2502 \u2502\"ansi_bright_red\"\u2502#FF0000\u2502rgb(255,\u00a00,\u00a00)\u2502\u2502 \u2502\"ansi_bright_white\"\u2502#FFFFFF\u2502rgb(255,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"ansi_bright_yellow\"\u2502#FFFF00\u2502rgb(255,\u00a0255,\u00a00)\u2502\u2502 \u2502\"ansi_cyan\"\u2502#008080\u2502rgb(0,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"ansi_green\"\u2502#008000\u2502rgb(0,\u00a0128,\u00a00)\u2502\u2502 \u2502\"ansi_magenta\"\u2502#800080\u2502rgb(128,\u00a00,\u00a0128)\u2502\u2502 \u2502\"ansi_red\"\u2502#800000\u2502rgb(128,\u00a00,\u00a00)\u2502\u2502 \u2502\"ansi_white\"\u2502#C0C0C0\u2502rgb(192,\u00a0192,\u00a0192)\u2502\u2502 \u2502\"ansi_yellow\"\u2502#808000\u2502rgb(128,\u00a0128,\u00a00)\u2502\u2502 \u2502\"antiquewhite\"\u2502#FAEBD7\u2502rgb(250,\u00a0235,\u00a0215)\u2502\u2502 \u2502\"aqua\"\u2502#00FFFF\u2502rgb(0,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"aquamarine\"\u2502#7FFFD4\u2502rgb(127,\u00a0255,\u00a0212)\u2502\u2502 \u2502\"azure\"\u2502#F0FFFF\u2502rgb(240,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"beige\"\u2502#F5F5DC\u2502rgb(245,\u00a0245,\u00a0220)\u2502\u2502 \u2502\"bisque\"\u2502#FFE4C4\u2502rgb(255,\u00a0228,\u00a0196)\u2502\u2502 \u2502\"black\"\u2502#000000\u2502rgb(0,\u00a00,\u00a00)\u2502\u2502 \u2502\"blanchedalmond\"\u2502#FFEBCD\u2502rgb(255,\u00a0235,\u00a0205)\u2502\u2502 \u2502\"blue\"\u2502#0000FF\u2502rgb(0,\u00a00,\u00a0255)\u2502\u2502 \u2502\"blueviolet\"\u2502#8A2BE2\u2502rgb(138,\u00a043,\u00a0226)\u2502\u2502 \u2502\"brown\"\u2502#A52A2A\u2502rgb(165,\u00a042,\u00a042)\u2502\u2502 \u2502\"burlywood\"\u2502#DEB887\u2502rgb(222,\u00a0184,\u00a0135)\u2502\u2502 \u2502\"cadetblue\"\u2502#5F9EA0\u2502rgb(95,\u00a0158,\u00a0160)\u2502\u2502 \u2502\"chartreuse\"\u2502#7FFF00\u2502rgb(127,\u00a0255,\u00a00)\u2502\u2502 \u2502\"chocolate\"\u2502#D2691E\u2502rgb(210,\u00a0105,\u00a030)\u2502\u2502 \u2502\"coral\"\u2502#FF7F50\u2502rgb(255,\u00a0127,\u00a080)\u2502\u2502 \u2502\"cornflowerblue\"\u2502#6495ED\u2502rgb(100,\u00a0149,\u00a0237)\u2502\u2502 \u2502\"cornsilk\"\u2502#FFF8DC\u2502rgb(255,\u00a0248,\u00a0220)\u2502\u2502 \u2502\"crimson\"\u2502#DC143C\u2502rgb(220,\u00a020,\u00a060)\u2502\u2502 \u2502\"cyan\"\u2502#00FFFF\u2502rgb(0,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"darkblue\"\u2502#00008B\u2502rgb(0,\u00a00,\u00a0139)\u2502\u2502 \u2502\"darkcyan\"\u2502#008B8B\u2502rgb(0,\u00a0139,\u00a0139)\u2502\u2502 \u2502\"darkgoldenrod\"\u2502#B8860B\u2502rgb(184,\u00a0134,\u00a011)\u2502\u2502 \u2502\"darkgray\"\u2502#A9A9A9\u2502rgb(169,\u00a0169,\u00a0169)\u2502\u2502 \u2502\"darkgreen\"\u2502#006400\u2502rgb(0,\u00a0100,\u00a00)\u2502\u2502 \u2502\"darkgrey\"\u2502#A9A9A9\u2502rgb(169,\u00a0169,\u00a0169)\u2502\u2502 \u2502\"darkkhaki\"\u2502#BDB76B\u2502rgb(189,\u00a0183,\u00a0107)\u2502\u2502 \u2502\"darkmagenta\"\u2502#8B008B\u2502rgb(139,\u00a00,\u00a0139)\u2502\u2502 \u2502\"darkolivegreen\"\u2502#556B2F\u2502rgb(85,\u00a0107,\u00a047)\u2502\u2502 \u2502\"darkorange\"\u2502#FF8C00\u2502rgb(255,\u00a0140,\u00a00)\u2502\u2502 \u2502\"darkorchid\"\u2502#9932CC\u2502rgb(153,\u00a050,\u00a0204)\u2502\u2502 \u2502\"darkred\"\u2502#8B0000\u2502rgb(139,\u00a00,\u00a00)\u2502\u2502 \u2502\"darksalmon\"\u2502#E9967A\u2502rgb(233,\u00a0150,\u00a0122)\u2502\u2502 \u2502\"darkseagreen\"\u2502#8FBC8F\u2502rgb(143,\u00a0188,\u00a0143)\u2502\u2502 \u2502\"darkslateblue\"\u2502#483D8B\u2502rgb(72,\u00a061,\u00a0139)\u2502\u2502 \u2502\"darkslategray\"\u2502#2F4F4F\u2502rgb(47,\u00a079,\u00a079)\u2502\u2502 \u2502\"darkslategrey\"\u2502#2F4F4F\u2502rgb(47,\u00a079,\u00a079)\u2502\u2502 \u2502\"darkturquoise\"\u2502#00CED1\u2502rgb(0,\u00a0206,\u00a0209)\u2502\u2502 \u2502\"darkviolet\"\u2502#9400D3\u2502rgb(148,\u00a00,\u00a0211)\u2502\u2502 \u2502\"deeppink\"\u2502#FF1493\u2502rgb(255,\u00a020,\u00a0147)\u2502\u2502 \u2502\"deepskyblue\"\u2502#00BFFF\u2502rgb(0,\u00a0191,\u00a0255)\u2502\u2502 \u2502\"dimgray\"\u2502#696969\u2502rgb(105,\u00a0105,\u00a0105)\u2502\u2502 \u2502\"dimgrey\"\u2502#696969\u2502rgb(105,\u00a0105,\u00a0105)\u2502\u2502 \u2502\"dodgerblue\"\u2502#1E90FF\u2502rgb(30,\u00a0144,\u00a0255)\u2502\u2502 \u2502\"firebrick\"\u2502#B22222\u2502rgb(178,\u00a034,\u00a034)\u2502\u2502 \u2502\"floralwhite\"\u2502#FFFAF0\u2502rgb(255,\u00a0250,\u00a0240)\u2502\u2502 \u2502\"forestgreen\"\u2502#228B22\u2502rgb(34,\u00a0139,\u00a034)\u2502\u2502 \u2502\"fuchsia\"\u2502#FF00FF\u2502rgb(255,\u00a00,\u00a0255)\u2502\u2502 \u2502\"gainsboro\"\u2502#DCDCDC\u2502rgb(220,\u00a0220,\u00a0220)\u2502\u2502 \u2502\"ghostwhite\"\u2502#F8F8FF\u2502rgb(248,\u00a0248,\u00a0255)\u2502\u2502 \u2502\"gold\"\u2502#FFD700\u2502rgb(255,\u00a0215,\u00a00)\u2502\u2502 \u2502\"goldenrod\"\u2502#DAA520\u2502rgb(218,\u00a0165,\u00a032)\u2502\u2502 \u2502\"gray\"\u2502#808080\u2502rgb(128,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"green\"\u2502#008000\u2502rgb(0,\u00a0128,\u00a00)\u2502\u2502 \u2502\"greenyellow\"\u2502#ADFF2F\u2502rgb(173,\u00a0255,\u00a047)\u2502\u2502 \u2502\"grey\"\u2502#808080\u2502rgb(128,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"honeydew\"\u2502#F0FFF0\u2502rgb(240,\u00a0255,\u00a0240)\u2502\u2502 \u2502\"hotpink\"\u2502#FF69B4\u2502rgb(255,\u00a0105,\u00a0180)\u2502\u2502 \u2502\"indianred\"\u2502#CD5C5C\u2502rgb(205,\u00a092,\u00a092)\u2502\u2502 \u2502\"indigo\"\u2502#4B0082\u2502rgb(75,\u00a00,\u00a0130)\u2502\u2502 \u2502\"ivory\"\u2502#FFFFF0\u2502rgb(255,\u00a0255,\u00a0240)\u2502\u2502 \u2502\"khaki\"\u2502#F0E68C\u2502rgb(240,\u00a0230,\u00a0140)\u2502\u2502 \u2502\"lavender\"\u2502#E6E6FA\u2502rgb(230,\u00a0230,\u00a0250)\u2502\u2502 \u2502\"lavenderblush\"\u2502#FFF0F5\u2502rgb(255,\u00a0240,\u00a0245)\u2502\u2502 \u2502\"lawngreen\"\u2502#7CFC00\u2502rgb(124,\u00a0252,\u00a00)\u2502\u2502 \u2502\"lemonchiffon\"\u2502#FFFACD\u2502rgb(255,\u00a0250,\u00a0205)\u2502\u2502 \u2502\"lightblue\"\u2502#ADD8E6\u2502rgb(173,\u00a0216,\u00a0230)\u2502\u2502 \u2502\"lightcoral\"\u2502#F08080\u2502rgb(240,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"lightcyan\"\u2502#E0FFFF\u2502rgb(224,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"lightgoldenrodyellow\"\u2502#FAFAD2\u2502rgb(250,\u00a0250,\u00a0210)\u2502\u2502 \u2502\"lightgray\"\u2502#D3D3D3\u2502rgb(211,\u00a0211,\u00a0211)\u2502\u2502 \u2502\"lightgreen\"\u2502#90EE90\u2502rgb(144,\u00a0238,\u00a0144)\u2502\u2502 \u2502\"lightgrey\"\u2502#D3D3D3\u2502rgb(211,\u00a0211,\u00a0211)\u2502\u2502 \u2502\"lightpink\"\u2502#FFB6C1\u2502rgb(255,\u00a0182,\u00a0193)\u2502\u2502 \u2502\"lightsalmon\"\u2502#FFA07A\u2502rgb(255,\u00a0160,\u00a0122)\u2502\u2502 \u2502\"lightseagreen\"\u2502#20B2AA\u2502rgb(32,\u00a0178,\u00a0170)\u2502\u2502 \u2502\"lightskyblue\"\u2502#87CEFA\u2502rgb(135,\u00a0206,\u00a0250)\u2502\u2502 \u2502\"lightslategray\"\u2502#778899\u2502rgb(119,\u00a0136,\u00a0153)\u2502\u2502 \u2502\"lightslategrey\"\u2502#778899\u2502rgb(119,\u00a0136,\u00a0153)\u2502\u2502 \u2502\"lightsteelblue\"\u2502#B0C4DE\u2502rgb(176,\u00a0196,\u00a0222)\u2502\u2502 \u2502\"lightyellow\"\u2502#FFFFE0\u2502rgb(255,\u00a0255,\u00a0224)\u2502\u2502 \u2502\"lime\"\u2502#00FF00\u2502rgb(0,\u00a0255,\u00a00)\u2502\u2502 \u2502\"limegreen\"\u2502#32CD32\u2502rgb(50,\u00a0205,\u00a050)\u2502\u2502 \u2502\"linen\"\u2502#FAF0E6\u2502rgb(250,\u00a0240,\u00a0230)\u2502\u2502 \u2502\"magenta\"\u2502#FF00FF\u2502rgb(255,\u00a00,\u00a0255)\u2502\u2502 \u2502\"maroon\"\u2502#800000\u2502rgb(128,\u00a00,\u00a00)\u2502\u2502 \u2502\"mediumaquamarine\"\u2502#66CDAA\u2502rgb(102,\u00a0205,\u00a0170)\u2502\u2502 \u2502\"mediumblue\"\u2502#0000CD\u2502rgb(0,\u00a00,\u00a0205)\u2502\u2502 \u2502\"mediumorchid\"\u2502#BA55D3\u2502rgb(186,\u00a085,\u00a0211)\u2502\u2502 \u2502\"mediumpurple\"\u2502#9370DB\u2502rgb(147,\u00a0112,\u00a0219)\u2502\u2502 \u2502\"mediumseagreen\"\u2502#3CB371\u2502rgb(60,\u00a0179,\u00a0113)\u2502\u2502 \u2502\"mediumslateblue\"\u2502#7B68EE\u2502rgb(123,\u00a0104,\u00a0238)\u2502\u2502 \u2502\"mediumspringgreen\"\u2502#00FA9A\u2502rgb(0,\u00a0250,\u00a0154)\u2502\u2502 \u2502\"mediumturquoise\"\u2502#48D1CC\u2502rgb(72,\u00a0209,\u00a0204)\u2502\u2502 \u2502\"mediumvioletred\"\u2502#C71585\u2502rgb(199,\u00a021,\u00a0133)\u2502\u2502 \u2502\"midnightblue\"\u2502#191970\u2502rgb(25,\u00a025,\u00a0112)\u2502\u2502 \u2502\"mintcream\"\u2502#F5FFFA\u2502rgb(245,\u00a0255,\u00a0250)\u2502\u2502 \u2502\"mistyrose\"\u2502#FFE4E1\u2502rgb(255,\u00a0228,\u00a0225)\u2502\u2502 \u2502\"moccasin\"\u2502#FFE4B5\u2502rgb(255,\u00a0228,\u00a0181)\u2502\u2502 \u2502\"navajowhite\"\u2502#FFDEAD\u2502rgb(255,\u00a0222,\u00a0173)\u2502\u2502 \u2502\"navy\"\u2502#000080\u2502rgb(0,\u00a00,\u00a0128)\u2502\u2502 \u2502\"oldlace\"\u2502#FDF5E6\u2502rgb(253,\u00a0245,\u00a0230)\u2502\u2502 \u2502\"olive\"\u2502#808000\u2502rgb(128,\u00a0128,\u00a00)\u2502\u2502 \u2502\"olivedrab\"\u2502#6B8E23\u2502rgb(107,\u00a0142,\u00a035)\u2502\u2502 \u2502\"orange\"\u2502#FFA500\u2502rgb(255,\u00a0165,\u00a00)\u2502\u2502 \u2502\"orangered\"\u2502#FF4500\u2502rgb(255,\u00a069,\u00a00)\u2502\u2502 \u2502\"orchid\"\u2502#DA70D6\u2502rgb(218,\u00a0112,\u00a0214)\u2502\u2502 \u2502\"palegoldenrod\"\u2502#EEE8AA\u2502rgb(238,\u00a0232,\u00a0170)\u2502\u2502 \u2502\"palegreen\"\u2502#98FB98\u2502rgb(152,\u00a0251,\u00a0152)\u2502\u2502 \u2502\"paleturquoise\"\u2502#AFEEEE\u2502rgb(175,\u00a0238,\u00a0238)\u2502\u2502 \u2502\"palevioletred\"\u2502#DB7093\u2502rgb(219,\u00a0112,\u00a0147)\u2502\u2502 \u2502\"papayawhip\"\u2502#FFEFD5\u2502rgb(255,\u00a0239,\u00a0213)\u2502\u2502 \u2502\"peachpuff\"\u2502#FFDAB9\u2502rgb(255,\u00a0218,\u00a0185)\u2502\u2502 \u2502\"peru\"\u2502#CD853F\u2502rgb(205,\u00a0133,\u00a063)\u2502\u2502 \u2502\"pink\"\u2502#FFC0CB\u2502rgb(255,\u00a0192,\u00a0203)\u2502\u2502 \u2502\"plum\"\u2502#DDA0DD\u2502rgb(221,\u00a0160,\u00a0221)\u2502\u2502 \u2502\"powderblue\"\u2502#B0E0E6\u2502rgb(176,\u00a0224,\u00a0230)\u2502\u2502 \u2502\"purple\"\u2502#800080\u2502rgb(128,\u00a00,\u00a0128)\u2502\u2502 \u2502\"rebeccapurple\"\u2502#663399\u2502rgb(102,\u00a051,\u00a0153)\u2502\u2502 \u2502\"red\"\u2502#FF0000\u2502rgb(255,\u00a00,\u00a00)\u2502\u2502 \u2502\"rosybrown\"\u2502#BC8F8F\u2502rgb(188,\u00a0143,\u00a0143)\u2502\u2502 \u2502\"royalblue\"\u2502#4169E1\u2502rgb(65,\u00a0105,\u00a0225)\u2502\u2502 \u2502\"saddlebrown\"\u2502#8B4513\u2502rgb(139,\u00a069,\u00a019)\u2502\u2502 \u2502\"salmon\"\u2502#FA8072\u2502rgb(250,\u00a0128,\u00a0114)\u2502\u2502 \u2502\"sandybrown\"\u2502#F4A460\u2502rgb(244,\u00a0164,\u00a096)\u2502\u2502 \u2502\"seagreen\"\u2502#2E8B57\u2502rgb(46,\u00a0139,\u00a087)\u2502\u2502 \u2502\"seashell\"\u2502#FFF5EE\u2502rgb(255,\u00a0245,\u00a0238)\u2502\u2502 \u2502\"sienna\"\u2502#A0522D\u2502rgb(160,\u00a082,\u00a045)\u2502\u2502 \u2502\"silver\"\u2502#C0C0C0\u2502rgb(192,\u00a0192,\u00a0192)\u2502\u2502 \u2502\"skyblue\"\u2502#87CEEB\u2502rgb(135,\u00a0206,\u00a0235)\u2502\u2502 \u2502\"slateblue\"\u2502#6A5ACD\u2502rgb(106,\u00a090,\u00a0205)\u2502\u2502 \u2502\"slategray\"\u2502#708090\u2502rgb(112,\u00a0128,\u00a0144)\u2502\u2502 \u2502\"slategrey\"\u2502#708090\u2502rgb(112,\u00a0128,\u00a0144)\u2502\u2502 \u2502\"snow\"\u2502#FFFAFA\u2502rgb(255,\u00a0250,\u00a0250)\u2502\u2502 \u2502\"springgreen\"\u2502#00FF7F\u2502rgb(0,\u00a0255,\u00a0127)\u2502\u2502 \u2502\"steelblue\"\u2502#4682B4\u2502rgb(70,\u00a0130,\u00a0180)\u2502\u2502 \u2502\"tan\"\u2502#D2B48C\u2502rgb(210,\u00a0180,\u00a0140)\u2502\u2502 \u2502\"teal\"\u2502#008080\u2502rgb(0,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"thistle\"\u2502#D8BFD8\u2502rgb(216,\u00a0191,\u00a0216)\u2502\u2502 \u2502\"tomato\"\u2502#FF6347\u2502rgb(255,\u00a099,\u00a071)\u2502\u2502 \u2502\"turquoise\"\u2502#40E0D0\u2502rgb(64,\u00a0224,\u00a0208)\u2502\u2502 \u2502\"violet\"\u2502#EE82EE\u2502rgb(238,\u00a0130,\u00a0238)\u2502\u2502 \u2502\"wheat\"\u2502#F5DEB3\u2502rgb(245,\u00a0222,\u00a0179)\u2502\u2502 \u2502\"white\"\u2502#FFFFFF\u2502rgb(255,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"whitesmoke\"\u2502#F5F5F5\u2502rgb(245,\u00a0245,\u00a0245)\u2502\u2502 \u2502\"yellow\"\u2502#FFFF00\u2502rgb(255,\u00a0255,\u00a00)\u2502\u2502 \u2502\"yellowgreen\"\u2502#9ACD32\u2502rgb(154,\u00a0205,\u00a050)\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

"},{"location":"api/color/#textual.color.BLACK","title":"BLACK module-attribute","text":"
BLACK: Final = Color(0, 0, 0)\n

A constant for pure black.

"},{"location":"api/color/#textual.color.WHITE","title":"WHITE module-attribute","text":"
WHITE: Final = Color(255, 255, 255)\n

A constant for pure white.

"},{"location":"api/color/#textual.color.Color","title":"Color class","text":"

Bases: NamedTuple

A class to represent a color.

Colors are stored as three values representing the degree of red, green, and blue in a color, and a fourth \"alpha\" value which defines where the color lies on a gradient of opaque to transparent.

Example
>>> from textual.color import Color\n>>> color = Color.parse(\"red\")\n>>> color\nColor(255, 0, 0)\n>>> color.darken(0.5)\nColor(98, 0, 0)\n>>> color + Color.parse(\"green\")\nColor(0, 128, 0)\n>>> color_with_alpha = Color(100, 50, 25, 0.5)\n>>> color_with_alpha\nColor(100, 50, 25, a=0.5)\n>>> color + color_with_alpha\nColor(177, 25, 12)\n
"},{"location":"api/color/#textual.color.Color.a","title":"a class-attribute instance-attribute","text":"
a: float = 1.0\n

Alpha (opacity) component in range 0 to 1.

"},{"location":"api/color/#textual.color.Color.b","title":"b instance-attribute","text":"
b: int\n

Blue component in range 0 to 255.

"},{"location":"api/color/#textual.color.Color.brightness","title":"brightness property","text":"
brightness: float\n

The human perceptual brightness.

A value of 1 is returned for pure white, and 0 for pure black. Other colors lie on a gradient between the two extremes.

"},{"location":"api/color/#textual.color.Color.clamped","title":"clamped property","text":"
clamped: Color\n

A clamped color (this color with all values in expected range).

"},{"location":"api/color/#textual.color.Color.css","title":"css property","text":"
css: str\n

The color in CSS RGB or RGBA form.

For example, \"rgb(10,20,30)\" for an RGB color, or \"rgb(50,70,80,0.5)\" for an RGBA color.

"},{"location":"api/color/#textual.color.Color.g","title":"g instance-attribute","text":"
g: int\n

Green component in range 0 to 255.

"},{"location":"api/color/#textual.color.Color.hex","title":"hex property","text":"
hex: str\n

The color in CSS hex form, with 6 digits for RGB, and 8 digits for RGBA.

For example, \"#46b3de\" for an RGB color, or \"#3342457f\" for a color with alpha.

"},{"location":"api/color/#textual.color.Color.hex6","title":"hex6 property","text":"
hex6: str\n

The color in CSS hex form, with 6 digits for RGB. Alpha is ignored.

For example, \"#46b3de\".

"},{"location":"api/color/#textual.color.Color.hsl","title":"hsl property","text":"
hsl: HSL\n

This color in HSL format.

HSL color is an alternative way of representing a color, which can be used in certain color calculations.

Returns Type Description HSL

Color encoded in HSL format.

"},{"location":"api/color/#textual.color.Color.inverse","title":"inverse property","text":"
inverse: Color\n

The inverse of this color.

Returns Type Description Color

Inverse color.

"},{"location":"api/color/#textual.color.Color.is_transparent","title":"is_transparent property","text":"
is_transparent: bool\n

Is the color transparent (i.e. has 0 alpha)?

"},{"location":"api/color/#textual.color.Color.monochrome","title":"monochrome property","text":"
monochrome: Color\n

A monochrome version of this color.

Returns Type Description Color

The monochrome (black and white) version of this color.

"},{"location":"api/color/#textual.color.Color.normalized","title":"normalized property","text":"
normalized: tuple[float, float, float]\n

A tuple of the color components normalized to between 0 and 1.

Returns Type Description tuple[float, float, float]

Normalized components.

"},{"location":"api/color/#textual.color.Color.r","title":"r instance-attribute","text":"
r: int\n

Red component in range 0 to 255.

"},{"location":"api/color/#textual.color.Color.rgb","title":"rgb property","text":"
rgb: tuple[int, int, int]\n

The red, green, and blue color components as a tuple of ints.

"},{"location":"api/color/#textual.color.Color.rich_color","title":"rich_color property","text":"
rich_color: RichColor\n

This color encoded in Rich's Color class.

Returns Type Description RichColor

A color object as used by Rich.

"},{"location":"api/color/#textual.color.Color.blend","title":"blend cached","text":"
def blend(self, destination, factor, alpha=None):\n

Generate a new color between two colors.

This method calculates a new color on a gradient. The position on the gradient is given by factor, which is a float between 0 and 1, where 0 is the original color, and 1 is the destination color. A value of gradient between the two extremes produces a color somewhere between the two end points.

Parameters Name Type Description Default destination Color

Another color.

required factor float

A blend factor, 0 -> 1.

required alpha float | None

New alpha for result.

None Returns Type Description Color

A new color.

"},{"location":"api/color/#textual.color.Color.darken","title":"darken cached","text":"
def darken(self, amount, alpha=None):\n

Darken the color by a given amount.

Parameters Name Type Description Default amount float

Value between 0-1 to reduce luminance by.

required alpha float | None

Alpha component for new color or None to copy alpha.

None Returns Type Description Color

New color.

"},{"location":"api/color/#textual.color.Color.from_hsl","title":"from_hsl classmethod","text":"
def from_hsl(cls, h, s, l):\n

Create a color from HLS components.

Parameters Name Type Description Default h float

Hue.

required l float

Lightness.

required s float

Saturation.

required Returns Type Description Color

A new color.

"},{"location":"api/color/#textual.color.Color.from_rich_color","title":"from_rich_color classmethod","text":"
def from_rich_color(cls, rich_color):\n

Create a new color from Rich's Color class.

Parameters Name Type Description Default rich_color RichColor

An instance of Rich color.

required Returns Type Description Color

A new Color instance.

"},{"location":"api/color/#textual.color.Color.get_contrast_text","title":"get_contrast_text cached","text":"
def get_contrast_text(self, alpha=0.95):\n

Get a light or dark color that best contrasts this color, for use with text.

Parameters Name Type Description Default alpha float

An alpha value to apply to the result.

0.95 Returns Type Description Color

A new color, either an off-white or off-black.

"},{"location":"api/color/#textual.color.Color.lighten","title":"lighten method","text":"
def lighten(self, amount, alpha=None):\n

Lighten the color by a given amount.

Parameters Name Type Description Default amount float

Value between 0-1 to increase luminance by.

required alpha float | None

Alpha component for new color or None to copy alpha.

None Returns Type Description Color

New color.

"},{"location":"api/color/#textual.color.Color.multiply_alpha","title":"multiply_alpha method","text":"
def multiply_alpha(self, alpha):\n

Create a new color, multiplying the alpha by a constant.

Parameters Name Type Description Default alpha float

A value to multiple the alpha by (expected to be in the range 0 to 1).

required Returns Type Description Color

A new color.

"},{"location":"api/color/#textual.color.Color.parse","title":"parse cached classmethod","text":"
def parse(cls, color_text):\n

Parse a string containing a named color or CSS-style color.

Colors may be parsed from the following formats:

  • Text beginning with a # is parsed as a hexadecimal color code, where R, G, B, and A must be hexadecimal digits (0-9A-F):

    • #RGB
    • #RGBA
    • #RRGGBB
    • #RRGGBBAA
  • Alternatively, RGB colors can also be specified in the format that follows, where R, G, and B must be numbers between 0 and 255 and A must be a value between 0 and 1:

    • rgb(R,G,B)
    • rgb(R,G,B,A)
  • The HSL model can also be used, with a syntax similar to the above, if H is a value between 0 and 360, S and L are percentages, and A is a value between 0 and 1:

    • hsl(H,S,L)
    • hsla(H,S,L,A)

Any other formats will raise a ColorParseError.

Parameters Name Type Description Default color_text str | Color

Text with a valid color format. Color objects will be returned unmodified.

required Raises Type Description ColorParseError

If the color is not encoded correctly.

Returns Type Description Color

Instance encoding the color specified by the argument.

"},{"location":"api/color/#textual.color.Color.with_alpha","title":"with_alpha method","text":"
def with_alpha(self, alpha):\n

Create a new color with the given alpha.

Parameters Name Type Description Default alpha float

New value for alpha.

required Returns Type Description Color

A new color.

"},{"location":"api/color/#textual.color.ColorParseError","title":"ColorParseError class","text":"
def __init__(self, message, suggested_color=None):\n

Bases: Exception

A color failed to parse.

Parameters Name Type Description Default message str

The error message

required suggested_color str | None

A close color we can suggest.

None"},{"location":"api/color/#textual.color.Gradient","title":"Gradient class","text":"
def __init__(self, *stops):\n

Defines a color gradient.

A gradient is defined by a sequence of \"stops\" consisting of a float and a color. The stop indicate the color at that point on a spectrum between 0 and 1.

Parameters Name Type Description Default stops tuple[float, Color]

A colors stop.

() Raises Type Description ValueError

If any stops are missing (must be at least a stop for 0 and 1).

"},{"location":"api/color/#textual.color.Gradient.get_color","title":"get_color method","text":"
def get_color(self, position):\n

Get a color from the gradient at a position between 0 and 1.

Positions that are between stops will return a blended color.

Parameters Name Type Description Default position float

A number between 0 and 1, where 0 is the first stop, and 1 is the last.

required Returns Type Description Color

A color.

"},{"location":"api/color/#textual.color.HSL","title":"HSL class","text":"

Bases: NamedTuple

A color in HLS (Hue, Saturation, Lightness) format.

"},{"location":"api/color/#textual.color.HSL.css","title":"css property","text":"
css: str\n

HSL in css format.

"},{"location":"api/color/#textual.color.HSL.h","title":"h instance-attribute","text":"
h: float\n

Hue in range 0 to 1.

"},{"location":"api/color/#textual.color.HSL.l","title":"l instance-attribute","text":"
l: float\n

Lightness in range 0 to 1.

"},{"location":"api/color/#textual.color.HSL.s","title":"s instance-attribute","text":"
s: float\n

Saturation in range 0 to 1.

"},{"location":"api/color/#textual.color.HSV","title":"HSV class","text":"

Bases: NamedTuple

A color in HSV (Hue, Saturation, Value) format.

"},{"location":"api/color/#textual.color.HSV.h","title":"h instance-attribute","text":"
h: float\n

Hue in range 0 to 1.

"},{"location":"api/color/#textual.color.HSV.s","title":"s instance-attribute","text":"
s: float\n

Saturation in range 0 to 1.

"},{"location":"api/color/#textual.color.HSV.v","title":"v instance-attribute","text":"
v: float\n

Value un range 0 to 1.

"},{"location":"api/color/#textual.color.Lab","title":"Lab class","text":"

Bases: NamedTuple

A color in CIE-L*ab format.

"},{"location":"api/color/#textual.color.Lab.L","title":"L instance-attribute","text":"
L: float\n

Lightness in range 0 to 100.

"},{"location":"api/color/#textual.color.Lab.a","title":"a instance-attribute","text":"
a: float\n

A axis in range -127 to 128.

"},{"location":"api/color/#textual.color.Lab.b","title":"b instance-attribute","text":"
b: float\n

B axis in range -127 to 128.

"},{"location":"api/color/#textual.color.lab_to_rgb","title":"lab_to_rgb function","text":"
def lab_to_rgb(lab, alpha=1.0):\n

Convert a CIE-L*ab color to RGB.

Uses the standard RGB color space with a D65/2\u2070 standard illuminant. Conversion passes through the XYZ color space. Cf. http://www.easyrgb.com/en/math.php.

"},{"location":"api/color/#textual.color.rgb_to_lab","title":"rgb_to_lab function","text":"
def rgb_to_lab(rgb):\n

Convert an RGB color to the CIE-L*ab format.

Uses the standard RGB color space with a D65/2\u2070 standard illuminant. Conversion passes through the XYZ color space. Cf. http://www.easyrgb.com/en/math.php.

"},{"location":"api/command/","title":"Command","text":"

The Textual command palette.

See the guide on the Command Palette for full details.

"},{"location":"api/command/#textual.command.Hits","title":"Hits module-attribute","text":"
Hits: TypeAlias = AsyncIterator[Hit]\n

Return type for the command provider's search method.

"},{"location":"api/command/#textual.command.Command","title":"Command class","text":"
def __init__(self, prompt, command, id=None, disabled=False):\n

Bases: Option

Class that holds a command in the CommandList.

Parameters Name Type Description Default prompt RenderableType

The prompt for the option.

required command Hit

The details of the command associated with the option.

required id str | None

The optional ID for the option.

None disabled bool

The initial enabled/disabled state. Enabled by default.

False"},{"location":"api/command/#textual.command.Command.command","title":"command instance-attribute","text":"
command = command\n

The details of the command associated with the option.

"},{"location":"api/command/#textual.command.CommandInput","title":"CommandInput class","text":"

Bases: Input

The command palette input control.

"},{"location":"api/command/#textual.command.CommandList","title":"CommandList class","text":"

Bases: OptionList

The command palette command list.

"},{"location":"api/command/#textual.command.CommandPalette","title":"CommandPalette class","text":"
def __init__(self):\n

Bases: ModalScreen[CallbackType]

The Textual command palette.

"},{"location":"api/command/#textual.command.CommandPalette.BINDINGS","title":"BINDINGS class-attribute","text":"
BINDINGS: list[BindingType] = [\nBinding(\n\"ctrl+end, shift+end\",\n\"command_list('last')\",\nshow=False,\n),\nBinding(\n\"ctrl+home, shift+home\",\n\"command_list('first')\",\nshow=False,\n),\nBinding(\"down\", \"cursor_down\", show=False),\nBinding(\"escape\", \"escape\", \"Exit the command palette\"),\nBinding(\n\"pagedown\", \"command_list('page_down')\", show=False\n),\nBinding(\n\"pageup\", \"command_list('page_up')\", show=False\n),\nBinding(\"up\", \"command_list('cursor_up')\", show=False),\n]\n
Key(s) Description ctrl+end, shift+end Jump to the last available commands. ctrl+home, shift+home Jump to the first available commands. down Navigate down through the available commands. escape Exit the command palette. pagedown Navigate down a page through the available commands. pageup Navigate up a page through the available commands. up Navigate up through the available commands."},{"location":"api/command/#textual.command.CommandPalette.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
COMPONENT_CLASSES: set[str] = {\n\"command-palette--help-text\",\n\"command-palette--highlight\",\n}\n
Class Description command-palette--help-text Targets the help text of a matched command. command-palette--highlight Targets the highlights of a matched command."},{"location":"api/command/#textual.command.CommandPalette.run_on_select","title":"run_on_select class-attribute","text":"
run_on_select: bool = True\n

A flag to say if a command should be run when selected by the user.

If True then when a user hits Enter on a command match in the result list, or if they click on one with the mouse, the command will be selected and run. If set to False the input will be filled with the command and then Enter should be pressed on the keyboard or the 'go' button should be pressed.

"},{"location":"api/command/#textual.command.CommandPalette.is_open","title":"is_open staticmethod","text":"
def is_open(app):\n

Is the command palette current open?

Parameters Name Type Description Default app App

The app to test.

required Returns Type Description bool

True if the command palette is currently open, False if not.

"},{"location":"api/command/#textual.command.CommandPalette.on_mount","title":"on_mount method","text":"
def on_mount(self, _):\n

Capture the calling screen.

"},{"location":"api/command/#textual.command.CommandPalette.on_unmount","title":"on_unmount async","text":"
def on_unmount(self):\n

Shutdown providers when command palette is closed.

"},{"location":"api/command/#textual.command.Hit","title":"Hit class","text":"

Holds the details of a single command search hit.

"},{"location":"api/command/#textual.command.Hit.command","title":"command instance-attribute","text":"
command: IgnoreReturnCallbackType\n

The function to call when the command is chosen.

"},{"location":"api/command/#textual.command.Hit.help","title":"help class-attribute instance-attribute","text":"
help: str | None = None\n

Optional help text for the command.

"},{"location":"api/command/#textual.command.Hit.match_display","title":"match_display instance-attribute","text":"
match_display: RenderableType\n

A string or Rich renderable representation of the hit.

"},{"location":"api/command/#textual.command.Hit.score","title":"score instance-attribute","text":"
score: float\n

The score of the command hit.

The value should be between 0 (no match) and 1 (complete match).

"},{"location":"api/command/#textual.command.Hit.text","title":"text class-attribute instance-attribute","text":"
text: str | None = None\n

The command text associated with the hit, as plain text.

If match_display is not simple text, this attribute should be provided by the Provider object.

"},{"location":"api/command/#textual.command.Matcher","title":"Matcher class","text":"
def __init__(\nself, query, *, match_style=None, case_sensitive=False\n):\n

A fuzzy matcher.

Parameters Name Type Description Default query str

A query as typed in by the user.

required match_style Style | None

The style to use to highlight matched portions of a string.

None case_sensitive bool

Should matching be case sensitive?

False"},{"location":"api/command/#textual.fuzzy.Matcher.case_sensitive","title":"case_sensitive property","text":"
case_sensitive: bool\n

Is this matcher case sensitive?

"},{"location":"api/command/#textual.fuzzy.Matcher.match_style","title":"match_style property","text":"
match_style: Style\n

The style that will be used to highlight hits in the matched text.

"},{"location":"api/command/#textual.fuzzy.Matcher.query","title":"query property","text":"
query: str\n

The query string to look for.

"},{"location":"api/command/#textual.fuzzy.Matcher.query_pattern","title":"query_pattern property","text":"
query_pattern: str\n

The regular expression pattern built from the query.

"},{"location":"api/command/#textual.fuzzy.Matcher.highlight","title":"highlight method","text":"
def highlight(self, candidate):\n

Highlight the candidate with the fuzzy match.

Parameters Name Type Description Default candidate str

The candidate string to match against the query.

required Returns Type Description Text

A [rich.text.Text][Text] object with highlighted matches.

"},{"location":"api/command/#textual.fuzzy.Matcher.match","title":"match method","text":"
def match(self, candidate):\n

Match the candidate against the query.

Parameters Name Type Description Default candidate str

Candidate string to match against the query.

required Returns Type Description float

Strength of the match from 0 to 1.

"},{"location":"api/command/#textual.command.Provider","title":"Provider class","text":"
def __init__(self, screen, match_style=None):\n

Bases: ABC

Base class for command palette command providers.

To create new command provider, inherit from this class and implement search.

Parameters Name Type Description Default screen Screen[Any]

A reference to the active screen.

required"},{"location":"api/command/#textual.command.Provider.app","title":"app property","text":"
app: App[object]\n

A reference to the application.

"},{"location":"api/command/#textual.command.Provider.focused","title":"focused property","text":"
focused: Widget | None\n

The currently-focused widget in the currently-active screen in the application.

If no widget has focus this will be None.

"},{"location":"api/command/#textual.command.Provider.match_style","title":"match_style property","text":"
match_style: Style | None\n

The preferred style to use when highlighting matching portions of the match_display.

"},{"location":"api/command/#textual.command.Provider.screen","title":"screen property","text":"
screen: Screen[object]\n

The currently-active screen in the application.

"},{"location":"api/command/#textual.command.Provider.matcher","title":"matcher method","text":"
def matcher(self, user_input, case_sensitive=False):\n

Create a fuzzy matcher for the given user input.

Parameters Name Type Description Default user_input str

The text that the user has input.

required case_sensitive bool

Should matching be case sensitive?

False Returns Type Description Matcher

A fuzzy matcher object for matching against candidate hits.

"},{"location":"api/command/#textual.command.Provider.search","title":"search abstractmethod async","text":"
def search(self, query):\n

A request to search for commands relevant to the given query.

Parameters Name Type Description Default query str

The user input to be matched.

required

Yields:

Type Description Hits

Instances of Hit.

"},{"location":"api/command/#textual.command.Provider.shutdown","title":"shutdown async","text":"
def shutdown(self):\n

Called when the Provider is shutdown.

Use this method to perform an cleanup, if required.

"},{"location":"api/command/#textual.command.Provider.startup","title":"startup async","text":"
def startup(self):\n

Called after the Provider is initialized, but before any calls to search.

"},{"location":"api/command/#textual.command.SearchIcon","title":"SearchIcon class","text":"

Bases: Static

Widget for displaying a search icon before the command input.

"},{"location":"api/command/#textual.command.SearchIcon.icon","title":"icon class-attribute instance-attribute","text":"
icon: var[str] = var(\nEmoji.replace(\":magnifying_glass_tilted_right:\")\n)\n

The icon to display.

"},{"location":"api/containers/","title":"Containers","text":"

Container widgets for quick styling.

With the exception of Center and Middle containers will fill all of the space in the parent widget.

"},{"location":"api/containers/#textual.containers.Center","title":"Center class","text":"

Bases: Widget

A container which aligns children on the X axis.

"},{"location":"api/containers/#textual.containers.Container","title":"Container class","text":"

Bases: Widget

Simple container widget, with vertical layout.

"},{"location":"api/containers/#textual.containers.Grid","title":"Grid class","text":"

Bases: Widget

A container with grid layout.

"},{"location":"api/containers/#textual.containers.Horizontal","title":"Horizontal class","text":"

Bases: Widget

A container with horizontal layout and no scrollbars.

"},{"location":"api/containers/#textual.containers.HorizontalScroll","title":"HorizontalScroll class","text":"

Bases: ScrollableContainer

A container with horizontal layout and an automatic scrollbar on the Y axis.

"},{"location":"api/containers/#textual.containers.Middle","title":"Middle class","text":"

Bases: Widget

A container which aligns children on the Y axis.

"},{"location":"api/containers/#textual.containers.ScrollableContainer","title":"ScrollableContainer class","text":"

Bases: Widget

A scrollable container with vertical layout, and auto scrollbars on both axis.

"},{"location":"api/containers/#textual.containers.ScrollableContainer.BINDINGS","title":"BINDINGS class-attribute","text":"
BINDINGS: list[BindingType] = [\nBinding(\"up\", \"scroll_up\", \"Scroll Up\", show=False),\nBinding(\n\"down\", \"scroll_down\", \"Scroll Down\", show=False\n),\nBinding(\"left\", \"scroll_left\", \"Scroll Up\", show=False),\nBinding(\n\"right\", \"scroll_right\", \"Scroll Right\", show=False\n),\nBinding(\n\"home\", \"scroll_home\", \"Scroll Home\", show=False\n),\nBinding(\"end\", \"scroll_end\", \"Scroll End\", show=False),\nBinding(\"pageup\", \"page_up\", \"Page Up\", show=False),\nBinding(\n\"pagedown\", \"page_down\", \"Page Down\", show=False\n),\n]\n

Keyboard bindings for scrollable containers.

Key(s) Description up Scroll up, if vertical scrolling is available. down Scroll down, if vertical scrolling is available. left Scroll left, if horizontal scrolling is available. right Scroll right, if horizontal scrolling is available. home Scroll to the home position, if scrolling is available. end Scroll to the end position, if scrolling is available. pageup Scroll up one page, if vertical scrolling is available. pagedown Scroll down one page, if vertical scrolling is available."},{"location":"api/containers/#textual.containers.Vertical","title":"Vertical class","text":"

Bases: Widget

A container with vertical layout and no scrollbars.

"},{"location":"api/containers/#textual.containers.VerticalScroll","title":"VerticalScroll class","text":"

Bases: ScrollableContainer

A container with vertical layout and an automatic scrollbar on the Y axis.

"},{"location":"api/content_switcher/","title":"Content switcher","text":""},{"location":"api/content_switcher/#textual.widgets.ContentSwitcher","title":"textual.widgets.ContentSwitcher class","text":"
def __init__(\nself,\n*children,\nname=None,\nid=None,\nclasses=None,\ndisabled=False,\ninitial=None\n):\n

Bases: Container

A widget for switching between different children.

Note

All child widgets that are to be switched between need a unique ID. Children that have no ID will be hidden and ignored.

Parameters Name Type Description Default *children Widget

The widgets to switch between.

() name str | None

The name of the content switcher.

None id str | None

The ID of the content switcher in the DOM.

None classes str | None

The CSS classes of the content switcher.

None disabled bool

Whether the content switcher is disabled or not.

False initial str | None

The ID of the initial widget to show, None or empty string for the first tab.

None Note

If initial is not supplied no children will be shown to start with.

"},{"location":"api/content_switcher/#textual.widgets._content_switcher.ContentSwitcher.current","title":"current class-attribute instance-attribute","text":"
current: reactive[str | None] = reactive[Optional[str]](\nNone, init=False\n)\n

The ID of the currently-displayed widget.

If set to None then no widget is visible.

Note

If set to an unknown ID, this will result in NoMatches being raised.

"},{"location":"api/content_switcher/#textual.widgets._content_switcher.ContentSwitcher.visible_content","title":"visible_content property","text":"
visible_content: Widget | None\n

A reference to the currently-visible widget.

None if nothing is visible.

"},{"location":"api/content_switcher/#textual.widgets._content_switcher.ContentSwitcher.watch_current","title":"watch_current method","text":"
def watch_current(self, old, new):\n

React to the current visible child choice being changed.

Parameters Name Type Description Default old str | None

The old widget ID (or None if there was no widget).

required new str | None

The new widget ID (or None if nothing should be shown).

required"},{"location":"api/coordinate/","title":"Coordinate","text":"

A class to store a coordinate, used by the DataTable.

"},{"location":"api/coordinate/#textual.coordinate.Coordinate","title":"Coordinate class","text":"

Bases: NamedTuple

An object representing a row/column coordinate within a grid.

"},{"location":"api/coordinate/#textual.coordinate.Coordinate.column","title":"column instance-attribute","text":"
column: int\n

The column of the coordinate within a grid.

"},{"location":"api/coordinate/#textual.coordinate.Coordinate.row","title":"row instance-attribute","text":"
row: int\n

The row of the coordinate within a grid.

"},{"location":"api/coordinate/#textual.coordinate.Coordinate.down","title":"down method","text":"
def down(self):\n

Get the coordinate below.

Returns Type Description Coordinate

The coordinate below.

"},{"location":"api/coordinate/#textual.coordinate.Coordinate.left","title":"left method","text":"
def left(self):\n

Get the coordinate to the left.

Returns Type Description Coordinate

The coordinate to the left.

"},{"location":"api/coordinate/#textual.coordinate.Coordinate.right","title":"right method","text":"
def right(self):\n

Get the coordinate to the right.

Returns Type Description Coordinate

The coordinate to the right.

"},{"location":"api/coordinate/#textual.coordinate.Coordinate.up","title":"up method","text":"
def up(self):\n

Get the coordinate above.

Returns Type Description Coordinate

The coordinate above.

"},{"location":"api/dom_node/","title":"Dom node","text":"

A DOMNode is a base class for any object within the Textual Document Object Model, which includes all Widgets, Screens, and Apps.

"},{"location":"api/dom_node/#textual.dom.WalkMethod","title":"WalkMethod module-attribute","text":"
WalkMethod: TypeAlias = Literal['depth', 'breadth']\n

Valid walking methods for the DOMNode.walk_children method.

"},{"location":"api/dom_node/#textual.dom.BadIdentifier","title":"BadIdentifier class","text":"

Bases: Exception

Exception raised if you supply a id attribute or class name in the wrong format.

"},{"location":"api/dom_node/#textual.dom.DOMError","title":"DOMError class","text":"

Bases: Exception

Base exception class for errors relating to the DOM.

"},{"location":"api/dom_node/#textual.dom.DOMNode","title":"DOMNode class","text":"
def __init__(self, *, name=None, id=None, classes=None):\n

Bases: MessagePump

The base class for object that can be in the Textual DOM (App and Widget)

"},{"location":"api/dom_node/#textual.dom.DOMNode.SCOPED_CSS","title":"SCOPED_CSS class-attribute","text":"
SCOPED_CSS: bool = True\n

Should default css be limited to the widget type?

"},{"location":"api/dom_node/#textual.dom.DOMNode.ancestors","title":"ancestors property","text":"
ancestors: list[DOMNode]\n

A list of ancestor nodes Nodes by tracing ancestors all the way back to App.

Returns Type Description list[DOMNode]

A list of nodes.

"},{"location":"api/dom_node/#textual.dom.DOMNode.ancestors_with_self","title":"ancestors_with_self property","text":"
ancestors_with_self: list[DOMNode]\n

A list of Nodes by tracing a path all the way back to App.

Note

This is inclusive of self.

Returns Type Description list[DOMNode]

A list of nodes.

"},{"location":"api/dom_node/#textual.dom.DOMNode.auto_refresh","title":"auto_refresh property writable","text":"
auto_refresh: float | None\n

Number of seconds between automatic refresh, or None for no automatic refresh.

"},{"location":"api/dom_node/#textual.dom.DOMNode.background_colors","title":"background_colors property","text":"
background_colors: tuple[Color, Color]\n

The background color and the color of the parent's background.

Returns Type Description tuple[Color, Color]

(<background color>, <color>)

"},{"location":"api/dom_node/#textual.dom.DOMNode.children","title":"children property","text":"
children: Sequence['Widget']\n

A view on to the children.

Returns Type Description Sequence['Widget']

The node's children.

"},{"location":"api/dom_node/#textual.dom.DOMNode.classes","title":"classes class-attribute instance-attribute","text":"
classes = _ClassesDescriptor()\n

CSS class names for this node.

"},{"location":"api/dom_node/#textual.dom.DOMNode.colors","title":"colors property","text":"
colors: tuple[Color, Color, Color, Color]\n

The widget's background and foreground colors, and the parent's background and foreground colors.

Returns Type Description tuple[Color, Color, Color, Color]

(<parent background>, <parent color>, <background>, <color>)

"},{"location":"api/dom_node/#textual.dom.DOMNode.css_identifier","title":"css_identifier property","text":"
css_identifier: str\n

A CSS selector that identifies this DOM node.

"},{"location":"api/dom_node/#textual.dom.DOMNode.css_identifier_styled","title":"css_identifier_styled property","text":"
css_identifier_styled: Text\n

A syntax highlighted CSS identifier.

Returns Type Description Text

A Rich Text object.

"},{"location":"api/dom_node/#textual.dom.DOMNode.css_path_nodes","title":"css_path_nodes property","text":"
css_path_nodes: list[DOMNode]\n

A list of nodes from the App to this node, forming a \"path\".

Returns Type Description list[DOMNode]

A list of nodes, where the first item is the App, and the last is this node.

"},{"location":"api/dom_node/#textual.dom.DOMNode.css_tree","title":"css_tree property","text":"
css_tree: Tree\n

A Rich tree to display the DOM, annotated with the node's CSS.

Log this to visualize your app in the textual console.

Example
self.log(self.css_tree)\n
Returns Type Description Tree

A Tree renderable.

"},{"location":"api/dom_node/#textual.dom.DOMNode.display","title":"display property writable","text":"
display: bool\n

Should the DOM node be displayed?

May be set to a boolean to show or hide the node, or to any valid value for the display rule.

Example
my_widget.display = False  # Hide my_widget\n
"},{"location":"api/dom_node/#textual.dom.DOMNode.displayed_children","title":"displayed_children property","text":"
displayed_children: list[Widget]\n

The child nodes which will be displayed.

Returns Type Description list[Widget]

A list of nodes.

"},{"location":"api/dom_node/#textual.dom.DOMNode.id","title":"id property writable","text":"
id: str | None\n

The ID of this node, or None if the node has no ID.

"},{"location":"api/dom_node/#textual.dom.DOMNode.is_modal","title":"is_modal property","text":"
is_modal: bool\n

Is the node a modal?

"},{"location":"api/dom_node/#textual.dom.DOMNode.name","title":"name property","text":"
name: str | None\n

The name of the node.

"},{"location":"api/dom_node/#textual.dom.DOMNode.parent","title":"parent property","text":"
parent: DOMNode | None\n

The parent node.

All nodes have parent once added to the DOM, with the exception of the App which is the root node.

"},{"location":"api/dom_node/#textual.dom.DOMNode.pseudo_classes","title":"pseudo_classes property","text":"
pseudo_classes: frozenset[str]\n

A (frozen) set of all pseudo classes.

"},{"location":"api/dom_node/#textual.dom.DOMNode.rich_style","title":"rich_style property","text":"
rich_style: Style\n

Get a Rich Style object for this DOMNode.

Returns Type Description Style

A Rich style.

"},{"location":"api/dom_node/#textual.dom.DOMNode.screen","title":"screen property","text":"
screen: 'Screen[object]'\n

The screen containing this node.

Returns Type Description 'Screen[object]'

A screen object.

Raises Type Description NoScreen

If this node isn't mounted (and has no screen).

"},{"location":"api/dom_node/#textual.dom.DOMNode.text_style","title":"text_style property","text":"
text_style: Style\n

Get the text style object.

A widget's style is influenced by its parent. for instance if a parent is bold, then the child will also be bold.

Returns Type Description Style

A Rich Style.

"},{"location":"api/dom_node/#textual.dom.DOMNode.tree","title":"tree property","text":"
tree: Tree\n

A Rich tree to display the DOM.

Log this to visualize your app in the textual console.

Example
self.log(self.tree)\n
Returns Type Description Tree

A Tree renderable.

"},{"location":"api/dom_node/#textual.dom.DOMNode.visible","title":"visible property writable","text":"
visible: bool\n

Is this widget visible in the DOM?

If a widget hasn't had its visibility set explicitly, then it inherits it from its DOM ancestors.

This may be set explicitly to override inherited values. The valid values include the valid values for the visibility rule and the booleans True or False, to set the widget to be visible or invisible, respectively.

When a node is invisible, Textual will reserve space for it, but won't display anything.

"},{"location":"api/dom_node/#textual.dom.DOMNode.workers","title":"workers property","text":"
workers: WorkerManager\n

The app's worker manager. Shortcut for self.app.workers.

"},{"location":"api/dom_node/#textual.dom.DOMNode.add_class","title":"add_class method","text":"
def add_class(self, *class_names):\n

Add class names to this Node.

Parameters Name Type Description Default *class_names str

CSS class names to add.

() Returns Type Description Self

Self.

"},{"location":"api/dom_node/#textual.dom.DOMNode.get_component_styles","title":"get_component_styles method","text":"
def get_component_styles(self, name):\n

Get a \"component\" styles object (must be defined in COMPONENT_CLASSES classvar).

Parameters Name Type Description Default name str

Name of the component.

required Raises Type Description KeyError

If the component class doesn't exist.

Returns Type Description RenderStyles

A Styles object.

"},{"location":"api/dom_node/#textual.dom.DOMNode.get_pseudo_classes","title":"get_pseudo_classes method","text":"
def get_pseudo_classes(self):\n

Get any pseudo classes applicable to this Node, e.g. hover, focus.

Returns Type Description Iterable[str]

Iterable of strings, such as a generator.

"},{"location":"api/dom_node/#textual.dom.DOMNode.has_class","title":"has_class method","text":"
def has_class(self, *class_names):\n

Check if the Node has all the given class names.

Parameters Name Type Description Default *class_names str

CSS class names to check.

() Returns Type Description bool

True if the node has all the given class names, otherwise False.

"},{"location":"api/dom_node/#textual.dom.DOMNode.has_pseudo_class","title":"has_pseudo_class method","text":"
def has_pseudo_class(self, *class_names):\n

Check for pseudo classes (such as hover, focus etc)

Parameters Name Type Description Default *class_names str

The pseudo classes to check for.

() Returns Type Description bool

True if the DOM node has those pseudo classes, False if not.

"},{"location":"api/dom_node/#textual.dom.DOMNode.notify_style_update","title":"notify_style_update method","text":"
def notify_style_update(self):\n

Called after styles are updated.

Implement this in a subclass if you want to clear any cached data when the CSS is reloaded.

"},{"location":"api/dom_node/#textual.dom.DOMNode.query","title":"query method","text":"
def query(self, selector=None):\n

Get a DOM query matching a selector.

Parameters Name Type Description Default selector str | type[QueryType] | None

A CSS selector or None for all nodes.

None Returns Type Description DOMQuery[Widget] | DOMQuery[QueryType]

A query object.

"},{"location":"api/dom_node/#textual.dom.DOMNode.query_one","title":"query_one method","text":"
def query_one(self, selector, expect_type=None):\n

Get a single Widget matching the given selector or selector type.

Parameters Name Type Description Default selector str | type[QueryType]

A selector.

required expect_type type[QueryType] | None

Require the object be of the supplied type, or None for any type.

None Raises Type Description WrongType

If the wrong type was found.

NoMatches

If no node matches the query.

TooManyMatches

If there is more than one matching node in the query.

Returns Type Description QueryType | Widget

A widget matching the selector.

"},{"location":"api/dom_node/#textual.dom.DOMNode.remove_class","title":"remove_class method","text":"
def remove_class(self, *class_names):\n

Remove class names from this Node.

Parameters Name Type Description Default *class_names str

CSS class names to remove.

() Returns Type Description Self

Self.

"},{"location":"api/dom_node/#textual.dom.DOMNode.reset_styles","title":"reset_styles method","text":"
def reset_styles(self):\n

Reset styles back to their initial state.

"},{"location":"api/dom_node/#textual.dom.DOMNode.run_worker","title":"run_worker method","text":"
def run_worker(\nself,\nwork,\nname=\"\",\ngroup=\"default\",\ndescription=\"\",\nexit_on_error=True,\nstart=True,\nexclusive=False,\nthread=False,\n):\n

Run work in a worker.

A worker runs a function, coroutine, or awaitable, in the background as an async task or as a thread.

Parameters Name Type Description Default work WorkType[ResultType]

A function, async function, or an awaitable object to run in a worker.

required name str | None

A short string to identify the worker (in logs and debugging).

'' group str

A short string to identify a group of workers.

'default' description str

A longer string to store longer information on the worker.

'' exit_on_error bool

Exit the app if the worker raises an error. Set to False to suppress exceptions.

True start bool

Start the worker immediately.

True exclusive bool

Cancel all workers in the same group.

False thread bool

Mark the worker as a thread worker.

False Returns Type Description Worker[ResultType]

New Worker instance.

"},{"location":"api/dom_node/#textual.dom.DOMNode.set_class","title":"set_class method","text":"
def set_class(self, add, *class_names):\n

Add or remove class(es) based on a condition.

Parameters Name Type Description Default add bool

Add the classes if True, otherwise remove them.

required Returns Type Description Self

Self.

"},{"location":"api/dom_node/#textual.dom.DOMNode.set_classes","title":"set_classes method","text":"
def set_classes(self, classes):\n

Replace all classes.

Parameters Name Type Description Default classes str | Iterable[str]

A string containing space separated classes, or an iterable of class names.

required Returns Type Description Self

Self.

"},{"location":"api/dom_node/#textual.dom.DOMNode.set_styles","title":"set_styles method","text":"
def set_styles(self, css=None, **update_styles):\n

Set custom styles on this object.

Parameters Name Type Description Default css str | None

Styles in CSS format.

None **update_styles

Keyword arguments map style names on to style.

{} Returns Type Description Self

Self.

"},{"location":"api/dom_node/#textual.dom.DOMNode.toggle_class","title":"toggle_class method","text":"
def toggle_class(self, *class_names):\n

Toggle class names on this Node.

Parameters Name Type Description Default *class_names str

CSS class names to toggle.

() Returns Type Description Self

Self.

"},{"location":"api/dom_node/#textual.dom.DOMNode.walk_children","title":"walk_children method","text":"
def walk_children(\nself,\nfilter_type=None,\n*,\nwith_self=False,\nmethod=\"depth\",\nreverse=False\n):\n

Walk the subtree rooted at this node, and return every descendant encountered in a list.

Parameters Name Type Description Default filter_type type[WalkType] | None

Filter only this type, or None for no filter.

None with_self bool

Also yield self in addition to descendants.

False method WalkMethod

One of \"depth\" or \"breadth\".

'depth' reverse bool

Reverse the order (bottom up).

False Returns Type Description list[DOMNode] | list[WalkType]

A list of nodes.

"},{"location":"api/dom_node/#textual.dom.DOMNode.watch","title":"watch method","text":"
def watch(self, obj, attribute_name, callback, init=True):\n

Watches for modifications to reactive attributes on another object.

Example

Here's how you could detect when the app changes from dark to light mode (and vice versa).

def on_dark_change(old_value:bool, new_value:bool):\n# Called when app.dark changes.\nprint(\"App.dark when from {old_value} to {new_value}\")\nself.watch(self.app, \"dark\", self.on_dark_change, init=False)\n
Parameters Name Type Description Default obj DOMNode

Object containing attribute to watch.

required attribute_name str

Attribute to watch.

required callback WatchCallbackType

A callback to run when attribute changes.

required init bool

Check watchers on first call.

True"},{"location":"api/dom_node/#textual.dom.NoScreen","title":"NoScreen class","text":"

Bases: DOMError

Raised when the node has no associated screen.

"},{"location":"api/dom_node/#textual.dom.check_identifiers","title":"check_identifiers function","text":"
def check_identifiers(description, *names):\n

Validate identifier and raise an error if it fails.

Parameters Name Type Description Default description str

Description of where identifier is used for error message.

required *names str

Identifiers to check.

()"},{"location":"api/errors/","title":"Errors","text":"

General exception classes.

"},{"location":"api/errors/#textual.errors.DuplicateKeyHandlers","title":"DuplicateKeyHandlers class","text":"

Bases: TextualError

More than one handler for a single key press.

For example, if the handlers key_ctrl_i and key_tab were defined on the same widget, then this error would be raised.

"},{"location":"api/errors/#textual.errors.NoWidget","title":"NoWidget class","text":"

Bases: TextualError

Specified widget was not found.

"},{"location":"api/errors/#textual.errors.RenderError","title":"RenderError class","text":"

Bases: TextualError

An object could not be rendered.

"},{"location":"api/errors/#textual.errors.TextualError","title":"TextualError class","text":"

Bases: Exception

Base class for Textual errors.

"},{"location":"api/events/","title":"Events","text":"

Builtin events sent by Textual.

Events may be marked as \"Bubbles\" and \"Verbose\". See the events guide for an explanation of bubbling. Verbose events are excluded from the textual console, unless you explicitly request them with the -v switch as follows:

textual console -v\n
"},{"location":"api/events/#textual.events.Blur","title":"Blur class","text":"

Bases: Event

Sent when a widget is blurred (un-focussed).

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.Click","title":"Click class","text":"

Bases: MouseEvent

Sent when a widget is clicked.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.Compose","title":"Compose class","text":"

Bases: Event

Sent to a widget to request it to compose and mount children.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.DescendantBlur","title":"DescendantBlur class","text":"

Bases: Event

Sent when a child widget is blurred.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.DescendantBlur.control","title":"control property","text":"
control: Widget\n

The widget that was blurred (alias of widget).

"},{"location":"api/events/#textual.events.DescendantBlur.widget","title":"widget instance-attribute","text":"
widget: Widget\n

The widget that was blurred.

"},{"location":"api/events/#textual.events.DescendantFocus","title":"DescendantFocus class","text":"

Bases: Event

Sent when a child widget is focussed.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.DescendantFocus.control","title":"control property","text":"
control: Widget\n

The widget that was focused (alias of widget).

"},{"location":"api/events/#textual.events.DescendantFocus.widget","title":"widget instance-attribute","text":"
widget: Widget\n

The widget that was focused.

"},{"location":"api/events/#textual.events.Enter","title":"Enter class","text":"

Bases: Event

Sent when the mouse is moved over a widget.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.Event","title":"Event class","text":"

Bases: Message

The base class for all events.

"},{"location":"api/events/#textual.events.Focus","title":"Focus class","text":"

Bases: Event

Sent when a widget is focussed.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.Hide","title":"Hide class","text":"

Bases: Event

Sent when a widget has been hidden.

  • Bubbles
  • Verbose

A widget may be hidden by setting its visible flag to False, if it is no longer in a layout, or if it has been offset beyond the edges of the terminal.

"},{"location":"api/events/#textual.events.Idle","title":"Idle class","text":"

Bases: Event

Sent when there are no more items in the message queue.

This is a pseudo-event in that it is created by the Textual system and doesn't go through the usual message queue.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.InputEvent","title":"InputEvent class","text":"

Bases: Event

Base class for input events.

"},{"location":"api/events/#textual.events.Key","title":"Key class","text":"
def __init__(self, key, character):\n

Bases: InputEvent

Sent when the user hits a key on the keyboard.

  • Bubbles
  • Verbose
Parameters Name Type Description Default key str

The key that was pressed.

required character str | None

A printable character or None if it is not printable.

required Attributes Name Type Description aliases list[str]

The aliases for the key, including the key itself.

"},{"location":"api/events/#textual.events.Key.is_printable","title":"is_printable property","text":"
is_printable: bool\n

Check if the key is printable (produces a unicode character).

Returns Type Description bool

True if the key is printable.

"},{"location":"api/events/#textual.events.Key.name","title":"name property","text":"
name: str\n

Name of a key suitable for use as a Python identifier.

"},{"location":"api/events/#textual.events.Key.name_aliases","title":"name_aliases property","text":"
name_aliases: list[str]\n

The corresponding name for every alias in aliases list.

"},{"location":"api/events/#textual.events.Leave","title":"Leave class","text":"

Bases: Event

Sent when the mouse is moved away from a widget.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.Load","title":"Load class","text":"

Bases: Event

Sent when the App is running but before the terminal is in application mode.

Use this event to run any set up that doesn't require any visuals such as loading configuration and binding keys.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.Mount","title":"Mount class","text":"

Bases: Event

Sent when a widget is mounted and may receive messages.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.MouseCapture","title":"MouseCapture class","text":"
def __init__(self, mouse_position):\n

Bases: Event

Sent when the mouse has been captured.

  • Bubbles
  • Verbose

When a mouse has been captured, all further mouse events will be sent to the capturing widget.

Parameters Name Type Description Default mouse_position Offset

The position of the mouse when captured.

required"},{"location":"api/events/#textual.events.MouseDown","title":"MouseDown class","text":"

Bases: MouseEvent

Sent when a mouse button is pressed.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.MouseEvent","title":"MouseEvent class","text":"
def __init__(\nself,\nx,\ny,\ndelta_x,\ndelta_y,\nbutton,\nshift,\nmeta,\nctrl,\nscreen_x=None,\nscreen_y=None,\nstyle=None,\n):\n

Bases: InputEvent

Sent in response to a mouse event.

  • Bubbles
  • Verbose
Parameters Name Type Description Default x int

The relative x coordinate.

required y int

The relative y coordinate.

required delta_x int

Change in x since the last message.

required delta_y int

Change in y since the last message.

required button int

Indexed of the pressed button.

required shift bool

True if the shift key is pressed.

required meta bool

True if the meta key is pressed.

required ctrl bool

True if the ctrl key is pressed.

required screen_x int | None

The absolute x coordinate.

None screen_y int | None

The absolute y coordinate.

None style Style | None

The Rich Style under the mouse cursor.

None"},{"location":"api/events/#textual.events.MouseEvent.delta","title":"delta property","text":"
delta: Offset\n

Mouse coordinate delta (change since last event).

Returns Type Description Offset

Mouse coordinate.

"},{"location":"api/events/#textual.events.MouseEvent.offset","title":"offset property","text":"
offset: Offset\n

The mouse coordinate as an offset.

Returns Type Description Offset

Mouse coordinate.

"},{"location":"api/events/#textual.events.MouseEvent.screen_offset","title":"screen_offset property","text":"
screen_offset: Offset\n

Mouse coordinate relative to the screen.

Returns Type Description Offset

Mouse coordinate.

"},{"location":"api/events/#textual.events.MouseEvent.style","title":"style property writable","text":"
style: Style\n

The (Rich) Style under the cursor.

"},{"location":"api/events/#textual.events.MouseEvent.get_content_offset","title":"get_content_offset method","text":"
def get_content_offset(self, widget):\n

Get offset within a widget's content area, or None if offset is not in content (i.e. padding or border).

Parameters Name Type Description Default widget Widget

Widget receiving the event.

required Returns Type Description Offset | None

An offset where the origin is at the top left of the content area.

"},{"location":"api/events/#textual.events.MouseEvent.get_content_offset_capture","title":"get_content_offset_capture method","text":"
def get_content_offset_capture(self, widget):\n

Get offset from a widget's content area.

This method works even if the offset is outside the widget content region.

Parameters Name Type Description Default widget Widget

Widget receiving the event.

required Returns Type Description Offset

An offset where the origin is at the top left of the content area.

"},{"location":"api/events/#textual.events.MouseMove","title":"MouseMove class","text":"

Bases: MouseEvent

Sent when the mouse cursor moves.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.MouseRelease","title":"MouseRelease class","text":"
def __init__(self, mouse_position):\n

Bases: Event

Mouse has been released.

  • Bubbles
  • Verbose
Parameters Name Type Description Default mouse_position Offset

The position of the mouse when released.

required"},{"location":"api/events/#textual.events.MouseScrollDown","title":"MouseScrollDown class","text":"

Bases: MouseEvent

Sent when the mouse wheel is scrolled down.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.MouseScrollUp","title":"MouseScrollUp class","text":"

Bases: MouseEvent

Sent when the mouse wheel is scrolled up.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.MouseUp","title":"MouseUp class","text":"

Bases: MouseEvent

Sent when a mouse button is released.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.Paste","title":"Paste class","text":"
def __init__(self, text):\n

Bases: Event

Event containing text that was pasted into the Textual application. This event will only appear when running in a terminal emulator that supports bracketed paste mode. Textual will enable bracketed pastes when an app starts, and disable it when the app shuts down.

  • Bubbles
  • Verbose
Parameters Name Type Description Default text str

The text that has been pasted.

required"},{"location":"api/events/#textual.events.Print","title":"Print class","text":"
def __init__(self, text, stderr=False):\n

Bases: Event

Sent to a widget that is capturing prints.

  • Bubbles
  • Verbose
Parameters Name Type Description Default text str

Text that was printed.

required stderr bool

True if the print was to stderr, or False for stdout.

False"},{"location":"api/events/#textual.events.Ready","title":"Ready class","text":"

Bases: Event

Sent to the app when the DOM is ready.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.Resize","title":"Resize class","text":"
def __init__(self, size, virtual_size, container_size=None):\n

Bases: Event

Sent when the app or widget has been resized.

  • Bubbles
  • Verbose
Parameters Name Type Description Default size Size

The new size of the Widget.

required virtual_size Size

The virtual size (scrollable size) of the Widget.

required container_size Size | None

The size of the Widget's container widget.

None"},{"location":"api/events/#textual.events.ScreenResume","title":"ScreenResume class","text":"

Bases: Event

Sent to screen that has been made active.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.ScreenSuspend","title":"ScreenSuspend class","text":"

Bases: Event

Sent to screen when it is no longer active.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.Show","title":"Show class","text":"

Bases: Event

Sent when a widget has become visible.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.Timer","title":"Timer class","text":"
def __init__(self, timer, time, count=0, callback=None):\n

Bases: Event

Sent in response to a timer.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.Unmount","title":"Unmount class","text":"

Bases: Event

Sent when a widget is unmounted and may not longer receive messages.

  • Bubbles
  • Verbose
"},{"location":"api/filter/","title":"Filter","text":"

Filter classes.

Note

Filters are used internally, and not recommended for use by Textual app developers.

Filters are used internally to process terminal output after it has been rendered. Currently this is used internally to convert the application to monochrome, when the NO_COLOR env var is set.

In the future, this system will be used to implement accessibility features.

"},{"location":"api/filter/#textual.filter.NO_DIM","title":"NO_DIM module-attribute","text":"
NO_DIM = Style(dim=False)\n

A Style to set dim to False.

"},{"location":"api/filter/#textual.filter.ANSIToTruecolor","title":"ANSIToTruecolor class","text":"
def __init__(self, terminal_theme):\n

Bases: LineFilter

Convert ANSI colors to their truecolor equivalents.

Parameters Name Type Description Default terminal_theme TerminalTheme

A rich terminal theme.

required"},{"location":"api/filter/#textual.filter.ANSIToTruecolor.apply","title":"apply method","text":"
def apply(self, segments, background):\n

Transform a list of segments.

Parameters Name Type Description Default segments list[Segment]

A list of segments.

required background Color

The background color.

required Returns Type Description list[Segment]

A new list of segments.

"},{"location":"api/filter/#textual.filter.ANSIToTruecolor.truecolor_style","title":"truecolor_style cached","text":"
def truecolor_style(self, style):\n

Replace system colors with truecolor equivalent.

Parameters Name Type Description Default style Style

Style to apply truecolor filter to.

required Returns Type Description Style

New style.

"},{"location":"api/filter/#textual.filter.DimFilter","title":"DimFilter class","text":"
def __init__(self, dim_factor=0.5):\n

Bases: LineFilter

Replace dim attributes with modified colors.

Parameters Name Type Description Default dim_factor float

The factor to dim by; 0 is 100% background (i.e. invisible), 1.0 is no change.

0.5"},{"location":"api/filter/#textual.filter.DimFilter.apply","title":"apply method","text":"
def apply(self, segments, background):\n

Transform a list of segments.

Parameters Name Type Description Default segments list[Segment]

A list of segments.

required background Color

The background color.

required Returns Type Description list[Segment]

A new list of segments.

"},{"location":"api/filter/#textual.filter.LineFilter","title":"LineFilter class","text":"

Bases: ABC

Base class for a line filter.

"},{"location":"api/filter/#textual.filter.LineFilter.apply","title":"apply abstractmethod","text":"
def apply(self, segments, background):\n

Transform a list of segments.

Parameters Name Type Description Default segments list[Segment]

A list of segments.

required background Color

The background color.

required Returns Type Description list[Segment]

A new list of segments.

"},{"location":"api/filter/#textual.filter.Monochrome","title":"Monochrome class","text":"

Bases: LineFilter

Convert all colors to monochrome.

"},{"location":"api/filter/#textual.filter.Monochrome.apply","title":"apply method","text":"
def apply(self, segments, background):\n

Transform a list of segments.

Parameters Name Type Description Default segments list[Segment]

A list of segments.

required background Color

The background color.

required Returns Type Description list[Segment]

A new list of segments.

"},{"location":"api/filter/#textual.filter.dim_color","title":"dim_color cached","text":"
def dim_color(background, color, factor):\n

Dim a color by blending towards the background

Parameters Name Type Description Default background RichColor

background color.

required color RichColor

Foreground color.

required factor float

Blend factor

required Returns Type Description RichColor

New dimmer color.

"},{"location":"api/filter/#textual.filter.dim_style","title":"dim_style cached","text":"
def dim_style(style, background, factor):\n

Replace dim attribute with a dim color.

Parameters Name Type Description Default style Style

Style to dim.

required factor float

Blend factor.

required Returns Type Description Style

New dimmed style.

"},{"location":"api/filter/#textual.filter.monochrome_style","title":"monochrome_style cached","text":"
def monochrome_style(style):\n

Convert colors in a style to monochrome.

Parameters Name Type Description Default style Style

A Rich Style.

required Returns Type Description Style

A new Rich style.

"},{"location":"api/fuzzy_matcher/","title":"Fuzzy matcher","text":"

Fuzzy matcher.

This class is used by the command palette to match search terms.

"},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher","title":"Matcher class","text":"
def __init__(\nself, query, *, match_style=None, case_sensitive=False\n):\n

A fuzzy matcher.

Parameters Name Type Description Default query str

A query as typed in by the user.

required match_style Style | None

The style to use to highlight matched portions of a string.

None case_sensitive bool

Should matching be case sensitive?

False"},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher.case_sensitive","title":"case_sensitive property","text":"
case_sensitive: bool\n

Is this matcher case sensitive?

"},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher.match_style","title":"match_style property","text":"
match_style: Style\n

The style that will be used to highlight hits in the matched text.

"},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher.query","title":"query property","text":"
query: str\n

The query string to look for.

"},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher.query_pattern","title":"query_pattern property","text":"
query_pattern: str\n

The regular expression pattern built from the query.

"},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher.highlight","title":"highlight method","text":"
def highlight(self, candidate):\n

Highlight the candidate with the fuzzy match.

Parameters Name Type Description Default candidate str

The candidate string to match against the query.

required Returns Type Description Text

A [rich.text.Text][Text] object with highlighted matches.

"},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher.match","title":"match method","text":"
def match(self, candidate):\n

Match the candidate against the query.

Parameters Name Type Description Default candidate str

Candidate string to match against the query.

required Returns Type Description float

Strength of the match from 0 to 1.

"},{"location":"api/geometry/","title":"Geometry","text":"

Functions and classes to manage terminal geometry (anything involving coordinates or dimensions).

"},{"location":"api/geometry/#textual.geometry.NULL_OFFSET","title":"NULL_OFFSET module-attribute","text":"
NULL_OFFSET: Final = Offset(0, 0)\n

An offset constant for (0, 0).

"},{"location":"api/geometry/#textual.geometry.NULL_REGION","title":"NULL_REGION module-attribute","text":"
NULL_REGION: Final = Region(0, 0, 0, 0)\n

A Region constant for a null region (at the origin, with both width and height set to zero).

"},{"location":"api/geometry/#textual.geometry.NULL_SPACING","title":"NULL_SPACING module-attribute","text":"
NULL_SPACING: Final = Spacing(0, 0, 0, 0)\n

A Spacing constant for no space.

"},{"location":"api/geometry/#textual.geometry.SpacingDimensions","title":"SpacingDimensions module-attribute","text":"
SpacingDimensions: TypeAlias = Union[\nint,\nTuple[int],\nTuple[int, int],\nTuple[int, int, int, int],\n]\n

The valid ways in which you can specify spacing.

"},{"location":"api/geometry/#textual.geometry.Offset","title":"Offset class","text":"

Bases: NamedTuple

A cell offset defined by x and y coordinates.

Offsets are typically relative to the top left of the terminal or other container.

Textual prefers the names x and y, but you could consider x to be the column and y to be the row.

Offsets support addition, subtraction, multiplication, and negation.

Example
>>> from textual.geometry import Offset\n>>> offset = Offset(3, 2)\n>>> offset\nOffset(x=3, y=2)\n>>> offset += Offset(10, 0)\n>>> offset\nOffset(x=13, y=2)\n>>> -offset\nOffset(x=-13, y=-2)\n
"},{"location":"api/geometry/#textual.geometry.Offset.clamped","title":"clamped property","text":"
clamped: Offset\n

This offset with x and y restricted to values above zero.

"},{"location":"api/geometry/#textual.geometry.Offset.is_origin","title":"is_origin property","text":"
is_origin: bool\n

Is the offset at (0, 0)?

"},{"location":"api/geometry/#textual.geometry.Offset.x","title":"x class-attribute instance-attribute","text":"
x: int = 0\n

Offset in the x-axis (horizontal)

"},{"location":"api/geometry/#textual.geometry.Offset.y","title":"y class-attribute instance-attribute","text":"
y: int = 0\n

Offset in the y-axis (vertical)

"},{"location":"api/geometry/#textual.geometry.Offset.blend","title":"blend method","text":"
def blend(self, destination, factor):\n

Calculate a new offset on a line between this offset and a destination offset.

Parameters Name Type Description Default destination Offset

Point where factor would be 1.0.

required factor float

A value between 0 and 1.0.

required Returns Type Description Offset

A new point on a line between self and destination.

"},{"location":"api/geometry/#textual.geometry.Offset.get_distance_to","title":"get_distance_to method","text":"
def get_distance_to(self, other):\n

Get the distance to another offset.

Parameters Name Type Description Default other Offset

An offset.

required Returns Type Description float

Distance to other offset.

"},{"location":"api/geometry/#textual.geometry.Region","title":"Region class","text":"

Bases: NamedTuple

Defines a rectangular region.

A Region consists of a coordinate (x and y) and dimensions (width and height).

  (x, y)\n    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u25b2\n    \u2502                    \u2502 \u2502\n    \u2502                    \u2502 \u2502\n    \u2502                    \u2502 height\n    \u2502                    \u2502 \u2502\n    \u2502                    \u2502 \u2502\n    \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u25bc\n    \u25c0\u2500\u2500\u2500\u2500\u2500\u2500\u2500 width \u2500\u2500\u2500\u2500\u2500\u2500\u25b6\n
Example
>>> from textual.geometry import Region\n>>> region = Region(4, 5, 20, 10)\n>>> region\nRegion(x=4, y=5, width=20, height=10)\n>>> region.area\n200\n>>> region.size\nSize(width=20, height=10)\n>>> region.offset\nOffset(x=4, y=5)\n>>> region.contains(1, 2)\nFalse\n>>> region.contains(10, 8)\nTrue\n
"},{"location":"api/geometry/#textual.geometry.Region.area","title":"area property","text":"
area: int\n

The area under the region.

"},{"location":"api/geometry/#textual.geometry.Region.bottom","title":"bottom property","text":"
bottom: int\n

Maximum Y value (non inclusive).

"},{"location":"api/geometry/#textual.geometry.Region.bottom_left","title":"bottom_left property","text":"
bottom_left: Offset\n

Bottom left offset of the region.

Returns Type Description Offset

An offset.

"},{"location":"api/geometry/#textual.geometry.Region.bottom_right","title":"bottom_right property","text":"
bottom_right: Offset\n

Bottom right offset of the region.

Returns Type Description Offset

An offset.

"},{"location":"api/geometry/#textual.geometry.Region.center","title":"center property","text":"
center: tuple[float, float]\n

The center of the region.

Note, that this does not return an Offset, because the center may not be an integer coordinate.

Returns Type Description tuple[float, float]

Tuple of floats.

"},{"location":"api/geometry/#textual.geometry.Region.column_range","title":"column_range property","text":"
column_range: range\n

A range object for X coordinates.

"},{"location":"api/geometry/#textual.geometry.Region.column_span","title":"column_span property","text":"
column_span: tuple[int, int]\n

A pair of integers for the start and end columns (x coordinates) in this region.

The end value is exclusive.

"},{"location":"api/geometry/#textual.geometry.Region.corners","title":"corners property","text":"
corners: tuple[int, int, int, int]\n

The top left and bottom right coordinates as a tuple of four integers.

"},{"location":"api/geometry/#textual.geometry.Region.height","title":"height class-attribute instance-attribute","text":"
height: int = 0\n

The height of the region.

"},{"location":"api/geometry/#textual.geometry.Region.line_range","title":"line_range property","text":"
line_range: range\n

A range object for Y coordinates.

"},{"location":"api/geometry/#textual.geometry.Region.line_span","title":"line_span property","text":"
line_span: tuple[int, int]\n

A pair of integers for the start and end lines (y coordinates) in this region.

The end value is exclusive.

"},{"location":"api/geometry/#textual.geometry.Region.offset","title":"offset property","text":"
offset: Offset\n

The top left corner of the region.

Returns Type Description Offset

An offset.

"},{"location":"api/geometry/#textual.geometry.Region.reset_offset","title":"reset_offset property","text":"
reset_offset: Region\n

An region of the same size at (0, 0).

Returns Type Description Region

A region at the origin.

"},{"location":"api/geometry/#textual.geometry.Region.right","title":"right property","text":"
right: int\n

Maximum X value (non inclusive).

"},{"location":"api/geometry/#textual.geometry.Region.size","title":"size property","text":"
size: Size\n

Get the size of the region.

"},{"location":"api/geometry/#textual.geometry.Region.top_right","title":"top_right property","text":"
top_right: Offset\n

Top right offset of the region.

Returns Type Description Offset

An offset.

"},{"location":"api/geometry/#textual.geometry.Region.width","title":"width class-attribute instance-attribute","text":"
width: int = 0\n

The width of the region.

"},{"location":"api/geometry/#textual.geometry.Region.x","title":"x class-attribute instance-attribute","text":"
x: int = 0\n

Offset in the x-axis (horizontal).

"},{"location":"api/geometry/#textual.geometry.Region.y","title":"y class-attribute instance-attribute","text":"
y: int = 0\n

Offset in the y-axis (vertical).

"},{"location":"api/geometry/#textual.geometry.Region.at_offset","title":"at_offset method","text":"
def at_offset(self, offset):\n

Get a new Region with the same size at a given offset.

Parameters Name Type Description Default offset tuple[int, int]

An offset.

required Returns Type Description Region

New Region with adjusted offset.

"},{"location":"api/geometry/#textual.geometry.Region.clip","title":"clip method","text":"
def clip(self, width, height):\n

Clip this region to fit within width, height.

Parameters Name Type Description Default width int

Width of bounds.

required height int

Height of bounds.

required Returns Type Description Region

Clipped region.

"},{"location":"api/geometry/#textual.geometry.Region.clip_size","title":"clip_size method","text":"
def clip_size(self, size):\n

Clip the size to fit within minimum values.

Parameters Name Type Description Default size tuple[int, int]

Maximum width and height.

required Returns Type Description Region

No region, not bigger than size.

"},{"location":"api/geometry/#textual.geometry.Region.contains","title":"contains method","text":"
def contains(self, x, y):\n

Check if a point is in the region.

Parameters Name Type Description Default x int

X coordinate.

required y int

Y coordinate.

required Returns Type Description bool

True if the point is within the region.

"},{"location":"api/geometry/#textual.geometry.Region.contains_point","title":"contains_point method","text":"
def contains_point(self, point):\n

Check if a point is in the region.

Parameters Name Type Description Default point tuple[int, int]

A tuple of x and y coordinates.

required Returns Type Description bool

True if the point is within the region.

"},{"location":"api/geometry/#textual.geometry.Region.contains_region","title":"contains_region cached","text":"
def contains_region(self, other):\n

Check if a region is entirely contained within this region.

Parameters Name Type Description Default other Region

A region.

required Returns Type Description bool

True if the other region fits perfectly within this region.

"},{"location":"api/geometry/#textual.geometry.Region.crop_size","title":"crop_size method","text":"
def crop_size(self, size):\n

Get a region with the same offset, with a size no larger than size.

Parameters Name Type Description Default size tuple[int, int]

Maximum width and height (WIDTH, HEIGHT).

required Returns Type Description Region

New region that could fit within size.

"},{"location":"api/geometry/#textual.geometry.Region.expand","title":"expand method","text":"
def expand(self, size):\n

Increase the size of the region by adding a border.

Parameters Name Type Description Default size tuple[int, int]

Additional width and height.

required Returns Type Description Region

A new region.

"},{"location":"api/geometry/#textual.geometry.Region.from_corners","title":"from_corners classmethod","text":"
def from_corners(cls, x1, y1, x2, y2):\n

Construct a Region form the top left and bottom right corners.

Parameters Name Type Description Default x1 int

Top left x.

required y1 int

Top left y.

required x2 int

Bottom right x.

required y2 int

Bottom right y.

required Returns Type Description Region

A new region.

"},{"location":"api/geometry/#textual.geometry.Region.from_offset","title":"from_offset classmethod","text":"
def from_offset(cls, offset, size):\n

Create a region from offset and size.

Parameters Name Type Description Default offset tuple[int, int]

Offset (top left point).

required size tuple[int, int]

Dimensions of region.

required Returns Type Description Region

A region instance.

"},{"location":"api/geometry/#textual.geometry.Region.from_union","title":"from_union classmethod","text":"
def from_union(cls, regions):\n

Create a Region from the union of other regions.

Parameters Name Type Description Default regions Collection[Region]

One or more regions.

required Returns Type Description Region

A Region that encloses all other regions.

"},{"location":"api/geometry/#textual.geometry.Region.get_scroll_to_visible","title":"get_scroll_to_visible classmethod","text":"
def get_scroll_to_visible(\ncls, window_region, region, *, top=False\n):\n

Calculate the smallest offset required to translate a window so that it contains another region.

This method is used to calculate the required offset to scroll something in to view.

Parameters Name Type Description Default window_region Region

The window region.

required region Region

The region to move inside the window.

required top bool

Get offset to top of window.

False Returns Type Description Offset

An offset required to add to region to move it inside window_region.

"},{"location":"api/geometry/#textual.geometry.Region.grow","title":"grow cached","text":"
def grow(self, margin):\n

Grow a region by adding spacing.

Parameters Name Type Description Default margin tuple[int, int, int, int]

Grow space by (<top>, <right>, <bottom>, <left>).

required Returns Type Description Region

New region.

"},{"location":"api/geometry/#textual.geometry.Region.inflect","title":"inflect method","text":"
def inflect(self, x_axis=+1, y_axis=+1, margin=None):\n

Inflect a region around one or both axis.

The x_axis and y_axis parameters define which direction to move the region. A positive value will move the region right or down, a negative value will move the region left or up. A value of 0 will leave that axis unmodified.

\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557    \u2502\n\u2551          \u2551\n\u2551   Self   \u2551    \u2502\n\u2551          \u2551\n\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d    \u2502\n\n\u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u253c \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500\n\n                \u2502    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                     \u2502          \u2502\n                \u2502    \u2502  Result  \u2502\n                     \u2502          \u2502\n                \u2502    \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
Parameters Name Type Description Default x_axis int

+1 to inflect in the positive direction, -1 to inflect in the negative direction.

+1 y_axis int

+1 to inflect in the positive direction, -1 to inflect in the negative direction.

+1 margin Spacing | None

Additional margin.

None Returns Type Description Region

A new region.

"},{"location":"api/geometry/#textual.geometry.Region.intersection","title":"intersection cached","text":"
def intersection(self, region):\n

Get the overlapping portion of the two regions.

Parameters Name Type Description Default region Region

A region that overlaps this region.

required Returns Type Description Region

A new region that covers when the two regions overlap.

"},{"location":"api/geometry/#textual.geometry.Region.overlaps","title":"overlaps cached","text":"
def overlaps(self, other):\n

Check if another region overlaps this region.

Parameters Name Type Description Default other Region

A Region.

required Returns Type Description bool

True if other region shares any cells with this region.

"},{"location":"api/geometry/#textual.geometry.Region.shrink","title":"shrink cached","text":"
def shrink(self, margin):\n

Shrink a region by subtracting spacing.

Parameters Name Type Description Default margin tuple[int, int, int, int]

Shrink space by (<top>, <right>, <bottom>, <left>).

required Returns Type Description Region

The new, smaller region.

"},{"location":"api/geometry/#textual.geometry.Region.split","title":"split cached","text":"
def split(self, cut_x, cut_y):\n

Split a region in to 4 from given x and y offsets (cuts).

           cut_x \u2193\n        \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2510\n        \u2502        \u2502 \u2502   \u2502\n        \u2502    0   \u2502 \u2502 1 \u2502\n        \u2502        \u2502 \u2502   \u2502\ncut_y \u2192 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2518\n        \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2510\n        \u2502    2   \u2502 \u2502 3 \u2502\n        \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2518\n
Parameters Name Type Description Default cut_x int

Offset from self.x where the cut should be made. If negative, the cut is taken from the right edge.

required cut_y int

Offset from self.y where the cut should be made. If negative, the cut is taken from the lower edge.

required Returns Type Description tuple[Region, Region, Region, Region]

Four new regions which add up to the original (self).

"},{"location":"api/geometry/#textual.geometry.Region.split_horizontal","title":"split_horizontal cached","text":"
def split_horizontal(self, cut):\n

Split a region in to two, from a given y offset.

            \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n            \u2502    0    \u2502\n            \u2502         \u2502\n    cut \u2192   \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n            \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n            \u2502    1    \u2502\n            \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
Parameters Name Type Description Default cut int

An offset from self.y where the cut should be made. May be negative, for the offset to start from the lower edge.

required Returns Type Description tuple[Region, Region]

Two regions, which add up to the original (self).

"},{"location":"api/geometry/#textual.geometry.Region.split_vertical","title":"split_vertical cached","text":"
def split_vertical(self, cut):\n

Split a region in to two, from a given x offset.

         cut \u2193\n    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2510\n    \u2502    0   \u2502\u2502 1 \u2502\n    \u2502        \u2502\u2502   \u2502\n    \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2518\n
Parameters Name Type Description Default cut int

An offset from self.x where the cut should be made. If cut is negative, it is taken from the right edge.

required Returns Type Description tuple[Region, Region]

Two regions, which add up to the original (self).

"},{"location":"api/geometry/#textual.geometry.Region.translate","title":"translate cached","text":"
def translate(self, offset):\n

Move the offset of the Region.

Parameters Name Type Description Default offset tuple[int, int]

Offset to add to region.

required Returns Type Description Region

A new region shifted by (x, y)

"},{"location":"api/geometry/#textual.geometry.Region.translate_inside","title":"translate_inside method","text":"
def translate_inside(self, container, x_axis=True, y_axis=True):\n

Translate this region, so it fits within a container.

This will ensure that there is as little overlap as possible. The top left of the returned region is guaranteed to be within the container.

\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510         \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502    container     \u2502         \u2502    container     \u2502\n\u2502                  \u2502         \u2502    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502                  \u2502   \u2500\u2500\u25b6   \u2502    \u2502    return   \u2502\n\u2502       \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2510      \u2502    \u2502             \u2502\n\u2502       \u2502    self     \u2502      \u2502    \u2502             \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524             \u2502      \u2514\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n        \u2502             \u2502\n        \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
Parameters Name Type Description Default container Region

A container region.

required x_axis bool

Allow translation of X axis.

True y_axis bool

Allow translation of Y axis.

True Returns Type Description Region

A new region with same dimensions that fits with inside container.

"},{"location":"api/geometry/#textual.geometry.Region.union","title":"union cached","text":"
def union(self, region):\n

Get the smallest region that contains both regions.

Parameters Name Type Description Default region Region

Another region.

required Returns Type Description Region

An optimally sized region to cover both regions.

"},{"location":"api/geometry/#textual.geometry.Size","title":"Size class","text":"

Bases: NamedTuple

The dimensions (width and height) of a rectangular region.

Example
>>> from textual.geometry import Size\n>>> size = Size(2, 3)\n>>> size\nSize(width=2, height=3)\n>>> size.area\n6\n>>> size + Size(10, 20)\nSize(width=12, height=23)\n
"},{"location":"api/geometry/#textual.geometry.Size.area","title":"area property","text":"
area: int\n

The area occupied by a region of this size.

"},{"location":"api/geometry/#textual.geometry.Size.height","title":"height class-attribute instance-attribute","text":"
height: int = 0\n

The height in cells.

"},{"location":"api/geometry/#textual.geometry.Size.line_range","title":"line_range property","text":"
line_range: range\n

A range object that covers values between 0 and height.

"},{"location":"api/geometry/#textual.geometry.Size.region","title":"region property","text":"
region: Region\n

A region of the same size, at the origin.

"},{"location":"api/geometry/#textual.geometry.Size.width","title":"width class-attribute instance-attribute","text":"
width: int = 0\n

The width in cells.

"},{"location":"api/geometry/#textual.geometry.Size.contains","title":"contains method","text":"
def contains(self, x, y):\n

Check if a point is in area defined by the size.

Parameters Name Type Description Default x int

X coordinate.

required y int

Y coordinate.

required Returns Type Description bool

True if the point is within the region.

"},{"location":"api/geometry/#textual.geometry.Size.contains_point","title":"contains_point method","text":"
def contains_point(self, point):\n

Check if a point is in the area defined by the size.

Parameters Name Type Description Default point tuple[int, int]

A tuple of x and y coordinates.

required Returns Type Description bool

True if the point is within the region.

"},{"location":"api/geometry/#textual.geometry.Spacing","title":"Spacing class","text":"

Bases: NamedTuple

The spacing around a renderable, such as padding and border.

Spacing is defined by four integers for the space at the top, right, bottom, and left of a region.

\u250c \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500\u25b2\u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2510\n               \u2502 top\n\u2502        \u250f\u2501\u2501\u2501\u2501\u2501\u25bc\u2501\u2501\u2501\u2501\u2501\u2501\u2513         \u2502\n \u25c0\u2500\u2500\u2500\u2500\u2500\u2500\u25b6\u2503            \u2503\u25c0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25b6\n\u2502  left  \u2503            \u2503 right   \u2502\n         \u2503            \u2503\n\u2502        \u2517\u2501\u2501\u2501\u2501\u2501\u25b2\u2501\u2501\u2501\u2501\u2501\u2501\u251b         \u2502\n               \u2502 bottom\n\u2514 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500\u25bc\u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2518\n
Example
>>> from textual.geometry import Region, Spacing\n>>> region = Region(2, 3, 20, 10)\n>>> spacing = Spacing(1, 2, 3, 4)\n>>> region.grow(spacing)\nRegion(x=-2, y=2, width=26, height=14)\n>>> region.shrink(spacing)\nRegion(x=6, y=4, width=14, height=6)\n>>> spacing.css\n'1 2 3 4'\n
"},{"location":"api/geometry/#textual.geometry.Spacing.bottom","title":"bottom class-attribute instance-attribute","text":"
bottom: int = 0\n

Space from the bottom of a region.

"},{"location":"api/geometry/#textual.geometry.Spacing.bottom_right","title":"bottom_right property","text":"
bottom_right: tuple[int, int]\n

A pair of integers for the right, and bottom space.

"},{"location":"api/geometry/#textual.geometry.Spacing.css","title":"css property","text":"
css: str\n

A string containing the spacing in CSS format.

For example: \"1\" or \"2 4\" or \"4 2 8 2\".

"},{"location":"api/geometry/#textual.geometry.Spacing.height","title":"height property","text":"
height: int\n

Total space in the y axis.

"},{"location":"api/geometry/#textual.geometry.Spacing.left","title":"left class-attribute instance-attribute","text":"
left: int = 0\n

Space from the left of a region.

"},{"location":"api/geometry/#textual.geometry.Spacing.right","title":"right class-attribute instance-attribute","text":"
right: int = 0\n

Space from the right of a region.

"},{"location":"api/geometry/#textual.geometry.Spacing.top","title":"top class-attribute instance-attribute","text":"
top: int = 0\n

Space from the top of a region.

"},{"location":"api/geometry/#textual.geometry.Spacing.top_left","title":"top_left property","text":"
top_left: tuple[int, int]\n

A pair of integers for the left, and top space.

"},{"location":"api/geometry/#textual.geometry.Spacing.totals","title":"totals property","text":"
totals: tuple[int, int]\n

A pair of integers for the total horizontal and vertical space.

"},{"location":"api/geometry/#textual.geometry.Spacing.width","title":"width property","text":"
width: int\n

Total space in the x axis.

"},{"location":"api/geometry/#textual.geometry.Spacing.all","title":"all classmethod","text":"
def all(cls, amount):\n

Construct a Spacing with a given amount of spacing on all edges.

Parameters Name Type Description Default amount int

The magnitude of spacing to apply to all edges

required Returns Type Description Spacing

Spacing(amount, amount, amount, amount)

"},{"location":"api/geometry/#textual.geometry.Spacing.grow_maximum","title":"grow_maximum method","text":"
def grow_maximum(self, other):\n

Grow spacing with a maximum.

Parameters Name Type Description Default other Spacing

Spacing object.

required Returns Type Description Spacing

New spacing where the values are maximum of the two values.

"},{"location":"api/geometry/#textual.geometry.Spacing.horizontal","title":"horizontal classmethod","text":"
def horizontal(cls, amount):\n

Construct a Spacing with a given amount of spacing on horizontal edges, and no vertical spacing.

Parameters Name Type Description Default amount int

The magnitude of spacing to apply to horizontal edges

required Returns Type Description Spacing

Spacing(0, amount, 0, amount)

"},{"location":"api/geometry/#textual.geometry.Spacing.unpack","title":"unpack classmethod","text":"
def unpack(cls, pad):\n

Unpack padding specified in CSS style.

Parameters Name Type Description Default pad SpacingDimensions

An integer, or tuple of 1, 2, or 4 integers.

required Raises Type Description ValueError

If pad is an invalid value.

Returns Type Description Spacing

New Spacing object.

"},{"location":"api/geometry/#textual.geometry.Spacing.vertical","title":"vertical classmethod","text":"
def vertical(cls, amount):\n

Construct a Spacing with a given amount of spacing on vertical edges, and no horizontal spacing.

Parameters Name Type Description Default amount int

The magnitude of spacing to apply to vertical edges

required Returns Type Description Spacing

Spacing(amount, 0, amount, 0)

"},{"location":"api/geometry/#textual.geometry.clamp","title":"clamp function","text":"
def clamp(value, minimum, maximum):\n

Adjust a value so it is not less than a minimum and not greater than a maximum value.

Parameters Name Type Description Default value T

A value.

required minimum T

Minimum value.

required maximum T

Maximum value.

required Returns Type Description T

New value that is not less than the minimum or greater than the maximum.

"},{"location":"api/logger/","title":"Logger","text":"

A logger class that logs to the Textual console.

"},{"location":"api/logger/#textual.Logger","title":"textual.Logger class","text":"
def __init__(\nself,\nlog_callable,\ngroup=LogGroup.INFO,\nverbosity=LogVerbosity.NORMAL,\n):\n

A Textual logger.

"},{"location":"api/logger/#textual.Logger.debug","title":"debug property","text":"
debug: Logger\n

Logs debug messages.

"},{"location":"api/logger/#textual.Logger.error","title":"error property","text":"
error: Logger\n

Logs errors.

"},{"location":"api/logger/#textual.Logger.event","title":"event property","text":"
event: Logger\n

Logs events.

"},{"location":"api/logger/#textual.Logger.info","title":"info property","text":"
info: Logger\n

Logs information.

"},{"location":"api/logger/#textual.Logger.logging","title":"logging property","text":"
logging: Logger\n

Logs from stdlib logging module.

"},{"location":"api/logger/#textual.Logger.system","title":"system property","text":"
system: Logger\n

Logs system information.

"},{"location":"api/logger/#textual.Logger.verbose","title":"verbose property","text":"
verbose: Logger\n

A verbose logger.

"},{"location":"api/logger/#textual.Logger.warning","title":"warning property","text":"
warning: Logger\n

Logs warnings.

"},{"location":"api/logger/#textual.Logger.worker","title":"worker property","text":"
worker: Logger\n

Logs worker information.

"},{"location":"api/logger/#textual.Logger.verbosity","title":"verbosity method","text":"
def verbosity(self, verbose):\n

Get a new logger with selective verbosity.

Parameters Name Type Description Default verbose bool

True to use HIGH verbosity, otherwise NORMAL.

required Returns Type Description Logger

New logger.

"},{"location":"api/logging/","title":"Logging","text":"

A Textual Logging handler.

If there is an active Textual app, then log messages will go via the app (and logged via textual console).

If there is no active app, then log messages will go to stderr or stdout, depending on configuration.

"},{"location":"api/logging/#textual.logging.TextualHandler","title":"TextualHandler class","text":"
def __init__(self, stderr=True, stdout=False):\n

Bases: Handler

A Logging handler for Textual apps.

Parameters Name Type Description Default stderr bool

Log to stderr when there is no active app.

True stdout bool

Log to stdout when there is not active app.

False"},{"location":"api/logging/#textual.logging.TextualHandler.emit","title":"emit method","text":"
def emit(self, record):\n

Invoked by logging.

"},{"location":"api/map_geometry/","title":"Map geometry","text":"

A data structure returned by screen.find_widget.

"},{"location":"api/map_geometry/#textual._compositor.MapGeometry","title":"textual._compositor.MapGeometry class","text":"

Bases: NamedTuple

Defines the absolute location of a Widget.

"},{"location":"api/map_geometry/#textual._compositor.MapGeometry.clip","title":"clip instance-attribute","text":"
clip: Region\n

A region to clip the widget by (if a Widget is within a container).

"},{"location":"api/map_geometry/#textual._compositor.MapGeometry.container_size","title":"container_size instance-attribute","text":"
container_size: Size\n

The container size (area not occupied by scrollbars).

"},{"location":"api/map_geometry/#textual._compositor.MapGeometry.dock_gutter","title":"dock_gutter instance-attribute","text":"
dock_gutter: Spacing\n

Space from the container reserved by docked widgets.

"},{"location":"api/map_geometry/#textual._compositor.MapGeometry.order","title":"order instance-attribute","text":"
order: tuple[tuple[int, int, int], ...]\n

Tuple of tuples defining the painting order of the widget.

Each successive triple represents painting order information with regards to ancestors in the DOM hierarchy and the last triple provides painting order information for this specific widget.

"},{"location":"api/map_geometry/#textual._compositor.MapGeometry.region","title":"region instance-attribute","text":"
region: Region\n

The (screen) region occupied by the widget.

"},{"location":"api/map_geometry/#textual._compositor.MapGeometry.virtual_region","title":"virtual_region instance-attribute","text":"
virtual_region: Region\n

The region relative to the container (but not necessarily visible).

"},{"location":"api/map_geometry/#textual._compositor.MapGeometry.virtual_size","title":"virtual_size instance-attribute","text":"
virtual_size: Size\n

The virtual size (scrollable area) of a widget if it is a container.

"},{"location":"api/map_geometry/#textual._compositor.MapGeometry.visible_region","title":"visible_region property","text":"
visible_region: Region\n

The Widget region after clipping.

"},{"location":"api/message/","title":"Message","text":"

The base class for all messages (including events).

"},{"location":"api/message/#textual.message.Message","title":"Message class","text":"
def __init__(self):\n

Base class for a message.

"},{"location":"api/message/#textual.message.Message.ALLOW_SELECTOR_MATCH","title":"ALLOW_SELECTOR_MATCH class-attribute","text":"
ALLOW_SELECTOR_MATCH: set[str] = set()\n

Additional attributes that can be used with the on decorator.

These attributes must be widgets.

"},{"location":"api/message/#textual.message.Message.control","title":"control property","text":"
control: Widget | None\n

The widget associated with this message, or None by default.

"},{"location":"api/message/#textual.message.Message.handler_name","title":"handler_name class-attribute","text":"
handler_name: str\n

Name of the default message handler.

"},{"location":"api/message/#textual.message.Message.is_forwarded","title":"is_forwarded property","text":"
is_forwarded: bool\n

Has the message been forwarded?

"},{"location":"api/message/#textual.message.Message.prevent_default","title":"prevent_default method","text":"
def prevent_default(self, prevent=True):\n

Suppress the default action(s). This will prevent handlers in any base classes from being called.

Parameters Name Type Description Default prevent bool

True if the default action should be suppressed, or False if the default actions should be performed.

True"},{"location":"api/message/#textual.message.Message.stop","title":"stop method","text":"
def stop(self, stop=True):\n

Stop propagation of the message to parent.

Parameters Name Type Description Default stop bool

The stop flag.

True"},{"location":"api/message_pump/","title":"Message pump","text":"

A MessagePump is a base class for any object which processes messages, which includes Widget, Screen, and App.

Tip

Most of the method here are useful in general app development.

"},{"location":"api/message_pump/#textual.message_pump.MessagePump","title":"MessagePump class","text":"
def __init__(self, parent=None):\n

Base class which supplies a message pump.

"},{"location":"api/message_pump/#textual.message_pump.MessagePump.app","title":"app property","text":"
app: 'App[object]'\n

Get the current app.

Returns Type Description 'App[object]'

The current app.

Raises Type Description NoActiveAppError

if no active app could be found for the current asyncio context

"},{"location":"api/message_pump/#textual.message_pump.MessagePump.has_parent","title":"has_parent property","text":"
has_parent: bool\n

Does this object have a parent?

"},{"location":"api/message_pump/#textual.message_pump.MessagePump.is_attached","title":"is_attached property","text":"
is_attached: bool\n

Is the node attached to the app via the DOM?

"},{"location":"api/message_pump/#textual.message_pump.MessagePump.is_parent_active","title":"is_parent_active property","text":"
is_parent_active: bool\n

Is the parent active?

"},{"location":"api/message_pump/#textual.message_pump.MessagePump.is_running","title":"is_running property","text":"
is_running: bool\n

Is the message pump running (potentially processing messages)?

"},{"location":"api/message_pump/#textual.message_pump.MessagePump.log","title":"log property","text":"
log: Logger\n

Get a logger for this object.

Returns Type Description Logger

A logger.

"},{"location":"api/message_pump/#textual.message_pump.MessagePump.call_after_refresh","title":"call_after_refresh method","text":"
def call_after_refresh(self, callback, *args, **kwargs):\n

Schedule a callback to run after all messages are processed and the screen has been refreshed. Positional and keyword arguments are passed to the callable.

Parameters Name Type Description Default callback Callback

A callable.

required Returns Type Description bool

True if the callback was scheduled, or False if the callback could not be scheduled (may occur if the message pump was closed or closing).

"},{"location":"api/message_pump/#textual.message_pump.MessagePump.call_later","title":"call_later method","text":"
def call_later(self, callback, *args, **kwargs):\n

Schedule a callback to run after all messages are processed in this object. Positional and keywords arguments are passed to the callable.

Parameters Name Type Description Default callback Callback

Callable to call next.

required *args Any

Positional arguments to pass to the callable.

() **kwargs Any

Keyword arguments to pass to the callable.

{} Returns Type Description bool

True if the callback was scheduled, or False if the callback could not be scheduled (may occur if the message pump was closed or closing).

"},{"location":"api/message_pump/#textual.message_pump.MessagePump.call_next","title":"call_next method","text":"
def call_next(self, callback, *args, **kwargs):\n

Schedule a callback to run immediately after processing the current message.

Parameters Name Type Description Default callback Callback

Callable to run after current event.

required *args Any

Positional arguments to pass to the callable.

() **kwargs Any

Keyword arguments to pass to the callable.

{}"},{"location":"api/message_pump/#textual.message_pump.MessagePump.check_idle","title":"check_idle method","text":"
def check_idle(self):\n

Prompt the message pump to call idle if the queue is empty.

"},{"location":"api/message_pump/#textual.message_pump.MessagePump.check_message_enabled","title":"check_message_enabled method","text":"
def check_message_enabled(self, message):\n

Check if a given message is enabled (allowed to be sent).

Parameters Name Type Description Default message Message

A message object.

required Returns Type Description bool

True if the message will be sent, or False if it is disabled.

"},{"location":"api/message_pump/#textual.message_pump.MessagePump.disable_messages","title":"disable_messages method","text":"
def disable_messages(self, *messages):\n

Disable message types from being processed.

"},{"location":"api/message_pump/#textual.message_pump.MessagePump.dispatch_key","title":"dispatch_key async","text":"
def dispatch_key(self, event):\n

Dispatch a key event to method.

This method will call the method named 'key_' if it exists. Some keys have aliases. The first alias found will be invoked if it exists. If multiple handlers exist that match the key, an exception is raised. Parameters Name Type Description Default event events.Key

A key event.

required Returns Type Description bool

True if key was handled, otherwise False.

Raises Type Description DuplicateKeyHandlers

When there's more than 1 handler that could handle this key.

"},{"location":"api/message_pump/#textual.message_pump.MessagePump.enable_messages","title":"enable_messages method","text":"
def enable_messages(self, *messages):\n

Enable processing of messages types.

"},{"location":"api/message_pump/#textual.message_pump.MessagePump.on_event","title":"on_event async","text":"
def on_event(self, event):\n

Called to process an event.

Parameters Name Type Description Default event events.Event

An Event object.

required"},{"location":"api/message_pump/#textual.message_pump.MessagePump.post_message","title":"post_message method","text":"
def post_message(self, message):\n

Posts a message on to this widget's queue.

Parameters Name Type Description Default message Message

A message (including Event).

required Returns Type Description bool

True if the messages was processed, False if it wasn't.

"},{"location":"api/message_pump/#textual.message_pump.MessagePump.prevent","title":"prevent method","text":"
def prevent(self, *message_types):\n

A context manager to temporarily prevent the given message types from being posted.

Example
input = self.query_one(Input)\nwith self.prevent(Input.Changed):\ninput.value = \"foo\"\n
"},{"location":"api/message_pump/#textual.message_pump.MessagePump.set_interval","title":"set_interval method","text":"
def set_interval(\nself,\ninterval,\ncallback=None,\n*,\nname=None,\nrepeat=0,\npause=False\n):\n

Call a function at periodic intervals.

Parameters Name Type Description Default interval float

Time between calls.

required callback TimerCallback | None

Function to call.

None name str | None

Name of the timer object.

None repeat int

Number of times to repeat the call or 0 for continuous.

0 pause bool

Start the timer paused.

False Returns Type Description Timer

A timer object.

"},{"location":"api/message_pump/#textual.message_pump.MessagePump.set_timer","title":"set_timer method","text":"
def set_timer(\nself, delay, callback=None, *, name=None, pause=False\n):\n

Make a function call after a delay.

Parameters Name Type Description Default delay float

Time to wait before invoking callback.

required callback TimerCallback | None

Callback to call after time has expired.

None name str | None

Name of the timer (for debug).

None pause bool

Start timer paused.

False Returns Type Description Timer

A timer object.

"},{"location":"api/on/","title":"On","text":"

Decorator to declare that the method is a message handler.

The decorator accepts an optional CSS selector that will be matched against a widget exposed by a control attribute on the message.

Example
# Handle the press of buttons with ID \"#quit\".\n@on(Button.Pressed, \"#quit\")\ndef quit_button(self) -> None:\nself.app.quit()\n

Keyword arguments can be used to match additional selectors for attributes listed in ALLOW_SELECTOR_MATCH.

Example
# Handle the activation of the tab \"#home\" within the `TabbedContent` \"#tabs\".\n@on(TabbedContent.TabActivated, \"#tabs\", tab=\"#home\")\ndef switch_to_home(self) -> None:\nself.log(\"Switching back to the home tab.\")\n...\n
Parameters Name Type Description Default message_type type[Message]

The message type (i.e. the class).

required selector str | None

An optional selector. If supplied, the handler will only be called if selector matches the widget from the control attribute of the message.

None **kwargs str

Additional selectors for other attributes of the message.

{}"},{"location":"api/pilot/","title":"Pilot","text":"

The pilot object is used by App.run_test to programmatically operate an app.

See the guide on how to test Textual apps.

"},{"location":"api/pilot/#textual.pilot.Pilot","title":"Pilot class","text":"
def __init__(self, app):\n

Bases: Generic[ReturnType]

Pilot object to drive an app.

"},{"location":"api/pilot/#textual.pilot.Pilot.app","title":"app property","text":"
app: App[ReturnType]\n
"},{"location":"api/pilot/#textual.pilot.Pilot.click","title":"click async","text":"
def click(\nself,\nselector=None,\noffset=(0, 0),\nshift=False,\nmeta=False,\ncontrol=False,\n):\n

Simulate clicking with the mouse.

Parameters Name Type Description Default selector type[Widget] | str | None

The widget that should be clicked. If None, then the click will occur relative to the screen. Note that this simply causes a click to occur at the location of the widget. If the widget is currently hidden or obscured by another widget, then the click may not land on it.

None offset tuple[int, int]

The offset to click within the selected widget.

(0, 0) shift bool

Click with the shift key held down.

False meta bool

Click with the meta key held down.

False control bool

Click with the control key held down.

False"},{"location":"api/pilot/#textual.pilot.Pilot.exit","title":"exit async","text":"
def exit(self, result):\n

Exit the app with the given result.

Parameters Name Type Description Default result ReturnType

The app result returned by run or run_async.

required"},{"location":"api/pilot/#textual.pilot.Pilot.hover","title":"hover async","text":"
def hover(self, selector=None, offset=(0, 0)):\n

Simulate hovering with the mouse cursor.

Parameters Name Type Description Default selector type[Widget] | str | None | None

The widget that should be hovered. If None, then the click will occur relative to the screen. Note that this simply causes a hover to occur at the location of the widget. If the widget is currently hidden or obscured by another widget, then the hover may not land on it.

None offset tuple[int, int]

The offset to hover over within the selected widget.

(0, 0)"},{"location":"api/pilot/#textual.pilot.Pilot.pause","title":"pause async","text":"
def pause(self, delay=None):\n

Insert a pause.

Parameters Name Type Description Default delay float | None

Seconds to pause, or None to wait for cpu idle.

None"},{"location":"api/pilot/#textual.pilot.Pilot.press","title":"press async","text":"
def press(self, *keys):\n

Simulate key-presses.

Parameters Name Type Description Default *keys str

Keys to press.

()"},{"location":"api/pilot/#textual.pilot.Pilot.wait_for_animation","title":"wait_for_animation async","text":"
def wait_for_animation(self):\n

Wait for any current animation to complete.

"},{"location":"api/pilot/#textual.pilot.Pilot.wait_for_scheduled_animations","title":"wait_for_scheduled_animations async","text":"
def wait_for_scheduled_animations(self):\n

Wait for any current and scheduled animations to complete.

"},{"location":"api/pilot/#textual.pilot.WaitForScreenTimeout","title":"WaitForScreenTimeout class","text":"

Bases: Exception

Exception raised if messages aren't being processed quickly enough.

If this occurs, the most likely explanation is some kind of deadlock in the app code.

"},{"location":"api/query/","title":"Query","text":"

A DOMQuery is a set of DOM nodes returned by query.

The set of nodes may be further refined with filter and exclude. Additional methods apply actions to all nodes in the query.

Info

If this sounds like JQuery, a (once) popular JS library, it is no coincidence.

"},{"location":"api/query/#textual.css.query.DOMQuery","title":"DOMQuery class","text":"
def __init__(\nself, node, *, filter=None, exclude=None, parent=None\n):\n

Bases: Generic[QueryType]

Warning

You won't need to construct this manually, as DOMQuery objects are returned by query.

Parameters Name Type Description Default node DOMNode

A DOM node.

required filter str | None

Query to filter children in the node.

None exclude str | None

Query to exclude children in the node.

None parent DOMQuery | None

The parent query, if this is the result of filtering another query.

None Raises Type Description InvalidQueryFormat

If the format of the query is invalid.

"},{"location":"api/query/#textual.css.query.DOMQuery.node","title":"node property","text":"
node: DOMNode\n

The node being queried.

"},{"location":"api/query/#textual.css.query.DOMQuery.nodes","title":"nodes property","text":"
nodes: list[QueryType]\n

Lazily evaluate nodes.

"},{"location":"api/query/#textual.css.query.DOMQuery.add_class","title":"add_class method","text":"
def add_class(self, *class_names):\n

Add the given class name(s) to nodes.

"},{"location":"api/query/#textual.css.query.DOMQuery.exclude","title":"exclude method","text":"
def exclude(self, selector):\n

Exclude nodes that match a given selector.

Parameters Name Type Description Default selector str

A CSS selector.

required Returns Type Description DOMQuery[QueryType]

New DOM query.

"},{"location":"api/query/#textual.css.query.DOMQuery.filter","title":"filter method","text":"
def filter(self, selector):\n

Filter this set by the given CSS selector.

Parameters Name Type Description Default selector str

A CSS selector.

required Returns Type Description DOMQuery[QueryType]

New DOM Query.

"},{"location":"api/query/#textual.css.query.DOMQuery.first","title":"first method","text":"
def first(self, expect_type=None):\n

Get the first matching node.

Parameters Name Type Description Default expect_type type[ExpectType] | None

Require matched node is of this type, or None for any type.

None Raises Type Description WrongType

If the wrong type was found.

NoMatches

If there are no matching nodes in the query.

Returns Type Description QueryType | ExpectType

The matching Widget.

"},{"location":"api/query/#textual.css.query.DOMQuery.last","title":"last method","text":"
def last(self, expect_type=None):\n

Get the last matching node.

Parameters Name Type Description Default expect_type type[ExpectType] | None

Require matched node is of this type, or None for any type.

None Raises Type Description WrongType

If the wrong type was found.

NoMatches

If there are no matching nodes in the query.

Returns Type Description QueryType | ExpectType

The matching Widget.

"},{"location":"api/query/#textual.css.query.DOMQuery.only_one","title":"only_one method","text":"
def only_one(self, expect_type=None):\n

Get the only matching node.

Parameters Name Type Description Default expect_type type[ExpectType] | None

Require matched node is of this type, or None for any type.

None Raises Type Description WrongType

If the wrong type was found.

NoMatches

If no node matches the query.

TooManyMatches

If there is more than one matching node in the query.

Returns Type Description Widget | ExpectType

The matching Widget.

"},{"location":"api/query/#textual.css.query.DOMQuery.refresh","title":"refresh method","text":"
def refresh(self, *, repaint=True, layout=False):\n

Refresh matched nodes.

Parameters Name Type Description Default repaint bool

Repaint node(s).

True layout bool

Layout node(s).

False Returns Type Description DOMQuery[QueryType]

Query for chaining.

"},{"location":"api/query/#textual.css.query.DOMQuery.remove","title":"remove method","text":"
def remove(self):\n

Remove matched nodes from the DOM.

Returns Type Description AwaitRemove

An awaitable object that waits for the widgets to be removed.

"},{"location":"api/query/#textual.css.query.DOMQuery.remove_class","title":"remove_class method","text":"
def remove_class(self, *class_names):\n

Remove the given class names from the nodes.

"},{"location":"api/query/#textual.css.query.DOMQuery.results","title":"results method","text":"
def results(self, filter_type=None):\n

Get query results, optionally filtered by a given type.

Parameters Name Type Description Default filter_type type[ExpectType] | None

A Widget class to filter results, or None for no filter.

None

Yields:

Type Description Widget | ExpectType

Iterator[Widget | ExpectType]: An iterator of Widget instances.

"},{"location":"api/query/#textual.css.query.DOMQuery.set_class","title":"set_class method","text":"
def set_class(self, add, *class_names):\n

Set the given class name(s) according to a condition.

Parameters Name Type Description Default add bool

Add the classes if True, otherwise remove them.

required Returns Type Description DOMQuery[QueryType]

Self.

"},{"location":"api/query/#textual.css.query.DOMQuery.set_classes","title":"set_classes method","text":"
def set_classes(self, classes):\n

Set the classes on nodes to exactly the given set.

Parameters Name Type Description Default classes str | Iterable[str]

A string of space separated classes, or an iterable of class names.

required Returns Type Description DOMQuery[QueryType]

Self.

"},{"location":"api/query/#textual.css.query.DOMQuery.set_styles","title":"set_styles method","text":"
def set_styles(self, css=None, **update_styles):\n

Set styles on matched nodes.

Parameters Name Type Description Default css str | None

CSS declarations to parser, or None.

None"},{"location":"api/query/#textual.css.query.DOMQuery.toggle_class","title":"toggle_class method","text":"
def toggle_class(self, *class_names):\n

Toggle the given class names from matched nodes.

"},{"location":"api/query/#textual.css.query.InvalidQueryFormat","title":"InvalidQueryFormat class","text":"

Bases: QueryError

Query did not parse correctly.

"},{"location":"api/query/#textual.css.query.NoMatches","title":"NoMatches class","text":"

Bases: QueryError

No nodes matched the query.

"},{"location":"api/query/#textual.css.query.QueryError","title":"QueryError class","text":"

Bases: Exception

Base class for a query related error.

"},{"location":"api/query/#textual.css.query.TooManyMatches","title":"TooManyMatches class","text":"

Bases: QueryError

Too many nodes matched the query.

"},{"location":"api/query/#textual.css.query.WrongType","title":"WrongType class","text":"

Bases: QueryError

Query result was not of the correct type.

"},{"location":"api/reactive/","title":"Reactive","text":"

The Reactive class implements reactivity.

"},{"location":"api/reactive/#textual.reactive.Reactive","title":"Reactive class","text":"
def __init__(\nself,\ndefault,\n*,\nlayout=False,\nrepaint=True,\ninit=False,\nalways_update=False,\ncompute=True\n):\n

Bases: Generic[ReactiveType]

Reactive descriptor.

Parameters Name Type Description Default default ReactiveType | Callable[[], ReactiveType]

A default value or callable that returns a default.

required layout bool

Perform a layout on change.

False repaint bool

Perform a repaint on change.

True init bool

Call watchers on initialize (post mount).

False always_update bool

Call watchers even when the new value equals the old value.

False compute bool

Run compute methods when attribute is changed.

True"},{"location":"api/reactive/#textual.reactive.TooManyComputesError","title":"TooManyComputesError class","text":"

Bases: Exception

Raised when an attribute has public and private compute methods.

"},{"location":"api/reactive/#textual.reactive.reactive","title":"reactive class","text":"
def __init__(\nself,\ndefault,\n*,\nlayout=False,\nrepaint=True,\ninit=True,\nalways_update=False\n):\n

Bases: Reactive[ReactiveType]

Create a reactive attribute.

Parameters Name Type Description Default default ReactiveType | Callable[[], ReactiveType]

A default value or callable that returns a default.

required layout bool

Perform a layout on change.

False repaint bool

Perform a repaint on change.

True init bool

Call watchers on initialize (post mount).

True always_update bool

Call watchers even when the new value equals the old value.

False"},{"location":"api/reactive/#textual.reactive.var","title":"var class","text":"
def __init__(self, default, init=True, always_update=False):\n

Bases: Reactive[ReactiveType]

Create a reactive attribute (with no auto-refresh).

Parameters Name Type Description Default default ReactiveType | Callable[[], ReactiveType]

A default value or callable that returns a default.

required init bool

Call watchers on initialize (post mount).

True always_update bool

Call watchers even when the new value equals the old value.

False"},{"location":"api/screen/","title":"Screen","text":"

The Screen class is a special widget which represents the content in the terminal. See Screens for details.

"},{"location":"api/screen/#textual.screen.ScreenResultCallbackType","title":"ScreenResultCallbackType module-attribute","text":"
ScreenResultCallbackType = Union[\nCallable[[ScreenResultType], None],\nCallable[[ScreenResultType], Awaitable[None]],\n]\n

Type of a screen result callback function.

"},{"location":"api/screen/#textual.screen.ScreenResultType","title":"ScreenResultType module-attribute","text":"
ScreenResultType = TypeVar('ScreenResultType')\n

The result type of a screen.

"},{"location":"api/screen/#textual.screen.ModalScreen","title":"ModalScreen class","text":"
def __init__(self, name=None, id=None, classes=None):\n

Bases: Screen[ScreenResultType]

A screen with bindings that take precedence over the App's key bindings.

The default styling of a modal screen will dim the screen underneath.

"},{"location":"api/screen/#textual.screen.ResultCallback","title":"ResultCallback class","text":"
def __init__(self, requester, callback):\n

Bases: Generic[ScreenResultType]

Holds the details of a callback.

Parameters Name Type Description Default requester MessagePump

The object making a request for the callback.

required callback ScreenResultCallbackType[ScreenResultType] | None

The callback function.

required"},{"location":"api/screen/#textual.screen.ResultCallback.callback","title":"callback instance-attribute","text":"
callback: ScreenResultCallbackType | None = callback\n

The callback function.

"},{"location":"api/screen/#textual.screen.ResultCallback.requester","title":"requester instance-attribute","text":"
requester = requester\n

The object in the DOM that requested the callback.

"},{"location":"api/screen/#textual.screen.Screen","title":"Screen class","text":"
def __init__(self, name=None, id=None, classes=None):\n

Bases: Generic[ScreenResultType], Widget

The base class for screens.

Parameters Name Type Description Default name str | None

The name of the screen.

None id str | None

The ID of the screen in the DOM.

None classes str | None

The CSS classes for the screen.

None"},{"location":"api/screen/#textual.screen.Screen.AUTO_FOCUS","title":"AUTO_FOCUS class-attribute","text":"
AUTO_FOCUS: str | None = None\n

A selector to determine what to focus automatically when the screen is activated.

The widget focused is the first that matches the given CSS selector. Set to None to inherit the value from the screen's app. Set to \"\" to disable auto focus.

"},{"location":"api/screen/#textual.screen.Screen.COMMANDS","title":"COMMANDS class-attribute","text":"
COMMANDS: set[type[Provider]] = set()\n

Command providers used by the command palette, associated with the screen.

Should be a set of command.Provider classes.

"},{"location":"api/screen/#textual.screen.Screen.CSS","title":"CSS class-attribute","text":"
CSS: str = ''\n

Inline CSS, useful for quick scripts. Rules here take priority over CSS_PATH.

Note

This CSS applies to the whole app.

"},{"location":"api/screen/#textual.screen.Screen.CSS_PATH","title":"CSS_PATH class-attribute","text":"
CSS_PATH: CSSPathType | None = None\n

File paths to load CSS from.

Note

This CSS applies to the whole app.

"},{"location":"api/screen/#textual.screen.Screen.SUB_TITLE","title":"SUB_TITLE class-attribute","text":"
SUB_TITLE: str | None = None\n

A class variable to set the default sub-title for the screen.

This overrides the app sub-title. To update the sub-title while the screen is running, you can set the sub_title attribute.

"},{"location":"api/screen/#textual.screen.Screen.TITLE","title":"TITLE class-attribute","text":"
TITLE: str | None = None\n

A class variable to set the default title for the screen.

This overrides the app title. To update the title while the screen is running, you can set the title attribute.

"},{"location":"api/screen/#textual.screen.Screen.focus_chain","title":"focus_chain property","text":"
focus_chain: list[Widget]\n

A list of widgets that may receive focus, in focus order.

"},{"location":"api/screen/#textual.screen.Screen.focused","title":"focused class-attribute instance-attribute","text":"
focused: Reactive[Widget | None] = Reactive(None)\n

The focused widget or None for no focus.

"},{"location":"api/screen/#textual.screen.Screen.is_current","title":"is_current property","text":"
is_current: bool\n

Is the screen current (i.e. visible to user)?

"},{"location":"api/screen/#textual.screen.Screen.is_modal","title":"is_modal property","text":"
is_modal: bool\n

Is the screen modal?

"},{"location":"api/screen/#textual.screen.Screen.layers","title":"layers property","text":"
layers: tuple[str, ...]\n

Layers from parent.

Returns Type Description tuple[str, ...]

Tuple of layer names.

"},{"location":"api/screen/#textual.screen.Screen.stack_updates","title":"stack_updates class-attribute instance-attribute","text":"
stack_updates: Reactive[int] = Reactive(0, repaint=False)\n

An integer that updates when the screen is resumed.

"},{"location":"api/screen/#textual.screen.Screen.sub_title","title":"sub_title class-attribute instance-attribute","text":"
sub_title: Reactive[str | None] = self.SUB_TITLE\n

Screen sub-title to override the app sub-title.

"},{"location":"api/screen/#textual.screen.Screen.title","title":"title class-attribute instance-attribute","text":"
title: Reactive[str | None] = self.TITLE\n

Screen title to override the app title.

"},{"location":"api/screen/#textual.screen.Screen.action_dismiss","title":"action_dismiss method","text":"
def action_dismiss(self, result=_NoResult):\n

A wrapper around dismiss that can be called as an action.

Parameters Name Type Description Default result ScreenResultType | Type[_NoResult]

The optional result to be passed to the result callback.

_NoResult"},{"location":"api/screen/#textual.screen.Screen.can_view","title":"can_view method","text":"
def can_view(self, widget):\n

Check if a given widget is in the current view (scrollable area).

Note: This doesn't necessarily equate to a widget being visible. There are other reasons why a widget may not be visible.

Parameters Name Type Description Default widget Widget

A widget that is a descendant of self.

required Returns Type Description bool

True if the entire widget is in view, False if it is partially visible or not in view.

"},{"location":"api/screen/#textual.screen.Screen.dismiss","title":"dismiss method","text":"
def dismiss(self, result=_NoResult):\n

Dismiss the screen, optionally with a result.

If result is provided and a callback was set when the screen was pushed, then the callback will be invoked with result.

Parameters Name Type Description Default result ScreenResultType | Type[_NoResult]

The optional result to be passed to the result callback.

_NoResult Raises Type Description ScreenStackError

If trying to dismiss a screen that is not at the top of the stack.

"},{"location":"api/screen/#textual.screen.Screen.find_widget","title":"find_widget method","text":"
def find_widget(self, widget):\n

Get the screen region of a Widget.

Parameters Name Type Description Default widget Widget

A Widget within the composition.

required Returns Type Description MapGeometry

Region relative to screen.

Raises Type Description NoWidget

If the widget could not be found in this screen.

"},{"location":"api/screen/#textual.screen.Screen.focus_next","title":"focus_next method","text":"
def focus_next(self, selector='*'):\n

Focus the next widget, optionally filtered by a CSS selector.

If no widget is currently focused, this will focus the first focusable widget. If no focusable widget matches the given CSS selector, focus is set to None.

Parameters Name Type Description Default selector str | type[QueryType]

CSS selector to filter what nodes can be focused.

'*' Returns Type Description Widget | None

Newly focused widget, or None for no focus. If the return is not None, then it is guaranteed that the widget returned matches the CSS selectors given in the argument.

"},{"location":"api/screen/#textual.screen.Screen.focus_previous","title":"focus_previous method","text":"
def focus_previous(self, selector='*'):\n

Focus the previous widget, optionally filtered by a CSS selector.

If no widget is currently focused, this will focus the first focusable widget. If no focusable widget matches the given CSS selector, focus is set to None.

Parameters Name Type Description Default selector str | type[QueryType]

CSS selector to filter what nodes can be focused.

'*' Returns Type Description Widget | None

Newly focused widget, or None for no focus. If the return is not None, then it is guaranteed that the widget returned matches the CSS selectors given in the argument.

"},{"location":"api/screen/#textual.screen.Screen.get_offset","title":"get_offset method","text":"
def get_offset(self, widget):\n

Get the absolute offset of a given Widget.

Parameters Name Type Description Default widget Widget

A widget

required Returns Type Description Offset

The widget's offset relative to the top left of the terminal.

"},{"location":"api/screen/#textual.screen.Screen.get_style_at","title":"get_style_at method","text":"
def get_style_at(self, x, y):\n

Get the style under a given coordinate.

Parameters Name Type Description Default x int

X Coordinate.

required y int

Y Coordinate.

required Returns Type Description Style

Rich Style object.

"},{"location":"api/screen/#textual.screen.Screen.get_widget_at","title":"get_widget_at method","text":"
def get_widget_at(self, x, y):\n

Get the widget at a given coordinate.

Parameters Name Type Description Default x int

X Coordinate.

required y int

Y Coordinate.

required Returns Type Description tuple[Widget, Region]

Widget and screen region.

"},{"location":"api/screen/#textual.screen.Screen.get_widgets_at","title":"get_widgets_at method","text":"
def get_widgets_at(self, x, y):\n

Get all widgets under a given coordinate.

Parameters Name Type Description Default x int

X coordinate.

required y int

Y coordinate.

required Returns Type Description Iterable[tuple[Widget, Region]]

Sequence of (WIDGET, REGION) tuples.

"},{"location":"api/screen/#textual.screen.Screen.set_focus","title":"set_focus method","text":"
def set_focus(self, widget, scroll_visible=True):\n

Focus (or un-focus) a widget. A focused widget will receive key events first.

Parameters Name Type Description Default widget Widget | None

Widget to focus, or None to un-focus.

required scroll_visible bool

Scroll widget in to view.

True"},{"location":"api/screen/#textual.screen.Screen.validate_sub_title","title":"validate_sub_title method","text":"
def validate_sub_title(self, sub_title):\n

Ensure the sub-title is a string or None.

"},{"location":"api/screen/#textual.screen.Screen.validate_title","title":"validate_title method","text":"
def validate_title(self, title):\n

Ensure the title is a string or None.

"},{"location":"api/scroll_view/","title":"Scroll view","text":"

ScrollView is a base class for line api widgets.

"},{"location":"api/scroll_view/#textual.scroll_view.ScrollView","title":"ScrollView class","text":"

Bases: ScrollableContainer

A base class for a Widget that handles its own scrolling (i.e. doesn't rely on the compositor to render children).

"},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.is_scrollable","title":"is_scrollable property","text":"
is_scrollable: bool\n

Always scrollable.

"},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.refresh_lines","title":"refresh_lines method","text":"
def refresh_lines(self, y_start, line_count=1):\n

Refresh one or more lines.

Parameters Name Type Description Default y_start int

First line to refresh.

required line_count int

Total number of lines to refresh.

1"},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.scroll_to","title":"scroll_to method","text":"
def scroll_to(\nself,\nx=None,\ny=None,\n*,\nanimate=True,\nspeed=None,\nduration=None,\neasing=None,\nforce=False,\non_complete=None\n):\n

Scroll to a given (absolute) coordinate, optionally animating.

Parameters Name Type Description Default x float | None

X coordinate (column) to scroll to, or None for no change.

None y float | None

Y coordinate (row) to scroll to, or None for no change.

None animate bool

Animate to new scroll position.

True speed float | None

Speed of scroll if animate is True; or None to use duration.

None duration float | None

Duration of animation, if animate is True and speed is None.

None easing EasingFunction | str | None

An easing method for the scrolling animation.

None force bool

Force scrolling even when prohibited by overflow styling.

False on_complete CallbackType | None

A callable to invoke when the animation is finished.

None"},{"location":"api/scrollbar/","title":"Scrollbar","text":"

Implements the scrollbar-related widgets for internal use.

You will not need to use the widgets defined in this module.

"},{"location":"api/scrollbar/#textual.scrollbar.ScrollBar","title":"ScrollBar class","text":"
def __init__(self, vertical=True, name=None, *, thickness=1):\n

Bases: Widget

"},{"location":"api/scrollbar/#textual.scrollbar.ScrollBar.renderer","title":"renderer class-attribute","text":"
renderer: Type[ScrollBarRender] = ScrollBarRender\n

The class used for rendering scrollbars. This can be overridden and set to a ScrollBarRender-derived class in order to delegate all scrollbar rendering to that class. E.g.:

class MyScrollBarRender(ScrollBarRender): ...\n\napp = MyApp()\nScrollBar.renderer = MyScrollBarRender\napp.run()\n

Because this variable is accessed through specific instances (rather than through the class ScrollBar itself) it is also possible to set this on specific scrollbar instance to change only that instance:

my_widget.horizontal_scrollbar.renderer = MyScrollBarRender\n
"},{"location":"api/scrollbar/#textual.scrollbar.ScrollBar.action_grab","title":"action_grab method","text":"
def action_grab(self):\n

Begin capturing the mouse cursor.

"},{"location":"api/scrollbar/#textual.scrollbar.ScrollBar.action_scroll_down","title":"action_scroll_down method","text":"
def action_scroll_down(self):\n

Scroll vertical scrollbars down, horizontal scrollbars right.

"},{"location":"api/scrollbar/#textual.scrollbar.ScrollBar.action_scroll_up","title":"action_scroll_up method","text":"
def action_scroll_up(self):\n

Scroll vertical scrollbars up, horizontal scrollbars left.

"},{"location":"api/scrollbar/#textual.scrollbar.ScrollBarCorner","title":"ScrollBarCorner class","text":"
def __init__(self, name=None):\n

Bases: Widget

Widget which fills the gap between horizontal and vertical scrollbars, should they both be present.

"},{"location":"api/scrollbar/#textual.scrollbar.ScrollDown","title":"ScrollDown class","text":"

Bases: ScrollMessage

Message sent when clicking below handle.

"},{"location":"api/scrollbar/#textual.scrollbar.ScrollLeft","title":"ScrollLeft class","text":"

Bases: ScrollMessage

Message sent when clicking above handle.

"},{"location":"api/scrollbar/#textual.scrollbar.ScrollMessage","title":"ScrollMessage class","text":"

Bases: Message

Base class for all scrollbar messages.

"},{"location":"api/scrollbar/#textual.scrollbar.ScrollRight","title":"ScrollRight class","text":"

Bases: ScrollMessage

Message sent when clicking below handle.

"},{"location":"api/scrollbar/#textual.scrollbar.ScrollTo","title":"ScrollTo class","text":"
def __init__(self, x=None, y=None, animate=True):\n

Bases: ScrollMessage

Message sent when click and dragging handle.

"},{"location":"api/scrollbar/#textual.scrollbar.ScrollUp","title":"ScrollUp class","text":"

Bases: ScrollMessage

Message sent when clicking above handle.

"},{"location":"api/strip/","title":"Strip","text":"

A Strip contains the result of rendering a widget. See line API for how to use Strips.

"},{"location":"api/strip/#textual.strip.Strip","title":"Strip class","text":"
def __init__(self, segments, cell_length=None):\n

Represents a 'strip' (horizontal line) of a Textual Widget.

A Strip is like an immutable list of Segments. The immutability allows for effective caching.

Parameters Name Type Description Default segments Iterable[Segment]

An iterable of segments.

required cell_length int | None

The cell length if known, or None to calculate on demand.

None"},{"location":"api/strip/#textual.strip.Strip.cell_length","title":"cell_length property","text":"
cell_length: int\n

Get the number of cells required to render this object.

"},{"location":"api/strip/#textual.strip.Strip.link_ids","title":"link_ids property","text":"
link_ids: set[str]\n

A set of the link ids in this Strip.

"},{"location":"api/strip/#textual.strip.Strip.text","title":"text property","text":"
text: str\n

Segment text.

"},{"location":"api/strip/#textual.strip.Strip.adjust_cell_length","title":"adjust_cell_length method","text":"
def adjust_cell_length(self, cell_length, style=None):\n

Adjust the cell length, possibly truncating or extending.

Parameters Name Type Description Default cell_length int

New desired cell length.

required style Style | None

Style when extending, or None.

None Returns Type Description Strip

A new strip with the supplied cell length.

"},{"location":"api/strip/#textual.strip.Strip.apply_filter","title":"apply_filter method","text":"
def apply_filter(self, filter, background):\n

Apply a filter to all segments in the strip.

Parameters Name Type Description Default filter LineFilter

A line filter object.

required Returns Type Description Strip

A new Strip.

"},{"location":"api/strip/#textual.strip.Strip.apply_style","title":"apply_style method","text":"
def apply_style(self, style):\n

Apply a style to the Strip.

Parameters Name Type Description Default style Style

A Rich style.

required Returns Type Description Strip

A new strip.

"},{"location":"api/strip/#textual.strip.Strip.blank","title":"blank classmethod","text":"
def blank(cls, cell_length, style=None):\n

Create a blank strip.

Parameters Name Type Description Default cell_length int

Desired cell length.

required style StyleType | None

Style of blank.

None Returns Type Description Strip

New strip.

"},{"location":"api/strip/#textual.strip.Strip.crop","title":"crop method","text":"
def crop(self, start, end=None):\n

Crop a strip between two cell positions.

Parameters Name Type Description Default start int

The start cell position (inclusive).

required end int | None

The end cell position (exclusive).

None Returns Type Description Strip

A new Strip.

"},{"location":"api/strip/#textual.strip.Strip.crop_extend","title":"crop_extend method","text":"
def crop_extend(self, start, end, style):\n

Crop between two points, extending the length if required.

Parameters Name Type Description Default start int

Start offset of crop.

required end int

End offset of crop.

required style Style | None

Style of additional padding.

required Returns Type Description Strip

New cropped Strip.

"},{"location":"api/strip/#textual.strip.Strip.divide","title":"divide method","text":"
def divide(self, cuts):\n

Divide the strip in to multiple smaller strips by cutting at given (cell) indices.

Parameters Name Type Description Default cuts Iterable[int]

An iterable of cell positions as ints.

required Returns Type Description Sequence[Strip]

A new list of strips.

"},{"location":"api/strip/#textual.strip.Strip.extend_cell_length","title":"extend_cell_length method","text":"
def extend_cell_length(self, cell_length, style=None):\n

Extend the cell length if it is less than the given value.

Parameters Name Type Description Default cell_length int

Required minimum cell length.

required style Style | None

Style for padding if the cell length is extended.

None Returns Type Description Strip

A new Strip.

"},{"location":"api/strip/#textual.strip.Strip.from_lines","title":"from_lines classmethod","text":"
def from_lines(cls, lines, cell_length=None):\n

Convert lines (lists of segments) to a list of Strips.

Parameters Name Type Description Default lines list[list[Segment]]

List of lines, where a line is a list of segments.

required cell_length int | None

Cell length of lines (must be same) or None if not known.

None Returns Type Description list[Strip]

List of strips.

"},{"location":"api/strip/#textual.strip.Strip.index_to_cell_position","title":"index_to_cell_position method","text":"
def index_to_cell_position(self, index):\n

Given a character index, return the cell position of that character. This is the sum of the cell lengths of all the characters before the character at index.

Parameters Name Type Description Default index int

The index to convert.

required Returns Type Description int

The cell position of the character at index.

"},{"location":"api/strip/#textual.strip.Strip.join","title":"join classmethod","text":"
def join(cls, strips):\n

Join a number of strips in to one.

Parameters Name Type Description Default strips Iterable[Strip | None]

An iterable of Strips.

required Returns Type Description Strip

A new combined strip.

"},{"location":"api/strip/#textual.strip.Strip.simplify","title":"simplify method","text":"
def simplify(self):\n

Simplify the segments (join segments with same style)

Returns Type Description Strip

New strip.

"},{"location":"api/strip/#textual.strip.Strip.style_links","title":"style_links method","text":"
def style_links(self, link_id, link_style):\n

Apply a style to Segments with the given link_id.

Parameters Name Type Description Default link_id str

A link id.

required link_style Style

Style to apply.

required Returns Type Description Strip

New strip (or same Strip if no changes).

"},{"location":"api/strip/#textual.strip.StripRenderable","title":"StripRenderable class","text":"
def __init__(self, strips):\n

A renderable which renders a list of strips in to lines.

"},{"location":"api/strip/#textual.strip.get_line_length","title":"get_line_length function","text":"
def get_line_length(segments):\n

Get the line length (total length of all segments).

Parameters Name Type Description Default segments Iterable[Segment]

Iterable of segments.

required Returns Type Description int

Length of line in cells.

"},{"location":"api/suggester/","title":"Suggester","text":"

The Suggester class is used by the Input widget.

"},{"location":"api/suggester/#textual.suggester.SuggestFromList","title":"SuggestFromList class","text":"
def __init__(self, suggestions, *, case_sensitive=True):\n

Bases: Suggester

Give completion suggestions based on a fixed list of options.

Example
countries = [\"England\", \"Scotland\", \"Portugal\", \"Spain\", \"France\"]\nclass MyApp(App[None]):\ndef compose(self) -> ComposeResult:\nyield Input(suggester=SuggestFromList(countries, case_sensitive=False))\n

If the user types P inside the input widget, a completion suggestion for \"Portugal\" appears.

Parameters Name Type Description Default suggestions Iterable[str]

Valid suggestions sorted by decreasing priority.

required case_sensitive bool

Whether suggestions are computed in a case sensitive manner or not. The values provided in the argument suggestions represent the canonical representation of the completions and they will be suggested with that same casing.

True"},{"location":"api/suggester/#textual.suggester.SuggestFromList.get_suggestion","title":"get_suggestion async","text":"
def get_suggestion(self, value):\n

Gets a completion from the given possibilities.

Parameters Name Type Description Default value str

The current value.

required Returns Type Description str | None

A valid completion suggestion or None.

"},{"location":"api/suggester/#textual.suggester.Suggester","title":"Suggester class","text":"
def __init__(self, *, use_cache=True, case_sensitive=False):\n

Bases: ABC

Defines how widgets generate completion suggestions.

To define a custom suggester, subclass Suggester and implement the async method get_suggestion. See SuggestFromList for an example.

Parameters Name Type Description Default use_cache bool

Whether to cache suggestion results.

True case_sensitive bool

Whether suggestions are case sensitive or not. If they are not, incoming values are casefolded before generating the suggestion.

False"},{"location":"api/suggester/#textual.suggester.Suggester.cache","title":"cache instance-attribute","text":"
cache: LRUCache[str, str | None] | None = (\nLRUCache(1024) if use_cache else None\n)\n

Suggestion cache, if used.

"},{"location":"api/suggester/#textual.suggester.Suggester.get_suggestion","title":"get_suggestion abstractmethod async","text":"
def get_suggestion(self, value):\n

Try to get a completion suggestion for the given input value.

Custom suggesters should implement this method.

Note

The value argument will be casefolded if self.case_sensitive is False.

Note

If your implementation is not deterministic, you may need to disable caching.

Parameters Name Type Description Default value str

The current value of the requester widget.

required Returns Type Description str | None

A valid suggestion or None.

"},{"location":"api/suggester/#textual.suggester.SuggestionReady","title":"SuggestionReady class","text":"

Bases: Message

Sent when a completion suggestion is ready.

"},{"location":"api/suggester/#textual.suggester.SuggestionReady.suggestion","title":"suggestion instance-attribute","text":"
suggestion: str\n

The string suggestion.

"},{"location":"api/suggester/#textual.suggester.SuggestionReady.value","title":"value instance-attribute","text":"
value: str\n

The value to which the suggestion is for.

"},{"location":"api/system_commands_source/","title":"System commands source","text":"

A command palette command provider for Textual system commands.

This is a simple command provider that makes the most obvious application actions available via the command palette.

"},{"location":"api/system_commands_source/#textual._system_commands.SystemCommands","title":"SystemCommands class","text":"

Bases: Provider

A source of command palette commands that run app-wide tasks.

Used by default in App.COMMANDS.

"},{"location":"api/system_commands_source/#textual._system_commands.SystemCommands.search","title":"search async","text":"
def search(self, query):\n

Handle a request to search for system commands that match the query.

Parameters Name Type Description Default user_input

The user input to be matched.

required

Yields:

Type Description Hits

Command hits for use in the command palette.

"},{"location":"api/timer/","title":"Timer","text":"

Timer objects are created by set_interval or set_timer.

"},{"location":"api/timer/#textual.timer.TimerCallback","title":"TimerCallback module-attribute","text":"
TimerCallback = Union[\nCallable[[], Awaitable[None]], Callable[[], None]\n]\n

Type of valid callbacks to be used with timers.

"},{"location":"api/timer/#textual.timer.Timer","title":"Timer class","text":"
def __init__(\nself,\nevent_target,\ninterval,\n*,\nname=None,\ncallback=None,\nrepeat=None,\nskip=True,\npause=False\n):\n

A class to send timer-based events.

Parameters Name Type Description Default event_target MessageTarget

The object which will receive the timer events.

required interval float

The time between timer events, in seconds.

required name str | None

A name to assign the event (for debugging).

None callback TimerCallback | None

A optional callback to invoke when the event is handled.

None repeat int | None

The number of times to repeat the timer, or None to repeat forever.

None skip bool

Enable skipping of scheduled events that couldn't be sent in time.

True pause bool

Start the timer paused.

False"},{"location":"api/timer/#textual.timer.Timer.pause","title":"pause method","text":"
def pause(self):\n

Pause the timer.

A paused timer will not send events until it is resumed.

"},{"location":"api/timer/#textual.timer.Timer.reset","title":"reset method","text":"
def reset(self):\n

Reset the timer, so it starts from the beginning.

"},{"location":"api/timer/#textual.timer.Timer.resume","title":"resume method","text":"
def resume(self):\n

Resume a paused timer.

"},{"location":"api/timer/#textual.timer.Timer.stop","title":"stop method","text":"
def stop(self):\n

Stop the timer.

"},{"location":"api/types/","title":"Types","text":"

Export some objects that are used by Textual and that help document other features.

"},{"location":"api/types/#textual.types.ActionParseResult","title":"ActionParseResult module-attribute","text":"
ActionParseResult: TypeAlias = \"tuple[str, tuple[Any, ...]]\"\n

An action is its name and the arbitrary tuple of its arguments.

"},{"location":"api/types/#textual.types.CSSPathType","title":"CSSPathType module-attribute","text":"
CSSPathType: TypeAlias = Union[\nstr, PurePath, List[Union[str, PurePath]]\n]\n

Valid ways of specifying paths to CSS files.

"},{"location":"api/types/#textual.types.CallbackType","title":"CallbackType module-attribute","text":"
CallbackType = Union[\nCallable[[], Awaitable[None]], Callable[[], None]\n]\n

Type used for arbitrary callables used in callbacks.

"},{"location":"api/types/#textual.types.CursorType","title":"CursorType module-attribute","text":"
CursorType = Literal['cell', 'row', 'column', 'none']\n

The valid types of cursors for DataTable.cursor_type.

"},{"location":"api/types/#textual.types.EasingFunction","title":"EasingFunction module-attribute","text":"
EasingFunction = Callable[[float], float]\n

Signature for a function that parametrises animation speed.

An easing function must map the interval [0, 1] into the interval [0, 1].

"},{"location":"api/types/#textual.types.IgnoreReturnCallbackType","title":"IgnoreReturnCallbackType module-attribute","text":"
IgnoreReturnCallbackType = Union[\nCallable[[], Awaitable[Any]], Callable[[], Any]\n]\n

A callback which ignores the return type.

"},{"location":"api/types/#textual.types.InputValidationOn","title":"InputValidationOn module-attribute","text":"
InputValidationOn = Literal['blur', 'changed', 'submitted']\n

Possible messages that trigger input validation.

"},{"location":"api/types/#textual.types.WatchCallbackType","title":"WatchCallbackType module-attribute","text":"
WatchCallbackType = Union[\nCallable[[], Awaitable[None]],\nCallable[[Any], Awaitable[None]],\nCallable[[Any, Any], Awaitable[None]],\nCallable[[], None],\nCallable[[Any], None],\nCallable[[Any, Any], None],\n]\n

Type used for callbacks passed to the watch method of widgets.

"},{"location":"api/types/#textual.types.Animatable","title":"Animatable class","text":"

Bases: Protocol

Protocol for objects that can have their intrinsic values animated.

For example, the transition between two colors can be animated because the class Color satisfies this protocol.

"},{"location":"api/types/#textual.types.CSSPathError","title":"CSSPathError class","text":"

Bases: Exception

Raised when supplied CSS path(s) are invalid.

"},{"location":"api/types/#textual.types.MessageTarget","title":"MessageTarget class","text":"

Bases: Protocol

Protocol that must be followed by objects that can receive messages.

"},{"location":"api/types/#textual.types.NoActiveAppError","title":"NoActiveAppError class","text":"

Bases: RuntimeError

Runtime error raised if we try to retrieve the active app when there is none.

"},{"location":"api/types/#textual.types.RenderStyles","title":"RenderStyles class","text":"
def __init__(self, node, base, inline_styles):\n

Bases: StylesBase

Presents a combined view of two Styles object: a base Styles and inline Styles.

"},{"location":"api/types/#textual.css.styles.RenderStyles.base","title":"base property","text":"
base: Styles\n

Quick access to base (css) style.

"},{"location":"api/types/#textual.css.styles.RenderStyles.css","title":"css property","text":"
css: str\n

Get the CSS for the combined styles.

"},{"location":"api/types/#textual.css.styles.RenderStyles.gutter","title":"gutter property","text":"
gutter: Spacing\n

Get space around widget.

Returns Type Description Spacing

Space around widget content.

"},{"location":"api/types/#textual.css.styles.RenderStyles.inline","title":"inline property","text":"
inline: Styles\n

Quick access to the inline styles.

"},{"location":"api/types/#textual.css.styles.RenderStyles.rich_style","title":"rich_style property","text":"
rich_style: Style\n

Get a Rich style for this Styles object.

"},{"location":"api/types/#textual.css.styles.RenderStyles.animate","title":"animate method","text":"
def animate(\nself,\nattribute,\nvalue,\n*,\nfinal_value=Ellipsis,\nduration=None,\nspeed=None,\ndelay=0.0,\neasing=DEFAULT_EASING,\non_complete=None\n):\n

Animate an attribute.

Parameters Name Type Description Default attribute str

Name of the attribute to animate.

required value str | float | Animatable

The value to animate to.

required final_value object

The final value of the animation. Defaults to value if not set.

Ellipsis duration float | None

The duration of the animate.

None speed float | None

The speed of the animation.

None delay float

A delay (in seconds) before the animation starts.

0.0 easing EasingFunction | str

An easing method.

DEFAULT_EASING on_complete CallbackType | None

A callable to invoke when the animation is finished.

None"},{"location":"api/types/#textual.css.styles.RenderStyles.clear_rule","title":"clear_rule method","text":"
def clear_rule(self, rule_name):\n

Clear a rule (from inline).

"},{"location":"api/types/#textual.css.styles.RenderStyles.get_rules","title":"get_rules method","text":"
def get_rules(self):\n

Get rules as a dictionary

"},{"location":"api/types/#textual.css.styles.RenderStyles.has_rule","title":"has_rule method","text":"
def has_rule(self, rule):\n

Check if a rule has been set.

"},{"location":"api/types/#textual.css.styles.RenderStyles.merge","title":"merge method","text":"
def merge(self, other):\n

Merge values from another Styles.

Parameters Name Type Description Default other StylesBase

A Styles object.

required"},{"location":"api/types/#textual.css.styles.RenderStyles.reset","title":"reset method","text":"
def reset(self):\n

Reset the rules to initial state.

"},{"location":"api/types/#textual.types.UnusedParameter","title":"UnusedParameter class","text":"

Helper type for a parameter that isn't specified in a method call.

"},{"location":"api/validation/","title":"Validation","text":"

Framework for validating string values

"},{"location":"api/validation/#textual.validation.Failure","title":"Failure class","text":"

Information about a validation failure.

"},{"location":"api/validation/#textual.validation.Failure.description","title":"description class-attribute instance-attribute","text":"
description: str | None = None\n

An optional override for describing this failure. Takes precedence over any messages set in the Validator.

"},{"location":"api/validation/#textual.validation.Failure.validator","title":"validator instance-attribute","text":"
validator: Validator\n

The Validator which produced the failure.

"},{"location":"api/validation/#textual.validation.Failure.value","title":"value class-attribute instance-attribute","text":"
value: str | None = None\n

The value which resulted in validation failing.

"},{"location":"api/validation/#textual.validation.Function","title":"Function class","text":"
def __init__(self, function, failure_description=None):\n

Bases: Validator

A flexible validator which allows you to provide custom validation logic.

"},{"location":"api/validation/#textual.validation.Function.function","title":"function instance-attribute","text":"
function = function\n

Function which takes the value to validate and returns True if valid, and False otherwise.

"},{"location":"api/validation/#textual.validation.Function.ReturnedFalse","title":"ReturnedFalse class","text":"

Bases: Failure

Indicates validation failed because the supplied function returned False.

"},{"location":"api/validation/#textual.validation.Function.describe_failure","title":"describe_failure method","text":"
def describe_failure(self, failure):\n

Describes why the validator failed.

Parameters Name Type Description Default failure Failure

Information about why the validation failed.

required Returns Type Description str | None

A string description of the failure.

"},{"location":"api/validation/#textual.validation.Function.validate","title":"validate method","text":"
def validate(self, value):\n

Validate that the supplied function returns True.

Parameters Name Type Description Default value str

The value to pass into the supplied function.

required Returns Type Description ValidationResult

A ValidationResult indicating success if the function returned True, and failure if the function return False.

"},{"location":"api/validation/#textual.validation.Integer","title":"Integer class","text":"

Bases: Number

Validator which ensures the value is an integer which falls within a range.

"},{"location":"api/validation/#textual.validation.Integer.NotAnInteger","title":"NotAnInteger class","text":"

Bases: Failure

Indicates a failure due to the value not being a valid integer.

"},{"location":"api/validation/#textual.validation.Integer.describe_failure","title":"describe_failure method","text":"
def describe_failure(self, failure):\n

Describes why the validator failed.

Parameters Name Type Description Default failure Failure

Information about why the validation failed.

required Returns Type Description str | None

A string description of the failure.

"},{"location":"api/validation/#textual.validation.Integer.validate","title":"validate method","text":"
def validate(self, value):\n

Ensure that value is an integer, optionally within a range.

Parameters Name Type Description Default value str

The value to validate.

required Returns Type Description ValidationResult

The result of the validation.

"},{"location":"api/validation/#textual.validation.Length","title":"Length class","text":"
def __init__(\nself,\nminimum=None,\nmaximum=None,\nfailure_description=None,\n):\n

Bases: Validator

Validate that a string is within a range (inclusive).

"},{"location":"api/validation/#textual.validation.Length.maximum","title":"maximum instance-attribute","text":"
maximum = maximum\n

The inclusive maximum length of the value, or None if unbounded.

"},{"location":"api/validation/#textual.validation.Length.minimum","title":"minimum instance-attribute","text":"
minimum = minimum\n

The inclusive minimum length of the value, or None if unbounded.

"},{"location":"api/validation/#textual.validation.Length.Incorrect","title":"Incorrect class","text":"

Bases: Failure

Indicates a failure due to the length of the value being outside the range.

"},{"location":"api/validation/#textual.validation.Length.describe_failure","title":"describe_failure method","text":"
def describe_failure(self, failure):\n

Describes why the validator failed.

Parameters Name Type Description Default failure Failure

Information about why the validation failed.

required Returns Type Description str | None

A string description of the failure.

"},{"location":"api/validation/#textual.validation.Length.validate","title":"validate method","text":"
def validate(self, value):\n

Ensure that value falls within the maximum and minimum length constraints.

Parameters Name Type Description Default value str

The value to validate.

required Returns Type Description ValidationResult

The result of the validation.

"},{"location":"api/validation/#textual.validation.Number","title":"Number class","text":"
def __init__(\nself,\nminimum=None,\nmaximum=None,\nfailure_description=None,\n):\n

Bases: Validator

Validator that ensures the value is a number, with an optional range check.

"},{"location":"api/validation/#textual.validation.Number.maximum","title":"maximum instance-attribute","text":"
maximum = maximum\n

The maximum value of the number, inclusive. If None, the maximum is unbounded.

"},{"location":"api/validation/#textual.validation.Number.minimum","title":"minimum instance-attribute","text":"
minimum = minimum\n

The minimum value of the number, inclusive. If None, the minimum is unbounded.

"},{"location":"api/validation/#textual.validation.Number.NotANumber","title":"NotANumber class","text":"

Bases: Failure

Indicates a failure due to the value not being a valid number (decimal/integer, inc. scientific notation)

"},{"location":"api/validation/#textual.validation.Number.NotInRange","title":"NotInRange class","text":"

Bases: Failure

Indicates a failure due to the number not being within the range [minimum, maximum].

"},{"location":"api/validation/#textual.validation.Number.describe_failure","title":"describe_failure method","text":"
def describe_failure(self, failure):\n

Describes why the validator failed.

Parameters Name Type Description Default failure Failure

Information about why the validation failed.

required Returns Type Description str | None

A string description of the failure.

"},{"location":"api/validation/#textual.validation.Number.validate","title":"validate method","text":"
def validate(self, value):\n

Ensure that value is a valid number, optionally within a range.

Parameters Name Type Description Default value str

The value to validate.

required Returns Type Description ValidationResult

The result of the validation.

"},{"location":"api/validation/#textual.validation.Regex","title":"Regex class","text":"
def __init__(self, regex, flags=0, failure_description=None):\n

Bases: Validator

A validator that checks the value matches a regex (via re.fullmatch).

"},{"location":"api/validation/#textual.validation.Regex.flags","title":"flags instance-attribute","text":"
flags = flags\n

The flags to pass to re.fullmatch.

"},{"location":"api/validation/#textual.validation.Regex.regex","title":"regex instance-attribute","text":"
regex = regex\n

The regex which we'll validate is matched by the value.

"},{"location":"api/validation/#textual.validation.Regex.NoResults","title":"NoResults class","text":"

Bases: Failure

Indicates validation failed because the regex could not be found within the value string.

"},{"location":"api/validation/#textual.validation.Regex.describe_failure","title":"describe_failure method","text":"
def describe_failure(self, failure):\n

Describes why the validator failed.

Parameters Name Type Description Default failure Failure

Information about why the validation failed.

required Returns Type Description str | None

A string description of the failure.

"},{"location":"api/validation/#textual.validation.Regex.validate","title":"validate method","text":"
def validate(self, value):\n

Ensure that the value matches the regex.

Parameters Name Type Description Default value str

The value that should match the regex.

required Returns Type Description ValidationResult

The result of the validation.

"},{"location":"api/validation/#textual.validation.URL","title":"URL class","text":"

Bases: Validator

Validator that checks if a URL is valid (ensuring a scheme is present).

"},{"location":"api/validation/#textual.validation.URL.InvalidURL","title":"InvalidURL class","text":"

Bases: Failure

Indicates that the URL is not valid.

"},{"location":"api/validation/#textual.validation.URL.describe_failure","title":"describe_failure method","text":"
def describe_failure(self, failure):\n

Describes why the validator failed.

Parameters Name Type Description Default failure Failure

Information about why the validation failed.

required Returns Type Description str | None

A string description of the failure.

"},{"location":"api/validation/#textual.validation.URL.validate","title":"validate method","text":"
def validate(self, value):\n

Validates that value is a valid URL (contains a scheme).

Parameters Name Type Description Default value str

The value to validate.

required Returns Type Description ValidationResult

The result of the validation.

"},{"location":"api/validation/#textual.validation.ValidationResult","title":"ValidationResult class","text":"

The result of calling a Validator.validate method.

"},{"location":"api/validation/#textual.validation.ValidationResult.failure_descriptions","title":"failure_descriptions property","text":"
failure_descriptions: list[str]\n

Utility for extracting failure descriptions as strings.

Useful if you don't care about the additional metadata included in the Failure objects.

Returns Type Description list[str]

A list of the string descriptions explaining the failing validations.

"},{"location":"api/validation/#textual.validation.ValidationResult.failures","title":"failures class-attribute instance-attribute","text":"
failures: Sequence[Failure] = field(default_factory=list)\n

A list of reasons why the value was invalid. Empty if valid=True

"},{"location":"api/validation/#textual.validation.ValidationResult.is_valid","title":"is_valid property","text":"
is_valid: bool\n

True if the validation was successful.

"},{"location":"api/validation/#textual.validation.ValidationResult.failure","title":"failure staticmethod","text":"
def failure(failures):\n

Construct a failure ValidationResult.

Parameters Name Type Description Default failures Sequence[Failure]

The failures.

required Returns Type Description ValidationResult

A failure ValidationResult.

"},{"location":"api/validation/#textual.validation.ValidationResult.merge","title":"merge staticmethod","text":"
def merge(results):\n

Merge multiple ValidationResult objects into one.

Parameters Name Type Description Default results Sequence['ValidationResult']

List of ValidationResult objects to merge.

required Returns Type Description 'ValidationResult'

Merged ValidationResult object.

"},{"location":"api/validation/#textual.validation.ValidationResult.success","title":"success staticmethod","text":"
def success():\n

Construct a successful ValidationResult.

Returns Type Description ValidationResult

A successful ValidationResult.

"},{"location":"api/validation/#textual.validation.Validator","title":"Validator class","text":"
def __init__(self, failure_description=None):\n

Bases: ABC

Base class for the validation of string values.

Commonly used in conjunction with the Input widget, which accepts a list of validators via its constructor. This validation framework can also be used to validate any 'stringly-typed' values (for example raw command line input from sys.args).

To implement your own Validator, subclass this class.

Example
class Palindrome(Validator):\ndef validate(self, value: str) -> ValidationResult:\ndef is_palindrome(value: str) -> bool:\nreturn value == value[::-1]\nreturn self.success() if is_palindrome(value) else self.failure(\"Not palindrome!\")\n
"},{"location":"api/validation/#textual.validation.Validator.failure_description","title":"failure_description instance-attribute","text":"
failure_description = failure_description\n

A description of why the validation failed.

The description (intended to be user-facing) to attached to the Failure if the validation fails. This failure description is ultimately accessible at the time of validation failure via the Input.Changed or Input.Submitted event, and you can access it on your message handler (a method called, for example, on_input_changed or a method decorated with @on(Input.Changed).

"},{"location":"api/validation/#textual.validation.Validator.describe_failure","title":"describe_failure method","text":"
def describe_failure(self, failure):\n

Return a string description of the Failure.

Used to provide a more fine-grained description of the failure. A Validator could fail for multiple reasons, so this method could be used to provide a different reason for different types of failure.

Warning

This method is only called if no other description has been supplied. If you supply a description inside a call to self.failure(description=\"...\"), or pass a description into the constructor of the validator, those will take priority, and this method won't be called.

Parameters Name Type Description Default failure Failure

Information about why the validation failed.

required Returns Type Description str | None

A string description of the failure.

"},{"location":"api/validation/#textual.validation.Validator.failure","title":"failure method","text":"
def failure(self, description=None, value=None, failures=None):\n

Shorthand for signaling validation failure.

You can return failure(...) from a Validator.validate implementation to signal validation succeeded.

Parameters Name Type Description Default description str | None

The failure description that will be used. When used in conjunction with the Input widget, this is the description that will ultimately be available inside the handler for Input.Changed. If not supplied, the failure_description from the Validator will be used. If that is not supplied either, then the describe_failure method on Validator will be called.

None value str | None

The value that was considered invalid. This is optional, and only needs to be supplied if required in your Input.Changed handler.

None failures Failure | Sequence[Failure] | None

The reasons the validator failed. If not supplied, a generic Failure will be included in the ValidationResult returned from this function.

None Returns Type Description ValidationResult

A ValidationResult representing failed validation, and containing the metadata supplied to this function.

"},{"location":"api/validation/#textual.validation.Validator.success","title":"success method","text":"
def success(self):\n

Shorthand for ValidationResult(True).

You can return success() from a Validator.validate method implementation to signal that validation has succeeded.

Returns Type Description ValidationResult

A ValidationResult indicating validation succeeded.

"},{"location":"api/validation/#textual.validation.Validator.validate","title":"validate abstractmethod","text":"
def validate(self, value):\n

Validate the value and return a ValidationResult describing the outcome of the validation.

Parameters Name Type Description Default value str

The value to validate.

required Returns Type Description ValidationResult

The result of the validation.

"},{"location":"api/walk/","title":"Walk","text":"

Functions for walking the DOM.

Note

For most purposes you would be better off using query, which uses these functions internally.

"},{"location":"api/walk/#textual.walk.walk_breadth_first","title":"walk_breadth_first function","text":"
def walk_breadth_first(\nroot, filter_type=None, *, with_root=True\n):\n

Walk the tree breadth first (children first).

Note

Avoid changing the DOM (mounting, removing etc.) while iterating with this function. Consider walk_children which doesn't have this limitation.

Parameters Name Type Description Default root DOMNode

The root note (starting point).

required filter_type type[WalkType] | None

Optional DOMNode subclass to filter by, or None for no filter.

None with_root bool

Include the root in the walk.

True Returns Type Description Iterable[DOMNode] | Iterable[WalkType]

An iterable of DOMNodes, or the type specified in filter_type.

"},{"location":"api/walk/#textual.walk.walk_depth_first","title":"walk_depth_first function","text":"
def walk_depth_first(root, filter_type=None, *, with_root=True):\n

Walk the tree depth first (parents first).

Note

Avoid changing the DOM (mounting, removing etc.) while iterating with this function. Consider walk_children which doesn't have this limitation.

Parameters Name Type Description Default root DOMNode

The root note (starting point).

required filter_type type[WalkType] | None

Optional DOMNode subclass to filter by, or None for no filter.

None with_root bool

Include the root in the walk.

True Returns Type Description Iterable[DOMNode] | Iterable[WalkType]

An iterable of DOMNodes, or the type specified in filter_type.

"},{"location":"api/widget/","title":"Widget","text":"

The base class for widgets.

"},{"location":"api/widget/#textual.widget.AwaitMount","title":"AwaitMount class","text":"
def __init__(self, parent, widgets):\n

An optional awaitable returned by mount and mount_all.

Example
await self.mount(Static(\"foo\"))\n
"},{"location":"api/widget/#textual.widget.MountError","title":"MountError class","text":"

Bases: WidgetError

Error raised when there was a problem with the mount request.

"},{"location":"api/widget/#textual.widget.PseudoClasses","title":"PseudoClasses class","text":"

Bases: NamedTuple

Used for render/render_line based widgets that use caching. This structure can be used as a cache-key.

"},{"location":"api/widget/#textual.widget.PseudoClasses.enabled","title":"enabled instance-attribute","text":"
enabled: bool\n

Is 'enabled' applied?

"},{"location":"api/widget/#textual.widget.PseudoClasses.focus","title":"focus instance-attribute","text":"
focus: bool\n

Is 'focus' applied?

"},{"location":"api/widget/#textual.widget.PseudoClasses.hover","title":"hover instance-attribute","text":"
hover: bool\n

Is 'hover' applied?

"},{"location":"api/widget/#textual.widget.Widget","title":"Widget class","text":"
def __init__(\nself,\n*children,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: DOMNode

A Widget is the base class for Textual widgets.

See also static for starting point for your own widgets.

Parameters Name Type Description Default *children Widget

Child widgets.

() name str | None

The name of the widget.

None id str | None

The ID of the widget in the DOM.

None classes str | None

The CSS classes for the widget.

None disabled bool

Whether the widget is disabled or not.

False"},{"location":"api/widget/#textual.widget.Widget.BORDER_SUBTITLE","title":"BORDER_SUBTITLE class-attribute","text":"
BORDER_SUBTITLE: str = ''\n

Initial value for border_subtitle attribute.

"},{"location":"api/widget/#textual.widget.Widget.BORDER_TITLE","title":"BORDER_TITLE class-attribute","text":"
BORDER_TITLE: str = ''\n

Initial value for border_title attribute.

"},{"location":"api/widget/#textual.widget.Widget.allow_horizontal_scroll","title":"allow_horizontal_scroll property","text":"
allow_horizontal_scroll: bool\n

Check if horizontal scroll is permitted.

May be overridden if you want different logic regarding allowing scrolling.

"},{"location":"api/widget/#textual.widget.Widget.allow_vertical_scroll","title":"allow_vertical_scroll property","text":"
allow_vertical_scroll: bool\n

Check if vertical scroll is permitted.

May be overridden if you want different logic regarding allowing scrolling.

"},{"location":"api/widget/#textual.widget.Widget.auto_links","title":"auto_links class-attribute instance-attribute","text":"
auto_links: Reactive[bool] = Reactive(True)\n

Widget will highlight links automatically.

"},{"location":"api/widget/#textual.widget.Widget.border_subtitle","title":"border_subtitle class-attribute instance-attribute","text":"
border_subtitle: str | Text | None = _BorderTitle()\n

A title to show in the bottom border (if there is one).

"},{"location":"api/widget/#textual.widget.Widget.border_title","title":"border_title class-attribute instance-attribute","text":"
border_title: str | Text | None = _BorderTitle()\n

A title to show in the top border (if there is one).

"},{"location":"api/widget/#textual.widget.Widget.can_focus","title":"can_focus class-attribute instance-attribute","text":"
can_focus: bool = False\n

Widget may receive focus.

"},{"location":"api/widget/#textual.widget.Widget.can_focus_children","title":"can_focus_children class-attribute instance-attribute","text":"
can_focus_children: bool = True\n

Widget's children may receive focus.

"},{"location":"api/widget/#textual.widget.Widget.container_size","title":"container_size property","text":"
container_size: Size\n

The size of the container (parent widget).

Returns Type Description Size

Container size.

"},{"location":"api/widget/#textual.widget.Widget.container_viewport","title":"container_viewport property","text":"
container_viewport: Region\n

The viewport region (parent window).

Returns Type Description Region

The region that contains this widget.

"},{"location":"api/widget/#textual.widget.Widget.content_offset","title":"content_offset property","text":"
content_offset: Offset\n

An offset from the Widget origin where the content begins.

Returns Type Description Offset

Offset from widget's origin.

"},{"location":"api/widget/#textual.widget.Widget.content_region","title":"content_region property","text":"
content_region: Region\n

Gets an absolute region containing the content (minus padding and border).

Returns Type Description Region

Screen region that contains a widget's content.

"},{"location":"api/widget/#textual.widget.Widget.content_size","title":"content_size property","text":"
content_size: Size\n

The size of the content area.

Returns Type Description Size

Content area size.

"},{"location":"api/widget/#textual.widget.Widget.disabled","title":"disabled class-attribute instance-attribute","text":"
disabled: Reactive[bool] = disabled\n

Is the widget disabled? Disabled widgets can not be interacted with, and are typically styled to look dimmer.

"},{"location":"api/widget/#textual.widget.Widget.dock_gutter","title":"dock_gutter property","text":"
dock_gutter: Spacing\n

Space allocated to docks in the parent.

Returns Type Description Spacing

Space to be subtracted from scrollable area.

"},{"location":"api/widget/#textual.widget.Widget.expand","title":"expand class-attribute instance-attribute","text":"
expand: Reactive[bool] = Reactive(False)\n

Rich renderable may expand beyond optimal size.

"},{"location":"api/widget/#textual.widget.Widget.focusable","title":"focusable property","text":"
focusable: bool\n

Can this widget currently be focused?

"},{"location":"api/widget/#textual.widget.Widget.gutter","title":"gutter property","text":"
gutter: Spacing\n

Spacing for padding / border / scrollbars.

Returns Type Description Spacing

Additional spacing around content area.

"},{"location":"api/widget/#textual.widget.Widget.has_focus","title":"has_focus class-attribute instance-attribute","text":"
has_focus: Reactive[bool] = Reactive(False, repaint=False)\n

Does this widget have focus? Read only.

"},{"location":"api/widget/#textual.widget.Widget.highlight_link_id","title":"highlight_link_id class-attribute instance-attribute","text":"
highlight_link_id: Reactive[str] = Reactive('')\n

The currently highlighted link id. Read only.

"},{"location":"api/widget/#textual.widget.Widget.horizontal_scrollbar","title":"horizontal_scrollbar property","text":"
horizontal_scrollbar: ScrollBar\n

The a horizontal scrollbar.

Note

This will create a scrollbar if one doesn't exist.

Returns Type Description ScrollBar

ScrollBar Widget.

"},{"location":"api/widget/#textual.widget.Widget.hover_style","title":"hover_style class-attribute instance-attribute","text":"
hover_style: Reactive[Style] = Reactive(\nStyle, repaint=False\n)\n

The current hover style (style under the mouse cursor). Read only.

"},{"location":"api/widget/#textual.widget.Widget.is_container","title":"is_container property","text":"
is_container: bool\n

Is this widget a container (contains other widgets)?

"},{"location":"api/widget/#textual.widget.Widget.is_horizontal_scroll_end","title":"is_horizontal_scroll_end property","text":"
is_horizontal_scroll_end: bool\n

Is the horizontal scroll position at the maximum?

"},{"location":"api/widget/#textual.widget.Widget.is_horizontal_scrollbar_grabbed","title":"is_horizontal_scrollbar_grabbed property","text":"
is_horizontal_scrollbar_grabbed: bool\n

Is the user dragging the vertical scrollbar?

"},{"location":"api/widget/#textual.widget.Widget.is_scrollable","title":"is_scrollable property","text":"
is_scrollable: bool\n

Can this widget be scrolled?

"},{"location":"api/widget/#textual.widget.Widget.is_vertical_scroll_end","title":"is_vertical_scroll_end property","text":"
is_vertical_scroll_end: bool\n

Is the vertical scroll position at the maximum?

"},{"location":"api/widget/#textual.widget.Widget.is_vertical_scrollbar_grabbed","title":"is_vertical_scrollbar_grabbed property","text":"
is_vertical_scrollbar_grabbed: bool\n

Is the user dragging the vertical scrollbar?

"},{"location":"api/widget/#textual.widget.Widget.layer","title":"layer property","text":"
layer: str\n

Get the name of this widgets layer.

Returns Type Description str

Name of layer.

"},{"location":"api/widget/#textual.widget.Widget.layers","title":"layers property","text":"
layers: tuple[str, ...]\n

Layers of from parent.

Returns Type Description tuple[str, ...]

Tuple of layer names.

"},{"location":"api/widget/#textual.widget.Widget.link_hover_style","title":"link_hover_style property","text":"
link_hover_style: Style\n

Style of links underneath the mouse cursor.

Returns Type Description Style

Rich Style.

"},{"location":"api/widget/#textual.widget.Widget.link_style","title":"link_style property","text":"
link_style: Style\n

Style of links.

Returns Type Description Style

Rich style.

"},{"location":"api/widget/#textual.widget.Widget.max_scroll_x","title":"max_scroll_x property","text":"
max_scroll_x: int\n

The maximum value of scroll_x.

"},{"location":"api/widget/#textual.widget.Widget.max_scroll_y","title":"max_scroll_y property","text":"
max_scroll_y: int\n

The maximum value of scroll_y.

"},{"location":"api/widget/#textual.widget.Widget.mouse_over","title":"mouse_over class-attribute instance-attribute","text":"
mouse_over: Reactive[bool] = Reactive(False, repaint=False)\n

Is the mouse over this widget? Read only.

"},{"location":"api/widget/#textual.widget.Widget.offset","title":"offset property writable","text":"
offset: Offset\n

Widget offset from origin.

Returns Type Description Offset

Relative offset.

"},{"location":"api/widget/#textual.widget.Widget.opacity","title":"opacity property","text":"
opacity: float\n

Total opacity of widget.

"},{"location":"api/widget/#textual.widget.Widget.outer_size","title":"outer_size property","text":"
outer_size: Size\n

The size of the widget (including padding and border).

Returns Type Description Size

Outer size.

"},{"location":"api/widget/#textual.widget.Widget.region","title":"region property","text":"
region: Region\n

The region occupied by this widget, relative to the Screen.

Raises Type Description NoScreen

If there is no screen.

errors.NoWidget

If the widget is not on the screen.

Returns Type Description Region

Region within screen occupied by widget.

"},{"location":"api/widget/#textual.widget.Widget.scroll_offset","title":"scroll_offset property","text":"
scroll_offset: Offset\n

Get the current scroll offset.

Returns Type Description Offset

Offset a container has been scrolled by.

"},{"location":"api/widget/#textual.widget.Widget.scroll_x","title":"scroll_x class-attribute instance-attribute","text":"
scroll_x: Reactive[float] = Reactive(\n0.0, repaint=False, layout=False\n)\n

The scroll position on the X axis.

"},{"location":"api/widget/#textual.widget.Widget.scroll_y","title":"scroll_y class-attribute instance-attribute","text":"
scroll_y: Reactive[float] = Reactive(\n0.0, repaint=False, layout=False\n)\n

The scroll position on the Y axis.

"},{"location":"api/widget/#textual.widget.Widget.scrollable_content_region","title":"scrollable_content_region property","text":"
scrollable_content_region: Region\n

Gets an absolute region containing the scrollable content (minus padding, border, and scrollbars).

Returns Type Description Region

Screen region that contains a widget's content.

"},{"location":"api/widget/#textual.widget.Widget.scrollbar_corner","title":"scrollbar_corner property","text":"
scrollbar_corner: ScrollBarCorner\n

The scrollbar corner.

Note

This will create a scrollbar corner if one doesn't exist.

Returns Type Description ScrollBarCorner

ScrollBarCorner Widget.

"},{"location":"api/widget/#textual.widget.Widget.scrollbar_gutter","title":"scrollbar_gutter property","text":"
scrollbar_gutter: Spacing\n

Spacing required to fit scrollbar(s).

Returns Type Description Spacing

Scrollbar gutter spacing.

"},{"location":"api/widget/#textual.widget.Widget.scrollbar_size_horizontal","title":"scrollbar_size_horizontal property","text":"
scrollbar_size_horizontal: int\n

Get the height used by the horizontal scrollbar.

Returns Type Description int

Number of rows in the horizontal scrollbar.

"},{"location":"api/widget/#textual.widget.Widget.scrollbar_size_vertical","title":"scrollbar_size_vertical property","text":"
scrollbar_size_vertical: int\n

Get the width used by the vertical scrollbar.

Returns Type Description int

Number of columns in the vertical scrollbar.

"},{"location":"api/widget/#textual.widget.Widget.scrollbars_enabled","title":"scrollbars_enabled property","text":"
scrollbars_enabled: tuple[bool, bool]\n

A tuple of booleans that indicate if scrollbars are enabled.

Returns Type Description tuple[bool, bool]

A tuple of (, )"},{"location":"api/widget/#textual.widget.Widget.scrollbars_space","title":"scrollbars_space property","text":"

scrollbars_space: tuple[int, int]\n

The number of cells occupied by scrollbars for width and height

"},{"location":"api/widget/#textual.widget.Widget.show_horizontal_scrollbar","title":"show_horizontal_scrollbar class-attribute instance-attribute","text":"
show_horizontal_scrollbar: Reactive[bool] = Reactive(\nFalse, layout=True\n)\n

Show a horizontal scrollbar?

"},{"location":"api/widget/#textual.widget.Widget.show_vertical_scrollbar","title":"show_vertical_scrollbar class-attribute instance-attribute","text":"
show_vertical_scrollbar: Reactive[bool] = Reactive(\nFalse, layout=True\n)\n

Show a horizontal scrollbar?

"},{"location":"api/widget/#textual.widget.Widget.shrink","title":"shrink class-attribute instance-attribute","text":"
shrink: Reactive[bool] = Reactive(True)\n

Rich renderable may shrink below optimal size.

"},{"location":"api/widget/#textual.widget.Widget.siblings","title":"siblings property","text":"
siblings: list[Widget]\n

Get the widget's siblings (self is removed from the return list).

Returns Type Description list[Widget]

A list of siblings.

"},{"location":"api/widget/#textual.widget.Widget.size","title":"size property","text":"
size: Size\n

The size of the content area.

Returns Type Description Size

Content area size.

"},{"location":"api/widget/#textual.widget.Widget.tooltip","title":"tooltip property writable","text":"
tooltip: RenderableType | None\n

Tooltip for the widget, or None for no tooltip.

"},{"location":"api/widget/#textual.widget.Widget.vertical_scrollbar","title":"vertical_scrollbar property","text":"
vertical_scrollbar: ScrollBar\n

The vertical scrollbar (create if necessary).

Note

This will create a scrollbar if one doesn't exist.

Returns Type Description ScrollBar

ScrollBar Widget.

"},{"location":"api/widget/#textual.widget.Widget.virtual_region","title":"virtual_region property","text":"
virtual_region: Region\n

The widget region relative to it's container (which may not be visible, depending on scroll offset).

Returns Type Description Region

The virtual region.

"},{"location":"api/widget/#textual.widget.Widget.virtual_region_with_margin","title":"virtual_region_with_margin property","text":"
virtual_region_with_margin: Region\n

The widget region relative to its container (including margin), which may not be visible, depending on the scroll offset.

Returns Type Description Region

The virtual region of the Widget, inclusive of its margin.

"},{"location":"api/widget/#textual.widget.Widget.virtual_size","title":"virtual_size class-attribute instance-attribute","text":"
virtual_size: Reactive[Size] = Reactive(\nSize(0, 0), layout=True\n)\n

The virtual (scrollable) size of the widget.

"},{"location":"api/widget/#textual.widget.Widget.visible_siblings","title":"visible_siblings property","text":"
visible_siblings: list[Widget]\n

A list of siblings which will be shown.

Returns Type Description list[Widget]

List of siblings.

"},{"location":"api/widget/#textual.widget.Widget.window_region","title":"window_region property","text":"
window_region: Region\n

The region within the scrollable area that is currently visible.

Returns Type Description Region

New region.

"},{"location":"api/widget/#textual.widget.Widget.animate","title":"animate method","text":"
def animate(\nself,\nattribute,\nvalue,\n*,\nfinal_value=Ellipsis,\nduration=None,\nspeed=None,\ndelay=0.0,\neasing=DEFAULT_EASING,\non_complete=None\n):\n

Animate an attribute.

Parameters Name Type Description Default attribute str

Name of the attribute to animate.

required value float | Animatable

The value to animate to.

required final_value object

The final value of the animation. Defaults to value if not set.

Ellipsis duration float | None

The duration of the animate.

None speed float | None

The speed of the animation.

None delay float

A delay (in seconds) before the animation starts.

0.0 easing EasingFunction | str

An easing method.

DEFAULT_EASING on_complete CallbackType | None

A callable to invoke when the animation is finished.

None"},{"location":"api/widget/#textual.widget.Widget.begin_capture_print","title":"begin_capture_print method","text":"
def begin_capture_print(self, stdout=True, stderr=True):\n

Capture text from print statements (or writes to stdout / stderr).

If printing is captured, the widget will be sent an events.Print message.

Call end_capture_print to disable print capture.

Parameters Name Type Description Default stdout bool

Capture stdout.

True stderr bool

Capture stderr.

True"},{"location":"api/widget/#textual.widget.Widget.blur","title":"blur method","text":"
def blur(self):\n

Blur (un-focus) the widget.

Focus will be moved to the next available widget in the focus chain..

Returns Type Description Self

The Widget instance.

"},{"location":"api/widget/#textual.widget.Widget.can_view","title":"can_view method","text":"
def can_view(self, widget):\n

Check if a given widget is in the current view (scrollable area).

Note: This doesn't necessarily equate to a widget being visible. There are other reasons why a widget may not be visible.

Parameters Name Type Description Default widget Widget

A widget that is a descendant of self.

required Returns Type Description bool

True if the entire widget is in view, False if it is partially visible or not in view.

"},{"location":"api/widget/#textual.widget.Widget.capture_mouse","title":"capture_mouse method","text":"
def capture_mouse(self, capture=True):\n

Capture (or release) the mouse.

When captured, mouse events will go to this widget even when the pointer is not directly over the widget.

Parameters Name Type Description Default capture bool

True to capture or False to release.

True"},{"location":"api/widget/#textual.widget.Widget.check_message_enabled","title":"check_message_enabled method","text":"
def check_message_enabled(self, message):\n

Check if a given message is enabled (allowed to be sent).

Parameters Name Type Description Default message Message

A message object

required Returns Type Description bool

True if the message will be sent, or False if it is disabled.

"},{"location":"api/widget/#textual.widget.Widget.compose","title":"compose method","text":"
def compose(self):\n

Called by Textual to create child widgets.

Extend this to build a UI.

Example
def compose(self) -> ComposeResult:\nyield Header()\nyield Container(\nTree(), Viewer()\n)\nyield Footer()\n
"},{"location":"api/widget/#textual.widget.Widget.end_capture_print","title":"end_capture_print method","text":"
def end_capture_print(self):\n

End print capture (set with capture_print).

"},{"location":"api/widget/#textual.widget.Widget.focus","title":"focus method","text":"
def focus(self, scroll_visible=True):\n

Give focus to this widget.

Parameters Name Type Description Default scroll_visible bool

Scroll parent to make this widget visible.

True Returns Type Description Self

The Widget instance.

"},{"location":"api/widget/#textual.widget.Widget.get_child_by_id","title":"get_child_by_id method","text":"
def get_child_by_id(self, id, expect_type=None):\n

Return the first child (immediate descendent) of this node with the given ID.

Parameters Name Type Description Default id str

The ID of the child.

required expect_type type[ExpectType] | None

Require the object be of the supplied type, or None for any type.

None Returns Type Description ExpectType | Widget

The first child of this node with the ID.

Raises Type Description NoMatches

if no children could be found for this ID

WrongType

if the wrong type was found.

"},{"location":"api/widget/#textual.widget.Widget.get_child_by_type","title":"get_child_by_type method","text":"
def get_child_by_type(self, expect_type):\n

Get a child of a give type.

Parameters Name Type Description Default expect_type type[ExpectType]

The type of the expected child.

required Raises Type Description NoMatches

If no valid child is found.

Returns Type Description ExpectType

A widget.

"},{"location":"api/widget/#textual.widget.Widget.get_component_rich_style","title":"get_component_rich_style method","text":"
def get_component_rich_style(self, name, *, partial=False):\n

Get a Rich style for a component.

Parameters Name Type Description Default name str

Name of component.

required partial bool

Return a partial style (not combined with parent).

False Returns Type Description Style

A Rich style object.

"},{"location":"api/widget/#textual.widget.Widget.get_content_height","title":"get_content_height method","text":"
def get_content_height(self, container, viewport, width):\n

Called by Textual to get the height of the content area. May be overridden in a subclass.

Parameters Name Type Description Default container Size

Size of the container (immediate parent) widget.

required viewport Size

Size of the viewport.

required width int

Width of renderable.

required Returns Type Description int

The height of the content.

"},{"location":"api/widget/#textual.widget.Widget.get_content_width","title":"get_content_width method","text":"
def get_content_width(self, container, viewport):\n

Called by textual to get the width of the content area. May be overridden in a subclass.

Parameters Name Type Description Default container Size

Size of the container (immediate parent) widget.

required viewport Size

Size of the viewport.

required Returns Type Description int

The optimal width of the content.

"},{"location":"api/widget/#textual.widget.Widget.get_pseudo_class_state","title":"get_pseudo_class_state method","text":"
def get_pseudo_class_state(self):\n

Get an object describing whether each pseudo class is present on this object or not.

Returns Type Description PseudoClasses

A PseudoClasses object describing the pseudo classes that are present.

"},{"location":"api/widget/#textual.widget.Widget.get_pseudo_classes","title":"get_pseudo_classes method","text":"
def get_pseudo_classes(self):\n

Pseudo classes for a widget.

Returns Type Description Iterable[str]

Names of the pseudo classes.

"},{"location":"api/widget/#textual.widget.Widget.get_style_at","title":"get_style_at method","text":"
def get_style_at(self, x, y):\n

Get the Rich style in a widget at a given relative offset.

Parameters Name Type Description Default x int

X coordinate relative to the widget.

required y int

Y coordinate relative to the widget.

required Returns Type Description Style

A rich Style object.

"},{"location":"api/widget/#textual.widget.Widget.get_widget_by_id","title":"get_widget_by_id method","text":"
def get_widget_by_id(self, id, expect_type=None):\n

Return the first descendant widget with the given ID.

Performs a depth-first search rooted at this widget.

Parameters Name Type Description Default id str

The ID to search for in the subtree.

required expect_type type[ExpectType] | None

Require the object be of the supplied type, or None for any type.

None Returns Type Description ExpectType | Widget

The first descendant encountered with this ID.

Raises Type Description NoMatches

if no children could be found for this ID.

WrongType

if the wrong type was found.

"},{"location":"api/widget/#textual.widget.Widget.mount","title":"mount method","text":"
def mount(self, *widgets, before=None, after=None):\n

Mount widgets below this widget (making this widget a container).

Parameters Name Type Description Default *widgets Widget

The widget(s) to mount.

() before int | str | Widget | None

Optional location to mount before. An int is the index of the child to mount before, a str is a query_one query to find the widget to mount before.

None after int | str | Widget | None

Optional location to mount after. An int is the index of the child to mount after, a str is a query_one query to find the widget to mount after.

None Returns Type Description AwaitMount

An awaitable object that waits for widgets to be mounted.

Raises Type Description MountError

If there is a problem with the mount request.

Note

Only one of before or after can be provided. If both are provided a MountError will be raised.

"},{"location":"api/widget/#textual.widget.Widget.mount_all","title":"mount_all method","text":"
def mount_all(self, widgets, *, before=None, after=None):\n

Mount widgets from an iterable.

Parameters Name Type Description Default widgets Iterable[Widget]

An iterable of widgets.

required before int | str | Widget | None

Optional location to mount before. An int is the index of the child to mount before, a str is a query_one query to find the widget to mount before.

None after int | str | Widget | None

Optional location to mount after. An int is the index of the child to mount after, a str is a query_one query to find the widget to mount after.

None Returns Type Description AwaitMount

An awaitable object that waits for widgets to be mounted.

Raises Type Description MountError

If there is a problem with the mount request.

Note

Only one of before or after can be provided. If both are provided a MountError will be raised.

"},{"location":"api/widget/#textual.widget.Widget.move_child","title":"move_child method","text":"
def move_child(self, child, before=None, after=None):\n

Move a child widget within its parent's list of children.

Parameters Name Type Description Default child int | Widget

The child widget to move.

required before int | Widget | None

Child widget or location index to move before.

None after int | Widget | None

Child widget or location index to move after.

None Raises Type Description WidgetError

If there is a problem with the child or target.

Note

Only one of before or after can be provided. If neither or both are provided a WidgetError will be raised.

"},{"location":"api/widget/#textual.widget.Widget.notify","title":"notify method","text":"
def notify(\nself,\nmessage,\n*,\ntitle=\"\",\nseverity=\"information\",\ntimeout=Notification.timeout\n):\n

Create a notification.

Tip

This method is thread-safe.

Parameters Name Type Description Default message str

The message for the notification.

required title str

The title for the notification.

'' severity SeverityLevel

The severity of the notification.

'information' timeout float

The timeout for the notification.

Notification.timeout

See App.notify for the full documentation for this method.

"},{"location":"api/widget/#textual.widget.Widget.post_message","title":"post_message method","text":"
def post_message(self, message):\n

Post a message to this widget.

Parameters Name Type Description Default message Message

Message to post.

required Returns Type Description bool

True if the message was posted, False if this widget was closed / closing.

"},{"location":"api/widget/#textual.widget.Widget.post_render","title":"post_render method","text":"
def post_render(self, renderable):\n

Applies style attributes to the default renderable.

Returns Type Description ConsoleRenderable

A new renderable.

"},{"location":"api/widget/#textual.widget.Widget.refresh","title":"refresh method","text":"
def refresh(self, *regions, repaint=True, layout=False):\n

Initiate a refresh of the widget.

This method sets an internal flag to perform a refresh, which will be done on the next idle event. Only one refresh will be done even if this method is called multiple times.

By default this method will cause the content of the widget to refresh, but not change its size. You can also set layout=True to perform a layout.

Warning

It is rarely necessary to call this method explicitly. Updating styles or reactive attributes will do this automatically.

Parameters Name Type Description Default *regions Region

Additional screen regions to mark as dirty.

() repaint bool

Repaint the widget (will call render() again).

True layout bool

Also layout widgets in the view.

False Returns Type Description Self

The Widget instance.

"},{"location":"api/widget/#textual.widget.Widget.release_mouse","title":"release_mouse method","text":"
def release_mouse(self):\n

Release the mouse.

Mouse events will only be sent when the mouse is over the widget.

"},{"location":"api/widget/#textual.widget.Widget.remove","title":"remove method","text":"
def remove(self):\n

Remove the Widget from the DOM (effectively deleting it).

Returns Type Description AwaitRemove

An awaitable object that waits for the widget to be removed.

"},{"location":"api/widget/#textual.widget.Widget.remove_children","title":"remove_children method","text":"
def remove_children(self):\n

Remove all children of this Widget from the DOM.

Returns Type Description AwaitRemove

An awaitable object that waits for the children to be removed.

"},{"location":"api/widget/#textual.widget.Widget.render","title":"render method","text":"
def render(self):\n

Get renderable for widget.

Returns Type Description RenderableType

Any renderable.

"},{"location":"api/widget/#textual.widget.Widget.render_line","title":"render_line method","text":"
def render_line(self, y):\n

Render a line of content.

Parameters Name Type Description Default y int

Y Coordinate of line.

required Returns Type Description Strip

A rendered line.

"},{"location":"api/widget/#textual.widget.Widget.render_lines","title":"render_lines method","text":"
def render_lines(self, crop):\n

Render the widget in to lines.

Parameters Name Type Description Default crop Region

Region within visible area to render.

required Returns Type Description list[Strip]

A list of list of segments.

"},{"location":"api/widget/#textual.widget.Widget.render_str","title":"render_str method","text":"
def render_str(self, text_content):\n

Convert str in to a Text object.

If you pass in an existing Text object it will be returned unaltered.

Parameters Name Type Description Default text_content str | Text

Text or str.

required Returns Type Description Text

A text object.

"},{"location":"api/widget/#textual.widget.Widget.run_action","title":"run_action async","text":"
def run_action(self, action):\n

Perform a given action, with this widget as the default namespace.

Parameters Name Type Description Default action str

Action encoded as a string.

required"},{"location":"api/widget/#textual.widget.Widget.scroll_down","title":"scroll_down method","text":"
def scroll_down(\nself,\n*,\nanimate=True,\nspeed=None,\nduration=None,\neasing=None,\nforce=False,\non_complete=None\n):\n

Scroll one line down.

Parameters Name Type Description Default animate bool

Animate scroll.

True speed float | None

Speed of scroll if animate is True; or None to use duration.

None duration float | None

Duration of animation, if animate is True and speed is None.

None easing EasingFunction | str | None

An easing method for the scrolling animation.

None force bool

Force scrolling even when prohibited by overflow styling.

False on_complete CallbackType | None

A callable to invoke when the animation is finished.

None"},{"location":"api/widget/#textual.widget.Widget.scroll_end","title":"scroll_end method","text":"
def scroll_end(\nself,\n*,\nanimate=True,\nspeed=None,\nduration=None,\neasing=None,\nforce=False,\non_complete=None\n):\n

Scroll to the end of the container.

Parameters Name Type Description Default animate bool

Animate scroll.

True speed float | None

Speed of scroll if animate is True; or None to use duration.

None duration float | None

Duration of animation, if animate is True and speed is None.

None easing EasingFunction | str | None

An easing method for the scrolling animation.

None force bool

Force scrolling even when prohibited by overflow styling.

False on_complete CallbackType | None

A callable to invoke when the animation is finished.

None"},{"location":"api/widget/#textual.widget.Widget.scroll_home","title":"scroll_home method","text":"
def scroll_home(\nself,\n*,\nanimate=True,\nspeed=None,\nduration=None,\neasing=None,\nforce=False,\non_complete=None\n):\n

Scroll to home position.

Parameters Name Type Description Default animate bool

Animate scroll.

True speed float | None

Speed of scroll if animate is True; or None to use duration.

None duration float | None

Duration of animation, if animate is True and speed is None.

None easing EasingFunction | str | None

An easing method for the scrolling animation.

None force bool

Force scrolling even when prohibited by overflow styling.

False on_complete CallbackType | None

A callable to invoke when the animation is finished.

None"},{"location":"api/widget/#textual.widget.Widget.scroll_left","title":"scroll_left method","text":"
def scroll_left(\nself,\n*,\nanimate=True,\nspeed=None,\nduration=None,\neasing=None,\nforce=False,\non_complete=None\n):\n

Scroll one cell left.

Parameters Name Type Description Default animate bool

Animate scroll.

True speed float | None

Speed of scroll if animate is True; or None to use duration.

None duration float | None

Duration of animation, if animate is True and speed is None.

None easing EasingFunction | str | None

An easing method for the scrolling animation.

None force bool

Force scrolling even when prohibited by overflow styling.

False on_complete CallbackType | None

A callable to invoke when the animation is finished.

None"},{"location":"api/widget/#textual.widget.Widget.scroll_page_down","title":"scroll_page_down method","text":"
def scroll_page_down(\nself,\n*,\nanimate=True,\nspeed=None,\nduration=None,\neasing=None,\nforce=False,\non_complete=None\n):\n

Scroll one page down.

Parameters Name Type Description Default animate bool

Animate scroll.

True speed float | None

Speed of scroll if animate is True; or None to use duration.

None duration float | None

Duration of animation, if animate is True and speed is None.

None easing EasingFunction | str | None

An easing method for the scrolling animation.

None force bool

Force scrolling even when prohibited by overflow styling.

False on_complete CallbackType | None

A callable to invoke when the animation is finished.

None"},{"location":"api/widget/#textual.widget.Widget.scroll_page_left","title":"scroll_page_left method","text":"
def scroll_page_left(\nself,\n*,\nanimate=True,\nspeed=None,\nduration=None,\neasing=None,\nforce=False,\non_complete=None\n):\n

Scroll one page left.

Parameters Name Type Description Default animate bool

Animate scroll.

True speed float | None

Speed of scroll if animate is True; or None to use duration.

None duration float | None

Duration of animation, if animate is True and speed is None.

None easing EasingFunction | str | None

An easing method for the scrolling animation.

None force bool

Force scrolling even when prohibited by overflow styling.

False on_complete CallbackType | None

A callable to invoke when the animation is finished.

None"},{"location":"api/widget/#textual.widget.Widget.scroll_page_right","title":"scroll_page_right method","text":"
def scroll_page_right(\nself,\n*,\nanimate=True,\nspeed=None,\nduration=None,\neasing=None,\nforce=False,\non_complete=None\n):\n

Scroll one page right.

Parameters Name Type Description Default animate bool

Animate scroll.

True speed float | None

Speed of scroll if animate is True; or None to use duration.

None duration float | None

Duration of animation, if animate is True and speed is None.

None easing EasingFunction | str | None

An easing method for the scrolling animation.

None force bool

Force scrolling even when prohibited by overflow styling.

False on_complete CallbackType | None

A callable to invoke when the animation is finished.

None"},{"location":"api/widget/#textual.widget.Widget.scroll_page_up","title":"scroll_page_up method","text":"
def scroll_page_up(\nself,\n*,\nanimate=True,\nspeed=None,\nduration=None,\neasing=None,\nforce=False,\non_complete=None\n):\n

Scroll one page up.

Parameters Name Type Description Default animate bool

Animate scroll.

True speed float | None

Speed of scroll if animate is True; or None to use duration.

None duration float | None

Duration of animation, if animate is True and speed is None.

None easing EasingFunction | str | None

An easing method for the scrolling animation.

None force bool

Force scrolling even when prohibited by overflow styling.

False on_complete CallbackType | None

A callable to invoke when the animation is finished.

None"},{"location":"api/widget/#textual.widget.Widget.scroll_relative","title":"scroll_relative method","text":"
def scroll_relative(\nself,\nx=None,\ny=None,\n*,\nanimate=True,\nspeed=None,\nduration=None,\neasing=None,\nforce=False,\non_complete=None\n):\n

Scroll relative to current position.

Parameters Name Type Description Default x float | None

X distance (columns) to scroll, or None for no change.

None y float | None

Y distance (rows) to scroll, or None for no change.

None animate bool

Animate to new scroll position.

True speed float | None

Speed of scroll if animate is True. Or None to use duration.

None duration float | None

Duration of animation, if animate is True and speed is None.

None easing EasingFunction | str | None

An easing method for the scrolling animation.

None force bool

Force scrolling even when prohibited by overflow styling.

False on_complete CallbackType | None

A callable to invoke when the animation is finished.

None"},{"location":"api/widget/#textual.widget.Widget.scroll_right","title":"scroll_right method","text":"
def scroll_right(\nself,\n*,\nanimate=True,\nspeed=None,\nduration=None,\neasing=None,\nforce=False,\non_complete=None\n):\n

Scroll one cell right.

Parameters Name Type Description Default animate bool

Animate scroll.

True speed float | None

Speed of scroll if animate is True; or None to use duration.

None duration float | None

Duration of animation, if animate is True and speed is None.

None easing EasingFunction | str | None

An easing method for the scrolling animation.

None force bool

Force scrolling even when prohibited by overflow styling.

False on_complete CallbackType | None

A callable to invoke when the animation is finished.

None"},{"location":"api/widget/#textual.widget.Widget.scroll_to","title":"scroll_to method","text":"
def scroll_to(\nself,\nx=None,\ny=None,\n*,\nanimate=True,\nspeed=None,\nduration=None,\neasing=None,\nforce=False,\non_complete=None\n):\n

Scroll to a given (absolute) coordinate, optionally animating.

Parameters Name Type Description Default x float | None

X coordinate (column) to scroll to, or None for no change.

None y float | None

Y coordinate (row) to scroll to, or None for no change.

None animate bool

Animate to new scroll position.

True speed float | None

Speed of scroll if animate is True; or None to use duration.

None duration float | None

Duration of animation, if animate is True and speed is None.

None easing EasingFunction | str | None

An easing method for the scrolling animation.

None force bool

Force scrolling even when prohibited by overflow styling.

False on_complete CallbackType | None

A callable to invoke when the animation is finished.

None Note

The call to scroll is made after the next refresh.

"},{"location":"api/widget/#textual.widget.Widget.scroll_to_center","title":"scroll_to_center method","text":"
def scroll_to_center(\nself,\nwidget,\nanimate=True,\n*,\nspeed=None,\nduration=None,\neasing=None,\nforce=False,\norigin_visible=True,\non_complete=None\n):\n

Scroll this widget to the center of self.

The center of the widget will be scrolled to the center of the container.

Parameters Name Type Description Default widget Widget

The widget to scroll to the center of self.

required animate bool

Whether to animate the scroll.

True speed float | None

Speed of scroll if animate is True; or None to use duration.

None duration float | None

Duration of animation, if animate is True and speed is None.

None easing EasingFunction | str | None

An easing method for the scrolling animation.

None force bool

Force scrolling even when prohibited by overflow styling.

False origin_visible bool

Ensure that the top left corner of the widget remains visible after the scroll.

True on_complete CallbackType | None

A callable to invoke when the animation is finished.

None"},{"location":"api/widget/#textual.widget.Widget.scroll_to_region","title":"scroll_to_region method","text":"
def scroll_to_region(\nself,\nregion,\n*,\nspacing=None,\nanimate=True,\nspeed=None,\nduration=None,\neasing=None,\ncenter=False,\ntop=False,\norigin_visible=True,\nforce=False,\non_complete=None\n):\n

Scrolls a given region in to view, if required.

This method will scroll the least distance required to move region fully within the scrollable area.

Parameters Name Type Description Default region Region

A region that should be visible.

required spacing Spacing | None

Optional spacing around the region.

None animate bool

True to animate, or False to jump.

True speed float | None

Speed of scroll if animate is True; or None to use duration.

None duration float | None

Duration of animation, if animate is True and speed is None.

None easing EasingFunction | str | None

An easing method for the scrolling animation.

None top bool

Scroll region to top of container.

False origin_visible bool

Ensure that the top left of the widget is within the window.

True force bool

Force scrolling even when prohibited by overflow styling.

False on_complete CallbackType | None

A callable to invoke when the animation is finished.

None Returns Type Description Offset

The distance that was scrolled.

"},{"location":"api/widget/#textual.widget.Widget.scroll_to_widget","title":"scroll_to_widget method","text":"
def scroll_to_widget(\nself,\nwidget,\n*,\nanimate=True,\nspeed=None,\nduration=None,\neasing=None,\ncenter=False,\ntop=False,\norigin_visible=True,\nforce=False,\non_complete=None\n):\n

Scroll scrolling to bring a widget in to view.

Parameters Name Type Description Default widget Widget

A descendant widget.

required animate bool

True to animate, or False to jump.

True speed float | None

Speed of scroll if animate is True; or None to use duration.

None duration float | None

Duration of animation, if animate is True and speed is None.

None easing EasingFunction | str | None

An easing method for the scrolling animation.

None top bool

Scroll widget to top of container.

False origin_visible bool

Ensure that the top left of the widget is within the window.

True force bool

Force scrolling even when prohibited by overflow styling.

False on_complete CallbackType | None

A callable to invoke when the animation is finished.

None Returns Type Description bool

True if any scrolling has occurred in any descendant, otherwise False.

"},{"location":"api/widget/#textual.widget.Widget.scroll_up","title":"scroll_up method","text":"
def scroll_up(\nself,\n*,\nanimate=True,\nspeed=None,\nduration=None,\neasing=None,\nforce=False,\non_complete=None\n):\n

Scroll one line up.

Parameters Name Type Description Default animate bool

Animate scroll.

True speed float | None

Speed of scroll if animate is True; or None to use duration.

None duration float | None

Duration of animation, if animate is True and speed is None.

None easing EasingFunction | str | None

An easing method for the scrolling animation.

None force bool

Force scrolling even when prohibited by overflow styling.

False on_complete CallbackType | None

A callable to invoke when the animation is finished.

None"},{"location":"api/widget/#textual.widget.Widget.scroll_visible","title":"scroll_visible method","text":"
def scroll_visible(\nself,\nanimate=True,\n*,\nspeed=None,\nduration=None,\ntop=False,\neasing=None,\nforce=False,\non_complete=None\n):\n

Scroll the container to make this widget visible.

Parameters Name Type Description Default animate bool

Animate scroll.

True speed float | None

Speed of scroll if animate is True; or None to use duration.

None duration float | None

Duration of animation, if animate is True and speed is None.

None top bool

Scroll to top of container.

False easing EasingFunction | str | None

An easing method for the scrolling animation.

None force bool

Force scrolling even when prohibited by overflow styling.

False on_complete CallbackType | None

A callable to invoke when the animation is finished.

None"},{"location":"api/widget/#textual.widget.Widget.stop_animation","title":"stop_animation async","text":"
def stop_animation(self, attribute, complete=True):\n

Stop an animation on an attribute.

Parameters Name Type Description Default attribute str

Name of the attribute whose animation should be stopped.

required complete bool

Should the animation be set to its final value?

True Note

If there is no animation scheduled or running, this is a no-op.

"},{"location":"api/widget/#textual.widget.Widget.watch_disabled","title":"watch_disabled method","text":"
def watch_disabled(self):\n

Update the styles of the widget and its children when disabled is toggled.

"},{"location":"api/widget/#textual.widget.Widget.watch_has_focus","title":"watch_has_focus method","text":"
def watch_has_focus(self, value):\n

Update from CSS if has focus state changes.

"},{"location":"api/widget/#textual.widget.Widget.watch_mouse_over","title":"watch_mouse_over method","text":"
def watch_mouse_over(self, value):\n

Update from CSS if mouse over state changes.

"},{"location":"api/widget/#textual.widget.WidgetError","title":"WidgetError class","text":"

Bases: Exception

Base widget error.

"},{"location":"api/work/","title":"Work","text":"

A decorator used to create workers.

Parameters Name Type Description Default method Callable[FactoryParamSpec, ReturnType] | Callable[FactoryParamSpec, Coroutine[None, None, ReturnType]] | None

A function or coroutine.

None name str

A short string to identify the worker (in logs and debugging).

'' group str

A short string to identify a group of workers.

'default' exit_on_error bool

Exit the app if the worker raises an error. Set to False to suppress exceptions.

True exclusive bool

Cancel all workers in the same group.

False description str | None

Readable description of the worker for debugging purposes. By default, it uses a string representation of the decorated method and its arguments.

None thread bool

Mark the method as a thread worker.

False"},{"location":"api/worker/","title":"Worker","text":"

A class to manage concurrent work.

"},{"location":"api/worker/#textual.worker.WorkType","title":"WorkType module-attribute","text":"
WorkType: TypeAlias = Union[\nCallable[[], Coroutine[None, None, ResultType]],\nCallable[[], ResultType],\nAwaitable[ResultType],\n]\n

Type used for workers.

"},{"location":"api/worker/#textual.worker.active_worker","title":"active_worker module-attribute","text":"
active_worker: ContextVar[Worker] = ContextVar(\n\"active_worker\"\n)\n

Currently active worker context var.

"},{"location":"api/worker/#textual.worker.DeadlockError","title":"DeadlockError class","text":"

Bases: WorkerError

The operation would result in a deadlock.

"},{"location":"api/worker/#textual.worker.NoActiveWorker","title":"NoActiveWorker class","text":"

Bases: Exception

There is no active worker.

"},{"location":"api/worker/#textual.worker.Worker","title":"Worker class","text":"
def __init__(\nself,\nnode,\nwork=None,\n*,\nname=\"\",\ngroup=\"default\",\ndescription=\"\",\nexit_on_error=True,\nthread=False\n):\n

Bases: Generic[ResultType]

A class to manage concurrent work (either a task or a thread).

Parameters Name Type Description Default node DOMNode

The widget, screen, or App that initiated the work.

required work WorkType | None

A callable, coroutine, or other awaitable object to run in the worker.

None name str

Name of the worker (short string to help identify when debugging).

'' group str

The worker group.

'default' description str

Description of the worker (longer string with more details).

'' exit_on_error bool

Exit the app if the worker raises an error. Set to False to suppress exceptions.

True thread bool

Mark the worker as a thread worker.

False"},{"location":"api/worker/#textual.worker.Worker.completed_steps","title":"completed_steps property","text":"
completed_steps: int\n

The number of completed steps.

"},{"location":"api/worker/#textual.worker.Worker.error","title":"error property","text":"
error: BaseException | None\n

The exception raised by the worker, or None if there was no error.

"},{"location":"api/worker/#textual.worker.Worker.is_cancelled","title":"is_cancelled property","text":"
is_cancelled: bool\n

Has the work been cancelled?

Note that cancelled work may still be running.

"},{"location":"api/worker/#textual.worker.Worker.is_finished","title":"is_finished property","text":"
is_finished: bool\n

Has the task finished (cancelled, error, or success)?

"},{"location":"api/worker/#textual.worker.Worker.is_running","title":"is_running property","text":"
is_running: bool\n

Is the task running?

"},{"location":"api/worker/#textual.worker.Worker.node","title":"node property","text":"
node: DOMNode\n

The node where this worker was run from.

"},{"location":"api/worker/#textual.worker.Worker.progress","title":"progress property","text":"
progress: float\n

Progress as a percentage.

If the total steps is None, then this will return 0. The percentage will be clamped between 0 and 100.

"},{"location":"api/worker/#textual.worker.Worker.result","title":"result property","text":"
result: ResultType | None\n

The result of the worker, or None if there is no result.

"},{"location":"api/worker/#textual.worker.Worker.state","title":"state property writable","text":"
state: WorkerState\n

The current state of the worker.

"},{"location":"api/worker/#textual.worker.Worker.total_steps","title":"total_steps property","text":"
total_steps: int | None\n

The number of total steps, or None if indeterminate.

"},{"location":"api/worker/#textual.worker.Worker.StateChanged","title":"StateChanged class","text":"
def __init__(self, worker, state):\n

Bases: Message

The worker state changed.

Parameters Name Type Description Default worker Worker

The worker object.

required state WorkerState

New state.

required"},{"location":"api/worker/#textual.worker.Worker.advance","title":"advance method","text":"
def advance(self, steps=1):\n

Advance the number of completed steps.

Parameters Name Type Description Default steps int

Number of steps to advance.

1"},{"location":"api/worker/#textual.worker.Worker.cancel","title":"cancel method","text":"
def cancel(self):\n

Cancel the task.

"},{"location":"api/worker/#textual.worker.Worker.run","title":"run async","text":"
def run(self):\n

Run the work.

Implement this method in a subclass, or pass a callable to the constructor.

Returns Type Description ResultType

Return value of the work.

"},{"location":"api/worker/#textual.worker.Worker.update","title":"update method","text":"
def update(self, completed_steps=None, total_steps=-1):\n

Update the number of completed steps.

Parameters Name Type Description Default completed_steps int | None

The number of completed seps, or None to not change.

None total_steps int | None

The total number of steps, None for indeterminate, or -1 to leave unchanged.

-1"},{"location":"api/worker/#textual.worker.Worker.wait","title":"wait async","text":"
def wait(self):\n

Wait for the work to complete.

Raises Type Description WorkerFailed

If the Worker raised an exception.

WorkerCancelled

If the Worker was cancelled before it completed.

Returns Type Description ResultType

The return value of the work.

"},{"location":"api/worker/#textual.worker.WorkerCancelled","title":"WorkerCancelled class","text":"

Bases: WorkerError

The worker was cancelled and did not complete.

"},{"location":"api/worker/#textual.worker.WorkerError","title":"WorkerError class","text":"

Bases: Exception

A worker related error.

"},{"location":"api/worker/#textual.worker.WorkerFailed","title":"WorkerFailed class","text":"
def __init__(self, error):\n

Bases: WorkerError

The worker raised an exception and did not complete.

"},{"location":"api/worker/#textual.worker.WorkerState","title":"WorkerState class","text":"

Bases: enum.Enum

A description of the worker's current state.

"},{"location":"api/worker/#textual.worker.WorkerState.CANCELLED","title":"CANCELLED class-attribute instance-attribute","text":"
CANCELLED = 3\n

Worker is not running, and was cancelled.

"},{"location":"api/worker/#textual.worker.WorkerState.ERROR","title":"ERROR class-attribute instance-attribute","text":"
ERROR = 4\n

Worker is not running, and exited with an error.

"},{"location":"api/worker/#textual.worker.WorkerState.PENDING","title":"PENDING class-attribute instance-attribute","text":"
PENDING = 1\n

Worker is initialized, but not running.

"},{"location":"api/worker/#textual.worker.WorkerState.RUNNING","title":"RUNNING class-attribute instance-attribute","text":"
RUNNING = 2\n

Worker is running.

"},{"location":"api/worker/#textual.worker.WorkerState.SUCCESS","title":"SUCCESS class-attribute instance-attribute","text":"
SUCCESS = 5\n

Worker is not running, and completed successfully.

"},{"location":"api/worker/#textual.worker.get_current_worker","title":"get_current_worker function","text":"
def get_current_worker():\n

Get the currently active worker.

Raises Type Description NoActiveWorker

If there is no active worker.

Returns Type Description Worker

A Worker instance.

"},{"location":"api/worker_manager/","title":"Worker manager","text":"

A class to manage workers for an app.

You access this object via App.workers or Widget.workers.

"},{"location":"api/worker_manager/#textual._worker_manager.WorkerManager","title":"WorkerManager class","text":"
def __init__(self, app):\n

An object to manager a number of workers.

You will not have to construct this class manually, as widgets, screens, and apps have a worker manager accessibly via a workers attribute.

Parameters Name Type Description Default app App

An App instance.

required"},{"location":"api/worker_manager/#textual._worker_manager.WorkerManager.add_worker","title":"add_worker method","text":"
def add_worker(self, worker, start=True, exclusive=True):\n

Add a new worker.

Parameters Name Type Description Default worker Worker

A Worker instance.

required start bool

Start the worker if True, otherwise the worker must be started manually.

True exclusive bool

Cancel all workers in the same group as worker.

True"},{"location":"api/worker_manager/#textual._worker_manager.WorkerManager.cancel_all","title":"cancel_all method","text":"
def cancel_all(self):\n

Cancel all workers.

"},{"location":"api/worker_manager/#textual._worker_manager.WorkerManager.cancel_group","title":"cancel_group method","text":"
def cancel_group(self, node, group):\n

Cancel a single group.

Parameters Name Type Description Default node DOMNode

Worker DOM node.

required group str

A group name.

required Returns Type Description list[Worker]

A list of workers that were cancelled.

"},{"location":"api/worker_manager/#textual._worker_manager.WorkerManager.cancel_node","title":"cancel_node method","text":"
def cancel_node(self, node):\n

Cancel all workers associated with a given node

Parameters Name Type Description Default node DOMNode

A DOM node (widget, screen, or App).

required Returns Type Description list[Worker]

List of cancelled workers.

"},{"location":"api/worker_manager/#textual._worker_manager.WorkerManager.start_all","title":"start_all method","text":"
def start_all(self):\n

Start all the workers.

"},{"location":"api/worker_manager/#textual._worker_manager.WorkerManager.wait_for_complete","title":"wait_for_complete async","text":"
def wait_for_complete(self, workers=None):\n

Wait for workers to complete.

Parameters Name Type Description Default workers Iterable[Worker] | None

An iterable of workers or None to wait for all workers in the manager.

None"},{"location":"blog/","title":"Textual Blog","text":""},{"location":"blog/2023/03/15/no-async-async-with-python/","title":"No-async async with Python","text":"

A (reasonable) criticism of async is that it tends to proliferate in your code. In order to await something, your functions must be async all the way up the call-stack. This tends to result in you making things async just to support that one call that needs it or, worse, adding async just-in-case. Given that going from def to async def is a breaking change there is a strong incentive to go straight there.

Before you know it, you have adopted a policy of \"async all the things\".

Textual is an async framework, but doesn't require the app developer to use the async and await keywords (but you can if you need to). This post is about how Textual accomplishes this async-agnosticism.

Info

See this example from the docs for an async-less Textual app.

"},{"location":"blog/2023/03/15/no-async-async-with-python/#an-apology","title":"An apology","text":"

But first, an apology! In a previous post I said Textual \"doesn't do any IO of its own\". This is not accurate. Textual responds to keys and mouse events (Input) and writes content to the terminal (Output).

Although Textual clearly does do IO, it uses asyncio mainly for concurrency. It allows each widget to update its part of the screen independently from the rest of the app.

"},{"location":"blog/2023/03/15/no-async-async-with-python/#await-me-maybe","title":"Await me (maybe)","text":"

The first no-async async technique is the \"Await me maybe\" pattern, a term first coined by Simon Willison. This is particularly applicable to callbacks (or in Textual terms, message handlers).

The await_me_maybe function below can run a callback that is either a plain old function or a coroutine (async def). It does this by awaiting the result of the callback if it is awaitable, or simply returning the result if it is not.

import asyncio\nimport inspect\ndef plain_old_function():\nreturn \"Plain old function\"\nasync def async_function():\nreturn \"Async function\"\nasync def await_me_maybe(callback):\nresult = callback()\nif inspect.isawaitable(result):\nreturn await result\nreturn result\nasync def run_framework():\nprint(\nawait await_me_maybe(plain_old_function)\n)\nprint(\nawait await_me_maybe(async_function)\n)\nif __name__ == \"__main__\":\nasyncio.run(run_framework())\n
"},{"location":"blog/2023/03/15/no-async-async-with-python/#optionally-awaitable","title":"Optionally awaitable","text":"

The \"await me maybe\" pattern is great when an async framework calls the app's code. The app developer can choose to write async code or not. Things get a little more complicated when the app wants to call the framework's API. If the API has asynced all the things, then it would force the app to do the same.

Textual's API consists of regular methods for the most part, but there are a few methods which are optionally awaitable. These are not coroutines (which must be awaited to do anything).

In practice, this means that those API calls initiate something which will complete a short time later. If you discard the return value then it won't prevent it from working. You only need to await if you want to know when it has finished.

The mount method is one such method. Calling it will add a widget to the screen:

def on_key(self):\n# Add MyWidget to the screen\nself.mount(MyWidget(\"Hello, World!\"))\n

In this example we don't care that the widget hasn't been mounted immediately, only that it will be soon.

Note

Textual awaits the result of mount after the message handler, so even if you don't explicitly await it, it will have been completed by the time the next message handler runs.

We might care if we want to mount a widget then make some changes to it. By making the handler async and awaiting the result of mount, we can be sure that the widget has been initialized before we update it:

async def on_key(self):\n# Add MyWidget to the screen\nawait self.mount(MyWidget(\"Hello, World!\"))\n# add a border\nself.query_one(MyWidget).styles.border = (\"heavy\", \"red\")\n

Incidentally, I found there were very few examples of writing awaitable objects in Python. So here is the code for AwaitMount which is returned by the mount method:

class AwaitMount:\n\"\"\"An awaitable returned by mount() and mount_all().\"\"\"\ndef __init__(self, parent: Widget, widgets: Sequence[Widget]) -> None:\nself._parent = parent\nself._widgets = widgets\nasync def __call__(self) -> None:\n\"\"\"Allows awaiting via a call operation.\"\"\"\nawait self\ndef __await__(self) -> Generator[None, None, None]:\nasync def await_mount() -> None:\nif self._widgets:\naws = [\ncreate_task(widget._mounted_event.wait(), name=\"await mount\")\nfor widget in self._widgets\n]\nif aws:\nawait wait(aws)\nself._parent.refresh(layout=True)\nreturn await_mount().__await__()\n
"},{"location":"blog/2023/03/15/no-async-async-with-python/#summing-up","title":"Summing up","text":"

Textual did initially \"async all the things\", which you might see if you find some old Textual code. Now async is optional.

This is not because I dislike async. I'm a fan! But it does place a small burden on the developer (more to type and think about). With the current API you generally don't need to write coroutines, or remember to await things. But async is there if you need it.

We're finding that Textual is increasingly becoming a UI to things which are naturally concurrent, so async was a good move. Concurrency can be a tricky subject, so we're planning some API magic to take the pain out of running tasks, threads, and processes. Stay tuned!

Join us on our Discord server if you want to talk about these things with the Textualize developers.

"},{"location":"blog/2022/12/08/be-the-keymaster/","title":"Be the Keymaster!","text":""},{"location":"blog/2022/12/08/be-the-keymaster/#that-didnt-go-to-plan","title":"That didn't go to plan","text":"

So... yeah... the blog. When I wrote my previous (and first) post I had wanted to try and do a post towards the end of each week, highlighting what I'd done on the \"dogfooding\" front. Life kinda had other plans. Not in a terrible way, but it turns out that getting both flu and Covid jabs (AKA \"jags\" as they tend to say in my adopted home) on the same day doesn't really agree with me too well.

I have been working, but there's been some odd moments in the past week and a bit and, last week, once I got to the end, I was glad for it to end. So no blog post happened.

Anyway...

"},{"location":"blog/2022/12/08/be-the-keymaster/#what-have-i-been-up-to","title":"What have I been up to?","text":"

While mostly sat feeling sorry for myself on my sofa, I have been coding. Rather than list all the different things here in detail, I'll quickly mention them with links to where to find them and play with them if you want:

"},{"location":"blog/2022/12/08/be-the-keymaster/#fivepyfive","title":"FivePyFive","text":"

While my Textual 5x5 puzzle is one of the examples in the Textual repo, I wanted to make it more widely available so people can download it with pip or pipx. See over on PyPi and see if you can solve it. ;-)

"},{"location":"blog/2022/12/08/be-the-keymaster/#textual-qrcode","title":"textual-qrcode","text":"

I wanted to put together a very small example of how someone may put together a third party widget library, and in doing so selected what I thought was going to be a mostly-useless example: a wrapper around a text-based QR code generator website. Weirdly I've had a couple of people express a need for QR codes in the terminal since publishing that!

"},{"location":"blog/2022/12/08/be-the-keymaster/#pispy","title":"PISpy","text":"

PISpy is a very simple terminal-based client for the PyPi API. Mostly it provides a hypertext interface to Python package details, letting you look up a package and then follow its dependency links. It's very simple at the moment, but I think more fun things can be done with this.

"},{"location":"blog/2022/12/08/be-the-keymaster/#oidia","title":"OIDIA","text":"

I'm a big fan of the use of streak-tracking in one form or another. Personally I use a streak-tracking app for keeping tabs of all sorts of good (and bad) habits, and as a heavy user of all things Apple I make a lot of use of the Fitness rings, etc. So I got to thinking it might be fun to do a really simple, no shaming, no counting, just recording, steak app for the Terminal. OIDIA is the result.

As of the time of writing I only finished the first version of this yesterday evening, so there are plenty of rough edges; but having got it to a point where it performed the basic tasks I wanted from it, that seemed like a good time to publish.

Expect to see this getting more updates and polish.

"},{"location":"blog/2022/12/08/be-the-keymaster/#wait-what-about-this-keymaster-thing","title":"Wait, what about this Keymaster thing?","text":"

Ahh, yes, about that... So one of the handy things I'm finding about Textual is its key binding system. The more I build Textual apps, the more I appreciate the bindings, how they can be associated with specific widgets, the use of actions (which can be used from other places too), etc.

But... (there's always a \"but\" right -- I mean, there'd be no blog post to be had here otherwise).

The terminal doesn't have access to all the key combinations you may want to use, and also, because some keys can't necessarily be \"typed\", at least not easily (think about it: there's no F1 character, you have to type F1), many keys and key combinations need to be bound with specific names.

So there's two problems here: how do I discover what keys even turn up in my application, and when they do, what should I call them when I pass them to Binding?

That felt like a \"well Dave just build an app for it!\" problem. So I did:

If you're building apps with Textual and you want to discover what keys turn up from your terminal and are available to your application, you can:

$ pipx install textual-keys\n

and then just run textual-keys and start mashing the keyboard to find out.

There's a good chance that this app, or at least a version of it, will make it into Textual itself (very likely as one of the devtools). But for now it's just an easy install away.

I think there's a call to be made here too: have you built anything to help speed up how you work with Textual, or just make the development experience \"just so\"? If so, do let us know, and come yell about it on the #show-and-tell channel in our Discord server.

"},{"location":"blog/2022/12/30/a-better-asyncio-sleep-for-windows-to-fix-animation/","title":"A better asyncio sleep for Windows to fix animation","text":"

I spent some time optimizing Textual on Windows recently, and discovered something which may be of interest to anyone working with async code on that platform.

Animation, scrolling, and fading had always been unsatisfactory on Windows. Textual was usable, but the lag when scrolling made apps feel far less snappy that other platforms. On macOS and Linux, scrolling is fast enough that it feels close to a native app, not something running in a terminal. Yet the Windows experience never improved, even as Textual got faster with each release.

I had chalked this up to Windows Terminal being slow to render updates. After all, the classic Windows terminal was (and still is) glacially slow. Perhaps Microsoft just weren't focusing on performance.

In retrospect, that was highly improbable. Like all modern terminals, Windows Terminal uses the GPU to render updates. Even without focussing on performance, it should be fast.

I figured I'd give it one last attempt to speed up Textual on Windows. If I failed, Windows would forever be a third-class platform for Textual apps.

It turned out that it was nothing to do with performance, per se. The issue was with a single asyncio function: asyncio.sleep.

Textual has a Timer class which creates events at regular intervals. It powers the JS-like set_interval and set_timer functions. It is also used internally to do animation (such as smooth scrolling). This Timer class calls asyncio.sleep to wait the time between one event and the next.

On macOS and Linux, calling asynco.sleep is fairly accurate. If you call sleep(3.14), it will return within 1% of 3.14 seconds. This is not the case for Windows, which for historical reasons uses a timer with a granularity of 15 milliseconds. The upshot is that sleep times will be rounded up to the nearest multiple of 15 milliseconds.

This limit appears to hold true for all async primitives on Windows. If you wait for something with a timeout, it will return on a multiple of 15 milliseconds. Fortunately there is work in the CPython pipeline to make this more accurate. Thanks to Steve Dower for pointing this out.

This lack of accuracy in the timer meant that timer events were created at a far slower rate than intended. Animation was slower because Textual was waiting too long between updates.

Once I had figured that out, I needed an alternative to asyncio.sleep for Textual's Timer class. And I found one. The following version of sleep is accurate to well within 1%:

from time import sleep as time_sleep\nfrom asyncio import get_running_loop\nasync def sleep(sleep_for: float) -> None:\n\"\"\"An asyncio sleep.\n    On Windows this achieves a better granularity than asyncio.sleep\n    Args:\n        sleep_for (float): Seconds to sleep for.\n    \"\"\"    \nawait get_running_loop().run_in_executor(None, time_sleep, sleep_for)\n

That is a drop-in replacement for sleep on Windows. With it, Textual runs a lot smoother. Easily on par with macOS and Linux.

It's not quite perfect. There is a little tearing during full \"screen\" updates, but performance is decent all round. I suspect when this bug is fixed (big thanks to Paul Moore for looking in to that), and Microsoft implements this protocol then Textual on Windows will be A+.

This Windows improvement will be in v0.9.0 of Textual, which will be released in a few days.

"},{"location":"blog/2023/02/11/the-heisenbug-lurking-in-your-async-code/","title":"The Heisenbug lurking in your async code","text":"

I'm taking a brief break from blogging about Textual to bring you this brief PSA for Python developers who work with async code. I wanted to expand a little on this tweet.

If you have ever used asyncio.create_task you may have created a bug for yourself that is challenging (read almost impossible) to reproduce. If it occurs, your code will likely fail in unpredictable ways.

The root cause of this Heisenbug is that if you don't hold a reference to the task object returned by create_task then the task may disappear without warning when Python runs garbage collection. In other words, the code in your task will stop running with no obvious indication why.

This behavior is well documented, as you can see from this excerpt (emphasis mine):

But who reads all the docs? And who has perfect recall if they do? A search on GitHub indicates that there are a lot of projects where this bug is waiting for just the right moment to ruin somebody's day.

I suspect the reason this mistake is so common is that tasks are a lot like threads (conceptually at least). With threads you can just launch them and forget. Unless you mark them as \"daemon\" threads they will exist for the lifetime of your app. Not so with Tasks.

The solution recommended in the docs is to keep a reference to the task for as long as you need it to live. On modern Python you could use TaskGroups which will keep references to your tasks. As long as all the tasks you spin up are in TaskGroups, you should be fine.

"},{"location":"blog/2023/03/08/overhead-of-python-asyncio-tasks/","title":"Overhead of Python Asyncio tasks","text":"

Every widget in Textual, be it a button, tree view, or a text input, runs an asyncio task. There is even a task for scrollbar corners (the little space formed when horizontal and vertical scrollbars meet).

Info

It may be IO that gives AsyncIO its name, but Textual doesn't do any IO of its own. Those tasks are used to power message queues, so that widgets (UI components) can do whatever they do at their own pace.

Its fair to say that Textual apps launch a lot of tasks. Which is why when I was trying to optimize startup (for apps with 1000s of widgets) I suspected it was task related.

I needed to know how much of an overhead it was to launch tasks. Tasks are lighter weight than threads, but how much lighter? The only way to know for certain was to profile.

The following code launches a load of do nothing tasks, then waits for them to shut down. This would give me an idea of how performant create_task is, and also a baseline for optimizations. I would know the absolute limit of any optimizations I make.

from asyncio import create_task, wait, run\nfrom time import process_time as time\nasync def time_tasks(count=100) -> float:\n\"\"\"Time creating and destroying tasks.\"\"\"\nasync def nop_task() -> None:\n\"\"\"Do nothing task.\"\"\"\npass\nstart = time()\ntasks = [create_task(nop_task()) for _ in range(count)]\nawait wait(tasks)\nelapsed = time() - start\nreturn elapsed\nfor count in range(100_000, 1000_000 + 1, 100_000):\ncreate_time = run(time_tasks(count))\ncreate_per_second = 1 / (create_time / count)\nprint(f\"{count:,} tasks \\t {create_per_second:0,.0f} tasks per/s\")\n

And here is the output:

100,000 tasks    280,003 tasks per/s\n200,000 tasks    255,275 tasks per/s\n300,000 tasks    248,713 tasks per/s\n400,000 tasks    248,383 tasks per/s\n500,000 tasks    241,624 tasks per/s\n600,000 tasks    260,660 tasks per/s\n700,000 tasks    244,510 tasks per/s\n800,000 tasks    247,455 tasks per/s\n900,000 tasks    242,744 tasks per/s\n1,000,000 tasks          259,715 tasks per/s\n

Info

Running on an M1 MacBook Pro.

This tells me I can create, run, and shutdown 260K tasks per second.

That's fast.

Clearly create_task is as close as you get to free in the Python world, and I would need to look elsewhere for optimizations. Turns out Textual spends far more time processing CSS rules than creating tasks (obvious in retrospect). I've noticed some big wins there, so the next version of Textual will be faster to start apps with a metric tonne of widgets.

But I still need to know what to do with those scrollbar corners. A task for two characters. I don't even...

"},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/","title":"A year of building for the terminal","text":"

I joined Textualize back in January 2022, and since then have been hard at work with the team on both Rich and Textual. Over the course of the year, I\u2019ve been able to work on a lot of really cool things. In this post, I\u2019ll review a subset of the more interesting and visual stuff I\u2019ve built. If you\u2019re into terminals and command line tooling, you\u2019ll hopefully see at least one thing of interest!

"},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/#a-file-manager-powered-by-textual","title":"A file manager powered by Textual","text":"

I\u2019ve been slowly developing a file manager as a \u201cdogfooding\u201d project for Textual. It takes inspiration from tools such as Ranger and Midnight Commander.

As of December 2022, it lets you browse your file system, filtering, multi-selection, creating and deleting files/directories, opening files in your $EDITOR and more.

I\u2019m happy with how far this project has come \u2014 I think it\u2019s a good example of the type of powerful application that can be built with Textual with relatively little code. I\u2019ve been able to focus on features, instead of worrying about terminal emulator implementation details.

The project is available on GitHub.

"},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/#better-diffs-in-the-terminal","title":"Better diffs in the terminal","text":"

Diffs in the terminal are often difficult to read at a glance. I wanted to see how close I could get to achieving a diff display of a quality similar to that found in the GitHub UI.

To attempt this, I built a tool called Dunk. It\u2019s a command line program which you can pipe your git diff output into, and it\u2019ll convert it into something which I find much more readable.

Although I\u2019m not particularly proud of the code - there are a lot of \u201chacks\u201d going on, but I\u2019m proud of the result. If anything, it shows what can be achieved for tools like this.

For many diffs, the difference between running git diff and git diff | dunk | less -R is night and day.

It\u2019d be interesting to revisit this at some point. It has its issues, but I\u2019d love to see how it can be used alongside Textual to build a terminal-based diff/merge tool. Perhaps it could be combined with\u2026

"},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/#code-editor-floating-gutter","title":"Code editor floating gutter","text":"

This is a common feature in text editors and IDEs: when you scroll to the right, you should still be able to see what line you\u2019re on. Out of interest, I tried to recreate the effect in the terminal using Textual.

Textual CSS offers a dock property which allows you to attach a widget to an edge of its parent. By creating a widget that contains a vertical list of numbers and setting the dock property to left, we can create a floating gutter effect. Then, we just need to keep the scroll_y in sync between the gutter and the content to ensure the line numbers stay aligned.

"},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/#dropdown-autocompletion-menu","title":"Dropdown autocompletion menu","text":"

While working on Shira (a proof-of-concept, terminal-based Python object explorer), I wrote some autocompleting dropdown functionality.

Textual forgoes the z-index concept from browser CSS and instead uses a \u201cnamed layer\u201d system. Using the layers property you can defined an ordered list of named layers, and using the layer property, you can assign a descendant widget to one of those layers.

By creating a new layer above all others and assigning a widget to that layer, we can ensure that widget is painted above everything else.

In order to determine where to place the dropdown, we can track the current value in the dropdown by watching the reactive input \u201cvalue\u201d inside the Input widget. This method will be called every time the value of the Input changes, and we can use this hook to amend the position of our dropdown position to accommodate for the length of the input value.

I\u2019ve now extracted this into a separate library called textual-autocomplete.

"},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/#tabs-with-animated-underline","title":"Tabs with animated underline","text":"

The aim here was to create a tab widget with underlines that animates smoothly as another tab is selected.

The difficulty with implementing something like this is that we don\u2019t have pixel-perfect resolution when animating - a terminal window is just a big grid of fixed-width character cells.

However, when animating things in a terminal, we can often achieve better granularity using Unicode related tricks. In this case, instead of shifting the bar along one whole cell, we adjust the endings of the bar to be a character which takes up half of a cell.

The exact characters that form the bar are \"\u257a\", \"\u2501\" and \"\u2578\". When the bar sits perfectly within cell boundaries, every character is \u201c\u2501\u201d. As it travels over a cell boundary, the left and right ends of the bar are updated to \"\u257a\" and \"\u2578\" respectively.

"},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/#snapshot-testing-for-terminal-apps","title":"Snapshot testing for terminal apps","text":"

One of the great features we added to Rich this year was the ability to export console contents to an SVG. This feature was later exposed to Textual, allowing users to capture screenshots of their running Textual apps. Ultimately, I ended up creating a tool for snapshot testing in the Textual codebase.

Snapshot testing is used to ensure that Textual output doesn\u2019t unexpectedly change. On disk, we store what we expect the output to look like. Then, when we run our unit tests, we get immediately alerted if the output has changed.

This essentially automates the process of manually spinning up several apps and inspecting them for unexpected visual changes. It\u2019s great for catching subtle regressions!

In Textual, each CSS property has its own canonical example and an associated snapshot test. If we accidentally break a property in a way that affects the visual output, the chances of it sneaking into a release are greatly reduced, because the corresponding snapshot test will fail.

As part of this work, I built a web interface for comparing snapshots with test output. There\u2019s even a little toggle which highlights the differences, since they\u2019re sometimes rather subtle.

Since the terminal output shown in the video above is just an SVG image, I was able to add the \"Show difference\" functionality by overlaying the two images and applying a single CSS property: mix-blend-mode: difference;.

The snapshot testing functionality itself is implemented as a pytest plugin, and it builds on top of a snapshot testing framework called syrupy.

It's quite likely that this will eventually be exposed to end-users of Textual.

"},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/#demonstrating-animation","title":"Demonstrating animation","text":"

I built an example app to demonstrate how to animate in Textual and the available easing functions.

The smoothness here is achieved using tricks similar to those used in the tabs I discussed earlier. In fact, the bar that animates in the video above is the same Rich renderable that is used by Textual's scrollbars.

You can play with this app by running textual easing. Please use animation sparingly.

"},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/#developer-console","title":"Developer console","text":"

When developing terminal based applications, performing simple debugging using print can be difficult, since the terminal is in application mode.

A project I worked on earlier in the year to improve the situation was the Textual developer console, which you can launch with textual console.

On the right, Dave's 5x5 Textual app. On the left, the Textual console.

Then, by running a Textual application with the --dev flag, all standard output will be redirected to it. This means you can use the builtin print function and still immediately see the output. Textual itself also writes information to this console, giving insight into the messages that are flowing through an application.

"},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/#pixel-art","title":"Pixel art","text":"

Cells in the terminal are roughly two times taller than they are wide. This means, that two horizontally adjacent cells form an approximate square.

Using this fact, I wrote a simple library based on Rich and PIL which can convert an image file into terminal output. You can find the library, rich-pixels, on GitHub.

It\u2019s particularly good for displaying simple pixel art images. The SVG image below is also a good example of the SVG export functionality I touched on earlier.

Rich

Since the library generates an object which is renderable using Rich, these can easily be embedded inside Textual applications.

Here's an example of that in a scrapped \"Pok\u00e9dex\" app I threw together:

This is a rather naive approach to the problem... but I did it for fun!

Other methods for displaying images in the terminal include:

  • A more advanced library like chafa, which uses a range of Unicode characters to achieve a more accurate representation of the image.
  • One of the available terminal image protocols, such as Sixel, Kitty\u2019s Terminal Graphics Protocol, and iTerm Inline Images Protocol.

That was a whirlwind tour of just some of the projects I tackled in 2022. If you found it interesting, be sure to follow me on Twitter. I don't post often, but when I do, it's usually about things similar to those I've discussed here.

"},{"location":"blog/2022/11/06/new-blog/","title":"New Blog","text":"

Welcome to the first post on the Textual blog.

I plan on using this as a place to make announcements regarding new releases of Textual, and any other relevant news.

The first piece of news is that we've reorganized this site a little. The Events, Styles, and Widgets references are now under \"Reference\", and what used to be under \"Reference\" is now \"API\" which contains API-level documentation. I hope that's a little clearer than it used to be!

"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/","title":"So you're looking for a wee bit of Textual help...","text":""},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#introduction","title":"Introduction","text":"

Quote

Patience, Highlander. You have done well. But it'll take time. You are generations being born and dying. You are at one with all living things. Each man's thoughts and dreams are yours to know. You have power beyond imagination. Use it well, my friend. Don't lose your head.

Juan S\u00e1nchez Villalobos Ram\u00edrez, Chief metallurgist to King Charles V of Spain

As of the time of writing, I'm a couple or so days off having been with Textualize for 3 months. It's been fun, and educational, and every bit as engaging as I'd hoped, and more. One thing I hadn't quite prepared for though, but which I really love, is how so many other people are learning Textual along with me.

Even in those three months the library has changed and expanded quite a lot, and it continues to do so. Meanwhile, more people are turning up and using the framework; you can see this online in social media, blogs and of course in the ever-growing list of projects on GitHub which depend on Textual.

This inevitably means there's a lot of people getting to grips with a new tool, and one that is still a bit of a moving target. This in turn means lots of people are coming to us to get help.

As I've watched this happen I've noticed a few patterns emerging. Some of these good or neutral, some... let's just say not really beneficial to those seeking the help, or to those trying to provide the help. So I wanted to write a little bit about the different ways you can get help with Textual and your Textual-based projects, and to also try and encourage people to take the most helpful and positive approach to getting that help.

Now, before I go on, I want to make something very clear: I'm writing this as an individual. This is my own personal view, and my own advice from me to anyone who wishes to take it. It's not Textual (the project) or Textualize (the company) policy, rules or guidelines. This is just some ageing hacker's take on how best to go about asking for help, informed by years of asking for and also providing help in email, on Usenet, on forums, etc.

Or, put another way: if what you read in here seems sensible to you, I figure we'll likely have already hit it off over on GitHub or in the Discord server. ;-)

"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#where-to-go-for-help","title":"Where to go for help","text":"

At this point this is almost a bit of an FAQ itself, so I thought I'd address it here: where's the best place to ask for help about Textual, and what's the difference between GitHub Issues, Discussions and our Discord server?

I'd suggest thinking of them like this:

"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#discord","title":"Discord","text":"

You have a question, or need help with something, and perhaps you could do with a reply as soon as possible. But, and this is the really important part, it doesn't matter if you don't get a response. If you're in this situation then the Discord server is possibly a good place to start. If you're lucky someone will be hanging about who can help out.

I can't speak for anyone else, but keep this in mind: when I look in on Discord I tend not to go scrolling back much to see if anything has been missed. If something catches my eye, I'll try and reply, but if it doesn't... well, it's mostly an instant chat thing so I don't dive too deeply back in time.

Going from Discord to a GitHub issue

As a slight aside here: sometimes people will pop up in Discord, ask a question about something that turns out looking like a bug, and that's the last we hear of it. Please, please, please, if this happens, the most helpful thing you can do is go raise an issue for us. It'll help us to keep track of problems, it'll help get your problem fixed, it'll mean everyone benefits.

My own advice would be to treat Discord as an ephemeral resource. It happens in the moment but fades away pretty quickly. It's like knocking on a friend's door to see if they're in. If they're not in, you might leave them a note, which is sort of like going to...

"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#github","title":"GitHub","text":"

On the other hand, if you have a question or need some help or something where you want to stand a good chance of the Textual developers (amongst others) seeing it and responding, I'd recommend that GitHub is the place to go. Dropping something into the discussions there, or leaving an issue, ensures it'll get seen. It won't get lost.

As for which you should use -- a discussion or an issue -- I'd suggest this: if you need help with something, or you want to check your understanding of something, or you just want to be sure something is a problem before taking it further, a discussion might be the best thing. On the other hand, if you've got a clear bug or feature request on your hands, an issue makes a lot of sense.

Don't worry if you're not sure which camp your question or whatever falls into though; go with what you think is right. There's no harm done either way (I may move an issue to a discussion first before replying, if it's really just a request for help -- but that's mostly so everyone can benefit from finding it in the right place later on down the line).

"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#the-dos-and-donts-of-getting-help","title":"The dos and don'ts of getting help","text":"

Now on to the fun part. This is where I get a bit preachy. Ish. Kinda. A little bit. Again, please remember, this isn't a set of rules, this isn't a set of official guidelines, this is just a bunch of \"if you want my advice, and I know you didn't ask but you've read this far so you actually sort of did don't say I didn't warn you!\" waffle.

This isn't going to be an exhaustive collection, far from it. But I feel these are some important highlights.

"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#do","title":"Do...","text":"

When looking for help, in any of the locations mentioned above, I'd totally encourage:

"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#be-clear-and-detailed","title":"Be clear and detailed","text":"

Too much detail is almost always way better than not enough. \"My program didn't run\", often even with some of the code supplied, is so much harder to help than \"I ran this code I'm posting here, and I expected this particular outcome, and I expected it because I'd read this particular thing in the docs and had comprehended it to mean this, but instead the outcome was this exception here, and I'm a bit stuck -- can someone offer some pointers?\"

The former approach means there often ends up having to be a back and forth which can last a long time, and which can sometimes be frustrating for the person asking. Manage frustration: be clear, tell us everything you can.

"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#say-what-resources-youve-used-already","title":"Say what resources you've used already","text":"

If you've read the potions of the documentation that relate to what you're trying to do, it's going to be really helpful if you say so. If you don't, it might be assumed you haven't and you may end up being pointed at them.

So, please, if you've checked the documentation, looked in the FAQ, done a search of past issues or discussions or perhaps even done a search on the Discord server... please say so.

"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#be-polite","title":"Be polite","text":"

This one can go a long way when looking for help. Look, I get it, programming is bloody frustrating at times. We've all rage-quit some code at some point, I'm sure. It's likely going to be your moment of greatest frustration when you go looking for help. But if you turn up looking for help acting all grumpy and stuff it's not going to come over well. Folk are less likely to be motivated to lend a hand to someone who seems rather annoyed.

If you throw in a please and thank-you here and there that makes it all the better.

"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#fully-consider-the-replies","title":"Fully consider the replies","text":"

You could find yourself getting a reply that you're sure won't help at all. That's fair. But be sure to fully consider it first. Perhaps you missed the obvious along the way and this is 100% the course correction you'd unknowingly come looking for in the first place. Sure, the person replying might have totally misunderstood what was being asked, or might be giving a wrong answer (it me! I've totally done that and will again!), but even then a reply along the lines of \"I'm not sure that's what I'm looking for, because...\" gets everyone to the solution faster than \"lol nah\".

"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#entertain-what-might-seem-like-odd-questions","title":"Entertain what might seem like odd questions","text":"

Aye, I get it, being asked questions when you're looking for an answer can be a bit frustrating. But if you find yourself on the receiving end of a small series of questions about your question, keep this in mind: Textual is still rather new and still developing and it's possible that what you're trying to do isn't the correct way to do that thing. To the person looking to help you it may seem to them you have an XY problem.

Entertaining those questions might just get you to the real solution to your problem.

"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#allow-for-language-differences","title":"Allow for language differences","text":"

You don't need me to tell you that a project such as Textual has a global audience. With that rather obvious fact comes the other fact that we don't all share the same first language. So, please, as much as possible, try and allow for that. If someone is trying to help you out, and they make it clear they're struggling to follow you, keep this in mind.

"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#acknowledge-the-answer","title":"Acknowledge the answer","text":"

I suppose this is a variation on \"be polite\" (really, a thanks can go a long way), but there's more to this than a friendly acknowledgement. If someone has gone to the trouble of offering some help, it's helpful to everyone who comes after you to acknowledge if it worked or not. That way a future help-seeker will know if the answer they're reading stands a chance of being the right one.

"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#accept-that-textual-is-zero-point-software-right-now","title":"Accept that Textual is zero-point software (right now)","text":"

Of course the aim is to have every release of Textual be stable and useful, but things will break. So, please, do keep in mind things like:

  • Textual likely doesn't have your feature of choice just yet.
  • We might accidentally break something (perhaps pinning Textual and testing each release is a good plan here?).
  • We might deliberately break something because we've decided to take a particular feature or way of doing things in a better direction.

Of course it can be a bit frustrating a times, but overall the aim is to have the best framework possible in the long run.

"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#dont","title":"Don't...","text":"

Okay, now for a bit of old-hacker finger-wagging. Here's a few things I'd personally discourage:

"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#lack-patience","title":"Lack patience","text":"

Sure, it can be annoying. You're in your flow, you've got a neat idea for a thing you want to build, you're stuck on one particular thing and you really need help right now! Thing is, that's unlikely to happen. Badgering individuals, or a whole resource, to reply right now, or complaining that it's been $TIME_PERIOD since you asked and nobody has replied... that's just going to make people less likely to reply.

"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#unnecessarily-tag-individuals","title":"Unnecessarily tag individuals","text":"

This one often goes hand in hand with the \"lack patience\" thing: Be it asking on Discord, or in GitHub issues, discussions or even PRs, unnecessarily tagging individuals is a bit rude. Speaking for myself and only myself: I love helping folk with Textual. If I could help everyone all the time the moment they have a problem, I would. But it doesn't work like that. There's any number of reasons I might not be responding to a particular request, including but not limited to (here I'm talking personally because I don't want to speak for anyone else, but I'm sure I'm not alone here):

  • I have a job. Sure, my job is (in part) Textual, but there's more to it than that particular issue. I might be doing other stuff.
  • I have my own projects to work on too. I like coding for fun as well (or writing preaching old dude blog posts like this I guess, but you get the idea).
  • I actually have other interests outside of work hours so I might actually be out doing a 10k in the local glen, or battling headcrabs in VR, or something.
  • Housework. :-/

You get the idea though. So while I'm off having a well-rounded life, it's not good to get unnecessarily intrusive alerts to something that either a) doesn't actually directly involve me or b) could wait.

"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#seek-personal-support","title":"Seek personal support","text":"

Again, I'm going to speak totally for myself here, but I also feel the general case is polite for all: there's a lot of good support resources available already; sending DMs on Discord or Twitter or in the Fediverse, looking for direct personal support, isn't really the best way to get help. Using the public/collective resources is absolutely the best way to get that help. Why's it a bad idea to dive into DMs? Here's some reasons I think it's not a good idea:

  • It's a variation on \"unnecessarily tagging individuals\".
  • You're short-changing yourself when it comes to getting help. If you ask somewhere more public you're asking a much bigger audience, who collectively have more time, more knowledge and more experience than a single individual.
  • Following on from that, any answers can be (politely) fact-checked or enhanced by that audience, resulting in a better chance of getting the best help possible.
  • The next seeker-of-help gets to miss out on your question and the answer. If asked and answered in public, it's a record that can help someone else in the future.
"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#doubt-your-ability-or-skill-level","title":"Doubt your ability or skill level","text":"

I suppose this should really be phrased as a do rather than a don't, as here I want to encourage something positive. A few times I've helped people out who have been very apologetic about their questions being \"noob\" questions, or about how they're fairly new to Python, or programming in general. Really, please, don't feel the need to apologise and don't be ashamed of where you're at.

If you've asked something that's obviously answered in the documentation, that's not a problem; you'll likely get pointed at the docs and it's what happens next that's the key bit. If the attitude is \"oh, cool, that's exactly what I needed to be reading, thanks!\" that's a really positive thing. The only time it's a problem is when there's a real reluctance to use the available resources. We've all seen that person somewhere at some point, right? ;-)

Not knowing things is totally cool.

"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#conclusion","title":"Conclusion","text":"

So, that's my waffle over. As I said at the start: this is my own personal thoughts on how to get help with Textual, both as someone whose job it is to work on Textual and help people with Textual, and also as a FOSS advocate and supporter who can normally be found helping Textual users when he's not \"on the clock\" too.

What I've written here isn't exhaustive. Neither is it novel. Plenty has been written on the general subject in the past, and I'm sure more will be written on the subject in the future. I do, however, feel that these are the most common things I notice. I'd say those dos and don'ts cover 90% of \"can I get some help?\" interactions; perhaps closer to 99%.

Finally, and I think this is the most important thing to remember, the next time you are battling some issue while working with Textual: don't lose your head!

"},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/","title":"On dog food, the (original) Metaverse, and (not) being bored","text":""},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#introduction","title":"Introduction","text":"

Quote

Cutler, armed with a schedule, was urging the team to \"eat its own dog food\". Part macho stunt and part common sense, the \"dog food diet\" was the cornerstone of Cutler\u2019s philosophy.

G. Pascal Zachary \u2014 Show-Stopper!

I can't remember exactly when it was -- it was likely late in 1994 or some time in 1995 -- when I first came across the concept of, or rather the name for the concept of, \"eating your own dog food\". The idea and the name played a huge part in the book Show-Stopper! by G. Pascal Zachary. The idea wasn't new to me of course; I'd been writing code for over a decade by then and plenty of times I'd built things and then used those things to do things, but it was fascinating to a mostly-self-taught 20-something me to be reading this (excellent -- go read it if you care about the history of your craft) book and to see the idea written down and named.

While Textualize isn't (thankfully -- really, I do recommend reading the book) anything like working on the team building Windows NT, the idea of taking a little time out from working on Textual, and instead work with Textual, makes a lot of sense. It's far too easy to get focused on adding things and improving things and tweaking things while losing sight of the fact that people will want to build with your product.

So you can imagine how pleased I was when Will announced that he wanted all of us to spend a couple or so weeks building something with Textual. I had, of course, already written one small application with the library, and had plans for another (in part it's how I ended up working here), but I'd yet to really dive in and try and build something more involved.

Giving it some thought: I wasn't entirely sure what I wanted to build though. I do want to use Textual to build a brand new terminal-based Norton Guide reader (not my first, not by a long way) but I felt that was possibly a bit too niche, and actually could take a bit too long anyway. Maybe not, it remains to be seen.

Eventually I decided on this approach: try and do a quick prototype of some daft idea each day or each couple of days, do that for a week or so, and then finally try and settle down on something less trivial. This approach should work well in that it'll help introduce me to more of Textual, help try out a few different parts of the library, and also hopefully discover some real pain-points with working with it and highlight a list of issues we should address -- as seen from the perspective of a developer working with the library.

So, here I am, at the end of week one. What I want to try and do is briefly (yes yes, I know, this introduction is the antithesis of brief) talk about what I built and perhaps try and highlight some lessons learnt, highlight some patterns I think are useful, and generally do an end-of-week version of a TIL. TWIL?

Yeah. I guess this is a TWIL.

"},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#gridinfo","title":"gridinfo","text":"

I started the week by digging out a quick hack I'd done a couple of weeks earlier, with a view to cleaning it up. It started out as a fun attempt to do something with Rich Pixels while also making a terminal-based take on slstats.el. I'm actually pleased with the result and how quickly it came together.

The point of the application itself is to show some general information about the current state of the Second Life grid (hello to any fellow residents of the original Metaverse!), and to also provide a simple region lookup screen that, using Rich Pixels, will display the object map (albeit in pretty low resolution -- but that's the fun of this!).

So the opening screen looks like this:

and a lookup of a region looks like this:

Here's a wee video of the whole thing in action:

"},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#worth-a-highlight","title":"Worth a highlight","text":"

Here's a couple of things from the code that I think are worth a highlight, as things to consider when building Textual apps:

"},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#dont-use-the-default-screen","title":"Don't use the default screen","text":"

Use of the default Screen that's provided by the App is handy enough, but I feel any non-trivial application should really put as much code as possible in screens that relate to key \"work\". Here's the entirety of my application code:

class GridInfo( App[ None ] ):\n\"\"\"TUI app for showing information about the Second Life grid.\"\"\"\nCSS_PATH = \"gridinfo.css\"\n\"\"\"The name of the CSS file for the app.\"\"\"\nTITLE = \"Grid Information\"\n\"\"\"str: The title of the application.\"\"\"\nSCREENS = {\n\"main\": Main,\n\"region\": RegionInfo\n}\n\"\"\"The collection of application screens.\"\"\"\ndef on_mount( self ) -> None:\n\"\"\"Set up the application on startup.\"\"\"\nself.push_screen( \"main\" )\n

You'll notice there's no work done in the app, other than to declare the screens, and to set the main screen running when the app is mounted.

"},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#dont-work-hard-on_mount","title":"Don't work hard on_mount","text":"

My initial version of the application had it loading up the data from the Second Life and GridSurvey APIs in Main.on_mount. This obviously wasn't a great idea as it made the startup appear slow. That's when I realised just how handy call_after_refresh is. This meant I could show some placeholder information and then fire off the requests (3 of them: one to get the main grid information, one to get the grid concurrency data, and one to get the grid size data), keeping the application looking active and updating the display when the replies came in.

"},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#pain-points","title":"Pain points","text":"

While building this app I think there was only really the one pain-point, and I suspect it's mostly more on me than on Textual itself: getting a good layout and playing whack-a-mole with CSS. I suspect this is going to be down to getting more and more familiar with CSS and the terminal (which is different from laying things out for the web), while also practising with various layout schemes -- which is where the revamped Placeholder class is going to be really useful.

"},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#unbored","title":"unbored","text":"

The next application was initially going to be a very quick hack, but actually turned into a less-trivial build than I'd initially envisaged; not in a negative way though. The more I played with it the more I explored and I feel that this ended up being my first really good exploration of some useful (personal -- your kilometerage may vary) patterns and approaches when working with Textual.

The application itself is a terminal client for the Bored-API. I had initially intended to roll my own code for working with the API, but I noticed that someone had done a nice library for it and it seemed silly to not build on that. Not needing to faff with that, I could concentrate on the application itself.

At first I was just going to let the user click away at a button that showed a random activity, but this quickly morphed into a \"why don't I make this into a sort of TODO list builder app, where you can add things to do when you are bored, and delete things you don't care for or have done\" approach.

Here's a view of the main screen:

and here's a view of the filter pop-over:

"},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#worth-a-highlight_1","title":"Worth a highlight","text":""},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#dont-put-all-your-bindings-in-one-place","title":"Don't put all your BINDINGS in one place","text":"

This came about from me overloading the use of the escape key. I wanted it to work more or less like this:

  • If you're inside an activity, move focus up to the activity type selection buttons.
  • If the filter pop-over is visible, close that.
  • Otherwise exit the application.

It was easy enough to do, and I had an action in the Main screen that escape was bound to (again, in the Main screen) that did all this logic with some if/elif work but it didn't feel elegant. Moreover, it meant that the Footer always displayed the same description for the key.

That's when I realised that it made way more sense to have a Binding for escape in every widget that was the actual context for escape's use. So I went from one top-level binding to...

...\nclass Activity( Widget ):\n\"\"\"A widget that holds and displays a suggested activity.\"\"\"\nBINDINGS = [\n...\nBinding( \"escape\", \"deselect\", \"Switch to Types\" )\n]\n...\nclass Filters( Vertical ):\n\"\"\"Filtering sidebar.\"\"\"\nBINDINGS = [\nBinding( \"escape\", \"close\", \"Close Filters\" )\n]\n...\nclass Main( Screen ):\n\"\"\"The main application screen.\"\"\"\nBINDINGS = [\nBinding( \"escape\", \"quit\", \"Close\" )\n]\n\"\"\"The bindings for the main screen.\"\"\"\n

This was so much cleaner and I got better Footer descriptions too. I'm going to be leaning hard on this approach from now on.

"},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#messages-are-awesome","title":"Messages are awesome","text":"

Until I wrote this application I hadn't really had a need to define or use my own Messages. During work on this I realised how handy they really are. In the code I have an Activity widget which takes care of the job of moving itself amongst its siblings if the user asks to move an activity up or down. When this happens I also want the Main screen to save the activities to the filesystem as things have changed.

Thing is: I don't want the screen to know what an Activity is capable of and I don't want an Activity to know what the screen is capable of; especially the latter as I really don't want a child of a screen to know what the screen can do (in this case \"save stuff\").

This is where messages come in. Using a message I could just set things up so that the Activity could shout out \"HEY I JUST DID A THING THAT CHANGES ME\" and not care who is listening and not care what they do with that information.

So, thanks to this bit of code in my Activity widget...

    class Moved( Message ):\n\"\"\"A message to indicate that an activity has moved.\"\"\"\ndef action_move_up( self ) -> None:\n\"\"\"Move this activity up one place in the list.\"\"\"\nif self.parent is not None and not self.is_first:\nparent = cast( Widget, self.parent )\nparent.move_child(\nself, before=parent.children.index( self ) - 1\n)\nself.emit_no_wait( self.Moved( self ) )\nself.scroll_visible( top=True )\n

...the Main screen can do this:

    def on_activity_moved( self, _: Activity.Moved ) -> None:\n\"\"\"React to an activity being moved.\"\"\"\nself.save_activity_list()\n

Warning

The code above used emit_no_wait. Since this blog post was first published that method has been removed from Textual. You should use post_message_no_wait or post_message instead now.

"},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#pain-points_1","title":"Pain points","text":"

On top of the issues of getting to know terminal-based-CSS that I mentioned earlier:

  • Textual currently lacks any sort of selection list or radio-set widget. This meant that I couldn't quite do the activity type picking how I would have wanted. Of course I could have rolled my own widgets for this, but I think I'd sooner wait until such things are in Textual itself.
  • Similar to that, I could have used some validating Input widgets. They too are on the roadmap but I managed to cobble together fairly good working versions for my purposes. In doing so though I did further highlight that the reactive attribute facility needs a wee bit more attention as I ran into some (already-known) bugs. Thankfully in my case it was a very easy workaround.
  • Scrolling in general seems a wee bit off when it comes to widgets that are more than one line tall. While there's nothing really obvious I can point my finger at, I'm finding that scrolling containers sometimes get confused about what should be in view. This becomes very obvious when forcing things to scroll from code. I feel this deserves a dedicated test application to explore this more.
"},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#conclusion","title":"Conclusion","text":"

The first week of \"dogfooding\" has been fun and I'm more convinced than ever that it's an excellent exercise for Textualize to engage in. I didn't quite manage my plan of \"one silly trivial prototype per day\", which means I've ended up with two (well technically one and a half I guess given that gridinfo already existed as a prototype) applications rather than four. I'm okay with that. I got a lot of utility out of this.

Now to look at the list of ideas I have going and think about what I'll kick next week off with...

"},{"location":"blog/2022/11/22/what-i-learned-from-my-first-non-trivial-pr/","title":"What I learned from my first non-trivial PR","text":"PlaceholderApp Placeholder\u00a0p2\u00a0here! This\u00a0is\u00a0a\u00a0custom\u00a0label\u00a0for\u00a0p1. #p4 #p3#p5Placeholde r Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0 26\u00a0x\u00a06amet,\u00a0consectetur\u00a027\u00a0x\u00a06 adipiscing\u00a0elit.\u00a0Etiam\u00a0 feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0 Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0 consectetur\u00a0adipiscing\u00a0elit.\u00a0Etiam\u00a040\u00a0x\u00a06 feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0accumsan.\u00a0 Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis gravida.\u00a0Phasellus\u00a0id\u00a0eleifend\u00a0 ligula.\u00a0Nullam\u00a0imperdiet\u00a0sem\u00a0tellus, sed\u00a0vehicula\u00a0nisl\u00a0faucibus\u00a0sit\u00a0amet.Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0 Praesent\u00a0iaculis\u00a0tempor\u00a0ultricies.\u00a0\u2586\u2586consectetur\u00a0adipiscing\u00a0elit.\u00a0Etiam\u00a0\u2586\u2586 Sed\u00a0lacinia,\u00a0tellus\u00a0id\u00a0rutrum\u00a0feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0accumsan.\u00a0 lacinia,\u00a0sapien\u00a0sapien\u00a0congue\u00a0Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis

It's 8:59 am and, by my Portuguese standards, it is freezing cold outside: 5 or 6 degrees Celsius. It is my second day at Textualize and I just got into the office. I undress my many layers of clothing to protect me from the Scottish cold and I sit down in my improvised corner of the Textualize office. As I sit down, I turn myself in my chair to face my boss and colleagues to ask \u201cSo, what should I do today?\u201d. I was not expecting Will's answer, but the challenge excited me:

\u201cI thought I'll just throw you in the deep end and have you write some code.\u201d

What happened next was that I spent two days working on PR #1229 to add a new widget to the Textual code base. At the time of writing, the pull request has not been merged yet. Well, to be honest with you, it hasn't even been reviewed by anyone... But that won't stop me from blogging about some of the things I learned while creating this PR.

"},{"location":"blog/2022/11/22/what-i-learned-from-my-first-non-trivial-pr/#the-placeholder-widget","title":"The placeholder widget","text":"

This PR adds a widget called Placeholder to Textual. As per the documentation, this widget \u201cis meant to have no complex functionality. Use the placeholder widget when studying the layout of your app before having to develop your custom widgets.\u201d

The point of the placeholder widget is that you can focus on building the layout of your app without having to have all of your (custom) widgets ready. The placeholder widget also displays a couple of useful pieces of information to help you work out the layout of your app, namely the ID of the widget itself (or a custom label, if you provide one) and the width and height of the widget.

As an example of usage of the placeholder widget, you can refer to the screenshot at the top of this blog post, which I included below so you don't have to scroll up:

PlaceholderApp Placeholder\u00a0p2\u00a0here! This\u00a0is\u00a0a\u00a0custom\u00a0label\u00a0for\u00a0p1. #p4 #p3#p5Placeholde r Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0 26\u00a0x\u00a06amet,\u00a0consectetur\u00a027\u00a0x\u00a06 adipiscing\u00a0elit.\u00a0Etiam\u00a0 feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0 Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0 consectetur\u00a0adipiscing\u00a0elit.\u00a0Etiam\u00a040\u00a0x\u00a06 feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0accumsan.\u00a0 Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis gravida.\u00a0Phasellus\u00a0id\u00a0eleifend\u00a0 ligula.\u00a0Nullam\u00a0imperdiet\u00a0sem\u00a0tellus, sed\u00a0vehicula\u00a0nisl\u00a0faucibus\u00a0sit\u00a0amet.Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0 Praesent\u00a0iaculis\u00a0tempor\u00a0ultricies.\u00a0\u2586\u2586consectetur\u00a0adipiscing\u00a0elit.\u00a0Etiam\u00a0\u2586\u2586 Sed\u00a0lacinia,\u00a0tellus\u00a0id\u00a0rutrum\u00a0feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0accumsan.\u00a0 lacinia,\u00a0sapien\u00a0sapien\u00a0congue\u00a0Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis

The top left and top right widgets have custom labels. Immediately under the top right placeholder, you can see some placeholders identified as #p3, #p4, and #p5. Those are the IDs of the respective placeholders. Then, rows 2 and 3 contain some placeholders that show their respective size and some placeholders that just contain some text.

"},{"location":"blog/2022/11/22/what-i-learned-from-my-first-non-trivial-pr/#bootstrapping-the-code-for-the-widget","title":"Bootstrapping the code for the widget","text":"

So, how does a code monkey start working on a non-trivial PR within 24 hours of joining a company? The answer is simple: just copy and paste code! But instead of copying and pasting from Stack Overflow, I decided to copy and paste from the internal code base.

My task was to create a new widget, so I thought it would be a good idea to take a look at the implementation of other Textual widgets. For some reason I cannot seem to recall, I decided to take a look at the implementation of the button widget that you can find in _button.py. By looking at how the button widget is implemented, I could immediately learn a few useful things about what I needed to do and some other things about how Textual works.

For example, a widget can have a class attribute called DEFAULT_CSS that specifies the default CSS for that widget. I learned this just from staring at the code for the button widget.

Studying the code base will also reveal the standards that are in place. For example, I learned that for a widget with variants (like the button with its \u201csuccess\u201d and \u201cerror\u201d variants), the widget gets a CSS class with the name of the variant prefixed by a dash. You can learn this by looking at the method Button.watch_variant:

class Button(Static, can_focus=True):\n# ...\ndef watch_variant(self, old_variant: str, variant: str):\nself.remove_class(f\"-{old_variant}\")\nself.add_class(f\"-{variant}\")\n

In short, looking at code and files that are related to the things you need to do is a great way to get information about things you didn't even know you needed.

"},{"location":"blog/2022/11/22/what-i-learned-from-my-first-non-trivial-pr/#handling-the-placeholder-variant","title":"Handling the placeholder variant","text":"

A button widget can have a different variant, which is mostly used by Textual to determine the CSS that should apply to the given button. For the placeholder widget, we want the variant to determine what information the placeholder shows. The original GitHub issue mentions 5 variants for the placeholder:

  • a variant that just shows a label or the placeholder ID;
  • a variant that shows the size and location of the placeholder;
  • a variant that shows the state of the placeholder (does it have focus? is the mouse over it?);
  • a variant that shows the CSS that is applied to the placeholder itself; and
  • a variant that shows some text inside the placeholder.

The variant can be assigned when the placeholder is first instantiated, for example, Placeholder(\"css\") would create a placeholder that shows its own CSS. However, we also want to have an on_click handler that cycles through all the possible variants. I was getting ready to reinvent the wheel when I remembered that the standard module itertools has a lovely tool that does exactly what I needed! Thus, all I needed to do was create a new cycle through the variants each time a placeholder is created and then grab the next variant whenever the placeholder is clicked:

class Placeholder(Static):\ndef __init__(\nself,\nvariant: PlaceholderVariant = \"default\",\n*,\nlabel: str | None = None,\nname: str | None = None,\nid: str | None = None,\nclasses: str | None = None,\n) -> None:\n# ...\nself.variant = self.validate_variant(variant)\n# Set a cycle through the variants with the correct starting point.\nself._variants_cycle = cycle(_VALID_PLACEHOLDER_VARIANTS_ORDERED)\nwhile next(self._variants_cycle) != self.variant:\npass\ndef on_click(self) -> None:\n\"\"\"Click handler to cycle through the placeholder variants.\"\"\"\nself.cycle_variant()\ndef cycle_variant(self) -> None:\n\"\"\"Get the next variant in the cycle.\"\"\"\nself.variant = next(self._variants_cycle)\n

I am just happy that I had the insight to add this little while loop when a placeholder is instantiated:

from itertools import cycle\n# ...\nclass Placeholder(Static):\n# ...\ndef __init__(...):\n# ...\nself._variants_cycle = cycle(_VALID_PLACEHOLDER_VARIANTS_ORDERED)\nwhile next(self._variants_cycle) != self.variant:\npass\n

Can you see what would be wrong if this loop wasn't there?

"},{"location":"blog/2022/11/22/what-i-learned-from-my-first-non-trivial-pr/#updating-the-render-of-the-placeholder-on-variant-change","title":"Updating the render of the placeholder on variant change","text":"

If the variant of the placeholder is supposed to determine what information the placeholder shows, then that information must be updated every time the variant of the placeholder changes. Thankfully, Textual has reactive attributes and watcher methods, so all I needed to do was... Defer the problem to another method:

class Placeholder(Static):\n# ...\nvariant = reactive(\"default\")\n# ...\ndef watch_variant(\nself, old_variant: PlaceholderVariant, variant: PlaceholderVariant\n) -> None:\nself.validate_variant(variant)\nself.remove_class(f\"-{old_variant}\")\nself.add_class(f\"-{variant}\")\nself.call_variant_update()  # <-- let this method do the heavy lifting!\n

Doing this properly required some thinking. Not that the current proposed solution is the best possible, but I did think of worse alternatives while I was thinking how to tackle this. I wasn't entirely sure how I would manage the variant-dependant rendering because I am not a fan of huge conditional statements that look like switch statements:

if variant == \"default\":\n# render the default placeholder\nelif variant == \"size\":\n# render the placeholder with its size\nelif variant == \"state\":\n# render the state of the placeholder\nelif variant == \"css\":\n# render the placeholder with its CSS rules\nelif variant == \"text\":\n# render the placeholder with some text inside\n

However, I am a fan of using the built-in getattr and I thought of creating a rendering method for each different variant. Then, all I needed to do was make sure the variant is part of the name of the method so that I can programmatically determine the name of the method that I need to call. This means that the method Placeholder.call_variant_update is just this:

class Placeholder(Static):\n# ...\ndef call_variant_update(self) -> None:\n\"\"\"Calls the appropriate method to update the render of the placeholder.\"\"\"\nupdate_variant_method = getattr(self, f\"_update_{self.variant}_variant\")\nupdate_variant_method()\n

If self.variant is, say, \"size\", then update_variant_method refers to _update_size_variant:

class Placeholder(Static):\n# ...\ndef _update_size_variant(self) -> None:\n\"\"\"Update the placeholder with the size of the placeholder.\"\"\"\nwidth, height = self.size\nself._placeholder_label.update(f\"[b]{width} x {height}[/b]\")\n

This variant \"size\" also interacts with resizing events, so we have to watch out for those:

class Placeholder(Static):\n# ...\ndef on_resize(self, event: events.Resize) -> None:\n\"\"\"Update the placeholder \"size\" variant with the new placeholder size.\"\"\"\nif self.variant == \"size\":\nself._update_size_variant()\n
"},{"location":"blog/2022/11/22/what-i-learned-from-my-first-non-trivial-pr/#deleting-code-is-a-hurtful-blessing","title":"Deleting code is a (hurtful) blessing","text":"

To conclude this blog post, let me muse about the fact that the original issue mentioned five placeholder variants and that my PR only includes two and a half.

After careful consideration and after coming up with the getattr mechanism to update the display of the placeholder according to the active variant, I started showing the \u201cfinal\u201d product to Will and my other colleagues. Eventually, we ended up getting rid of the variant for CSS and the variant that shows the placeholder state. This means that I had to delete part of my code even before it saw the light of day.

On the one hand, deleting those chunks of code made me a bit sad. After all, I had spent quite some time thinking about how to best implement that functionality! But then, it was time to write documentation and tests, and I verified that the best code is the code that you don't even write! The code you don't write is guaranteed to have zero bugs and it also does not need any documentation whatsoever!

So, it was a shame that some lines of code I poured my heart and keyboard into did not get merged into the Textual code base. On the other hand, I am quite grateful that I won't have to fix the bugs that will certainly reveal themselves in a couple of weeks or months from now. Heck, the code hasn't been merged yet and just by writing this blog post I noticed a couple of tweaks that were missing!

"},{"location":"blog/2023/07/29/pull-requests-are-cake-or-puppies/","title":"Pull Requests are cake or puppies","text":"

Broadly speaking, there are two types of contributions you can make to an Open Source project.

The first type is typically a bug fix, but could also be a documentation update, linting fix, or other change which doesn't impact core functionality. Such a contribution is like cake. It's a simple, delicious, gift to the project.

The second type of contribution often comes in the form of a new feature. This contribution likely represents a greater investment of time and effort than a bug fix. It is still a gift to the project, but this contribution is not cake.

A feature PR has far more in common with a puppy. The maintainer(s) may really like the feature but hesitate to merge all the same. They may even reject the contribution entirely. This is because a feature PR requires an ongoing burden to maintain. In the same way that a puppy needs food and walkies, a new feature will require updates and fixes long after the original contribution. Even if it is an amazing feature, the maintainer may not want to commit to that ongoing work.

The chances of a feature being merged can depend on the maturity of the project. At the beginning of a project, a maintainer may be delighted with a new feature contribution. After all, having others join you to build something is the joy of Open Source. And yet when a project gets more mature there may be a growing resistance to adding new features, and a greater risk that a feature PR is rejected or sits unappreciated in the PR queue.

So how should a contributor avoid this? If there is any doubt, it's best to propose the feature to the maintainers before undertaking the work. In all likelihood they will be happy for your contribution, just be prepared for them to say \"thanks but no thanks\". Don't take it as a rejection of your gift: it's just that the maintainer can't commit to taking on a puppy.

There are other ways to contribute code to a project that don't require the code to be merged in to the core. You could publish your change as a third party library. Take it from me: maintainers love it when their project spawns an ecosystem. You could also blog about how you solved your problem without an update to the core project. Having a resource that can be googled for, or a maintainer can direct people to, can be a huge help.

What prompted me to think about this is that my two main projects, Rich and Textual, are at quite different stages in their lifetime. Rich is relatively mature, and I'm unlikely to accept a puppy. If you can achieve what you need without adding to the core library, I am probably going to decline a new feature. Textual is younger and still accepting puppies \u2014 in addition to stick insects, gerbils, capybaras and giraffes.

Tip

If you are maintainer, and you do have to close a feature PR, feel free to link to this post.

Join us on the Discord Server if you want to discuss puppies and other creatures.

"},{"location":"blog/2023/02/15/textual-0110-adds-a-beautiful-markdown-widget/","title":"Textual 0.11.0 adds a beautiful Markdown widget","text":"

We released Textual 0.10.0 25 days ago, which is a little longer than our usual release cycle. What have we been up to?

The headline feature of this release is the enhanced Markdown support. Here's a screenshot of an example:

MarkdownApp \u258bHeader\u00a0level\u00a06\u00a0content. \u25bc\u00a0\u2160\u00a0Textual\u00a0Markdown\u00a0Browser\u00a0-\u00a0Demo\u258b \u251c\u2500\u2500\u00a0\u25bc\u00a0\u2161\u00a0Headers\u258b\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2502\u00a0\u00a0\u00a0\u2514\u2500\u2500\u00a0\u25bc\u00a0\u2162\u00a0This\u00a0is\u00a0H3\u258b\u258e\u258b \u2502\u00a0\u00a0\u00a0\u2514\u2500\u2500\u00a0\u25bc\u00a0\u2163\u00a0This\u00a0is\u00a0H4\u258b\u258eTypography\u258b \u2502\u00a0\u00a0\u00a0\u2514\u2500\u2500\u00a0\u25bc\u00a0\u2164\u00a0This\u00a0is\u00a0H5\u258b\u258e\u258b \u2502\u00a0\u00a0\u00a0\u2514\u2500\u2500\u00a0\u2165\u00a0This\u00a0is\u00a0H6\u258b\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u251c\u2500\u2500\u00a0\u25bc\u00a0\u2161\u00a0Typography\u258bThe\u00a0usual\u00a0Markdown\u00a0typography\u00a0is\u00a0supported.\u00a0The\u00a0exact\u00a0output\u00a0depends\u00a0on\u00a0 \u2502\u00a0\u00a0\u00a0\u2523\u2501\u2501\u00a0\u2162\u00a0Emphasis\u258byour\u00a0terminal,\u00a0although\u00a0most\u00a0are\u00a0fairly\u00a0consistent.\u2581\u2581 \u2502\u00a0\u00a0\u00a0\u2523\u2501\u2501\u00a0\u2162\u00a0Strong\u258b \u2502\u00a0\u00a0\u00a0\u2523\u2501\u2501\u00a0\u2162\u00a0Strikethrough\u258bEmphasis \u2502\u00a0\u00a0\u00a0\u2517\u2501\u2501\u00a0\u2162\u00a0Inline\u00a0code\u258b\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u251c\u2500\u2500\u00a0\u2161\u00a0Fences\u258bEmphasis\u00a0is\u00a0rendered\u00a0with\u00a0*asterisks*,\u00a0and\u00a0looks\u00a0like\u00a0this; \u251c\u2500\u2500\u00a0\u2161\u00a0Quote\u258b \u2514\u2500\u2500\u00a0\u2161\u00a0Tables\u258bStrong \u258b\u2594\u2594\u2594\u2594\u2594\u2594 \u258bUse\u00a0two\u00a0asterisks\u00a0to\u00a0indicate\u00a0strong\u00a0which\u00a0renders\u00a0in\u00a0bold,\u00a0e.g.\u00a0 \u258b**strong**\u00a0render\u00a0strong. \u258b \u258bStrikethrough \u258b\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258bTwo\u00a0tildes\u00a0indicates\u00a0strikethrough,\u00a0e.g.\u00a0~~cross\u00a0out~~\u00a0render\u00a0cross\u00a0out. \u258b\u2582\u2582 \u258bInline\u00a0code \u258b\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258bInline\u00a0code\u00a0is\u00a0indicated\u00a0by\u00a0backticks.\u00a0e.g.\u00a0import\u00a0this. \u258b \u258b\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258b\u258e\u258b \u258b\u258eFences\u258b \u258b\u258e\u258b \u258b\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258bFenced\u00a0code\u00a0blocks\u00a0are\u00a0introduced\u00a0with\u00a0three\u00a0back-ticks\u00a0and\u00a0the\u00a0optional\u00a0 \u258bparser.\u00a0Here\u00a0we\u00a0are\u00a0rendering\u00a0the\u00a0code\u00a0in\u00a0a\u00a0sub-widget\u00a0with\u00a0syntax\u00a0 \u258bhighlighting\u00a0and\u00a0indent\u00a0guides. \u258b \u258bIn\u00a0the\u00a0future\u00a0I\u00a0think\u00a0we\u00a0could\u00a0add\u00a0controls\u00a0to\u00a0export\u00a0the\u00a0code,\u00a0copy\u00a0to\u00a0 \u258bthe\u00a0clipboard.\u00a0Heck,\u00a0even\u00a0run\u00a0it\u00a0and\u00a0show\u00a0the\u00a0output? \u258b \u258b \u258b@lru_cache(maxsize=1024) \u258bdefsplit(self,cut_x:int,cut_y:int)->tuple[Region,Region,Regi \u258b\u2502\u00a0\u00a0\u00a0\"\"\"Split\u00a0a\u00a0region\u00a0in\u00a0to\u00a04\u00a0from\u00a0given\u00a0x\u00a0and\u00a0y\u00a0offsets\u00a0(cuts). \u00a0T\u00a0\u00a0TOC\u00a0\u00a0B\u00a0\u00a0Back\u00a0\u00a0F\u00a0\u00a0Forward\u00a0

Tip

You can generate these SVG screenshots for your app with textual run my_app.py --screenshot 5 which will export a screenshot after 5 seconds.

There are actually 2 new widgets: Markdown for a simple Markdown document, and MarkdownViewer which adds browser-like navigation and a table of contents.

Textual has had support for Markdown since day one by embedding a Rich Markdown object -- which still gives decent results! This new widget adds dynamic controls such as scrollable code fences and tables, in addition to working links.

In future releases we plan on adding more Markdown extensions, and the ability to easily embed custom widgets within the document. I'm sure there are plenty of interesting applications that could be powered by dynamically generated Markdown documents.

"},{"location":"blog/2023/02/15/textual-0110-adds-a-beautiful-markdown-widget/#datatable-improvements","title":"DataTable improvements","text":"

There has been a lot of work on the DataTable API. We've added the ability to sort the data, which required that we introduce the concept of row and column keys. You can now reference rows / columns / cells by their coordinate or by row / column key.

Additionally there are new update_cell and update_cell_at methods to update cells after the data has been populated. Future releases will have more methods to manipulate table data, which will make it a very general purpose (and powerful) widget.

"},{"location":"blog/2023/02/15/textual-0110-adds-a-beautiful-markdown-widget/#tree-control","title":"Tree control","text":"

The Tree widget has grown a few methods to programmatically expand, collapse and toggle tree nodes.

"},{"location":"blog/2023/02/15/textual-0110-adds-a-beautiful-markdown-widget/#breaking-changes","title":"Breaking changes","text":"

There are a few breaking changes in this release. These are mostly naming and import related, which should be easy to fix if you are affected. Here's a few notable examples:

  • Checkbox has been renamed to Switch. This is because we plan to introduce complimentary Checkbox and RadioButton widgets in a future release, but we loved the look of Switches too much to drop them.
  • We've dropped the emit and emit_no_wait methods. These methods posted message to the parent widget, but we found that made it problematic to subclass widgets. In almost all situations you want to replace these with self.post_message (or self.post_message_no_wait).

Be sure to check the CHANGELOG for the full details on potential breaking changes.

"},{"location":"blog/2023/02/15/textual-0110-adds-a-beautiful-markdown-widget/#join-us","title":"Join us!","text":"

We're having fun on our Discord server. Join us there to talk to Textualize developers and share ideas.

"},{"location":"blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/","title":"Textual 0.12.0 adds syntactical sugar and batch updates","text":"

It's been just 9 days since the previous release, but we have a few interesting enhancements to the Textual API to talk about.

"},{"location":"blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/#better-compose","title":"Better compose","text":"

We've added a little syntactical sugar to Textual's compose methods, which aids both readability and editability (that might not be a word).

First, let's look at the old way of building compose methods. This snippet is taken from the textual colors command.

for color_name in ColorSystem.COLOR_NAMES:\nitems: list[Widget] = [ColorLabel(f'\"{color_name}\"')]\nfor level in LEVELS:\ncolor = f\"{color_name}-{level}\" if level else color_name\nitem = ColorItem(\nColorBar(f\"${color}\", classes=\"text label\"),\nColorBar(\"$text-muted\", classes=\"muted\"),\nColorBar(\"$text-disabled\", classes=\"disabled\"),\nclasses=color,\n)\nitems.append(item)\nyield ColorGroup(*items, id=f\"group-{color_name}\")\n

This code composes the following color swatches:

ColorsApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 primary \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b\u2581\u2581 secondary\u258e\"primary\"\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b background\u258e$primary-darken-3$text-muted$text-disabled\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b primary-background\u258e$primary-darken-2$text-muted$text-disabled\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b secondary-background\u258e$primary-darken-1$text-muted$text-disabled\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b surface\u258e$primary$text-muted$text-disabled\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b panel\u258e$primary-lighten-1$text-muted$text-disabled\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b boost\u258e$primary-lighten-2$text-muted$text-disabled\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b warning\u258e$primary-lighten-3$text-muted$text-disabled\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b error\u258e\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 success \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b accent\u258e\"secondary\"\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258b \u258e\u258b \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0

Tip

You can see this by running textual colors from the command line.

The old way was not all that bad, but it did make it hard to see the structure of your app at-a-glance, and editing compose methods always felt a little laborious.

Here's the new syntax, which uses context managers to add children to containers:

for color_name in ColorSystem.COLOR_NAMES:\nwith ColorGroup(id=f\"group-{color_name}\"):\nyield Label(f'\"{color_name}\"')\nfor level in LEVELS:\ncolor = f\"{color_name}-{level}\" if level else color_name\nwith ColorItem(classes=color):\nyield ColorBar(f\"${color}\", classes=\"text label\")\nyield ColorBar(\"$text-muted\", classes=\"muted\")\nyield ColorBar(\"$text-disabled\", classes=\"disabled\")\n

The context manager approach generally results in fewer lines of code, and presents attributes on the same line as containers themselves. Additionally, adding widgets to a container can be as simple is indenting them.

You can still construct widgets and containers with positional arguments, but this new syntax is preferred. It's not documented yet, but you can start using it now. We will be updating our examples in the next few weeks.

"},{"location":"blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/#batch-updates","title":"Batch updates","text":"

Textual is smart about performing updates to the screen. When you make a change that might repaint the screen, those changes don't happen immediately. Textual makes a note of them, and repaints the screen a short time later (around a 1/60th of a second). Multiple updates are combined so that Textual does less work overall, and there is none of the flicker you might get with multiple repaints.

Although this works very well, it is possible to introduce a little flicker if you make changes across multiple widgets. And especially if you add or remove many widgets at once. To combat this we have added a batch_update context manager which tells Textual to disable screen updates until the end of the with block.

The new Markdown widget uses this context manager when it updates its content. Here's the code:

with self.app.batch_update():\nawait self.query(\"MarkdownBlock\").remove()\nawait self.mount_all(output)\n

Without the batch update there are a few frames where the old markdown blocks are removed and the new blocks are added (which would be perceived as a brief flicker). With the update, the update appears instant.

"},{"location":"blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/#disabled-widgets","title":"Disabled widgets","text":"

A few widgets (such as Button) had a disabled attribute which would fade the widget a little and make it unselectable. We've extended this to all widgets. Although it is particularly applicable to input controls, anything may be disabled. Disabling a container makes its children disabled, so you could use this for disabling a form, for example.

Tip

Disabled widgets may be styled with the :disabled CSS pseudo-selector.

"},{"location":"blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/#preventing-messages","title":"Preventing messages","text":"

Also in this release is another context manager, which will disable specified Message types. This doesn't come up as a requirement very often, but it can be very useful when it does. This one is documented, see Preventing events for details.

"},{"location":"blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/#full-changelog","title":"Full changelog","text":"

As always see the release page for additional changes and bug fixes.

"},{"location":"blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/#join-us","title":"Join us!","text":"

We're having fun on our Discord server. Join us there to talk to Textualize developers and share ideas.

"},{"location":"blog/2023/03/09/textual-0140-shakes-up-posting-messages/","title":"Textual 0.14.0 shakes up posting messages","text":"

Textual version 0.14.0 has landed just a week after 0.13.0.

Note

We like fast releases for Textual. Fast releases means quicker feedback, which means better code.

What's new?

We did a little shake-up of posting messages which will simplify building widgets. But this does mean a few breaking changes.

There are two methods in Textual to post messages: post_message and post_message_no_wait. The former was asynchronous (you needed to await it), and the latter was a regular method call. These two methods have been replaced with a single post_message method.

To upgrade your project to Textual 0.14.0, you will need to do the following:

  • Remove await keywords from any calls to post_message.
  • Replace any calls to post_message_no_wait with post_message.

Additionally, we've simplified constructing messages classes. Previously all messages required a sender argument, which had to be manually set. This was a clear violation of our \"no boilerplate\" policy, and has been dropped. There is still a sender property on messages / events, but it is set automatically.

So prior to 0.14.0 you might have posted messages like the following:

await self.post_message(self.Changed(self, item=self.item))\n

You can now replace it with this simpler function call:

self.post_message(self.Change(item=self.item))\n

This also means that you will need to drop the sender from any custom messages you have created.

If this was code pre-0.14.0:

class MyWidget(Widget):\nclass Changed(Message):\n\"\"\"My widget change event.\"\"\"\ndef __init__(self, sender:MessageTarget, item_index:int) -> None:\nself.item_index = item_index\nsuper().__init__(sender)\n

You would need to make the following change (dropping sender).

class MyWidget(Widget):\nclass Changed(Message):\n\"\"\"My widget change event.\"\"\"\ndef __init__(self, item_index:int) -> None:\nself.item_index = item_index\nsuper().__init__()\n

If you have any problems upgrading, join our Discord server, we would be happy to help.

See the release notes for the full details on this update.

"},{"location":"blog/2023/03/13/textual-0150-adds-a-tabs-widget/","title":"Textual 0.15.0 adds a tabs widget","text":"

We've just pushed Textual 0.15.0, only 4 days after the previous version. That's a little faster than our typical release cadence of 1 to 2 weeks.

What's new in this release?

The highlight of this release is a new Tabs widget to display tabs which can be navigated much like tabs in a browser. Here's a screenshot:

TabsApp Paul\u00a0AtreidiesDuke\u00a0Leto\u00a0AtreidesLady\u00a0JessicaGurney\u00a0Halleck \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258aLady\u00a0Jessica\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u00a0A\u00a0\u00a0Add\u00a0tab\u00a0\u00a0R\u00a0\u00a0Remove\u00a0active\u00a0tab\u00a0\u00a0C\u00a0\u00a0Clear\u00a0tabs\u00a0

In a future release, this will be combined with the ContentSwitcher widget to create a traditional tabbed dialog. Although Tabs is still useful as a standalone widgets.

Tip

I like to tweet progress with widgets on Twitter. See the #textualtabs hashtag which documents progress on this widget.

Also in this release is a new LoadingIndicator widget to display a simple animation while waiting for data. Here's a screenshot:

LoadingApp \u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf

As always, see the release notes for the full details on this update.

If you want to talk about these widgets, or anything else Textual related, join us on our Discord server.

"},{"location":"blog/2023/03/22/textual-0160-adds-tabbedcontent-and-border-titles/","title":"Textual 0.16.0 adds TabbedContent and border titles","text":"

Textual 0.16.0 lands 9 days after the previous release. We have some new features to show you.

There are two highlights in this release. In no particular order, the first is TabbedContent which uses a row of tabs to navigate content. You will have likely encountered this UI in the desktop and web. I think in Windows they are known as \"Tabbed Dialogs\".

This widget combines existing Tabs and ContentSwitcher widgets and adds an expressive interface for composing. Here's a trivial example to use content tabs to navigate a set of three markdown documents:

def compose(self) -> ComposeResult:\nwith TabbedContent(\"Leto\", \"Jessica\", \"Paul\"):\nyield Markdown(LETO)\nyield Markdown(JESSICA)\nyield Markdown(PAUL)\n

Here's an example of the UI you can create with this widget (note the nesting)!

TabbedApp LetoJessicaPaul \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eLady\u00a0Jessica\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Bene\u00a0Gesserit\u00a0and\u00a0concubine\u00a0of\u00a0Leto,\u00a0and\u00a0mother\u00a0of\u00a0Paul\u00a0and\u00a0Alia. PaulAlia \u2501\u2578\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 First\u00a0child \u00a0L\u00a0\u00a0Leto\u00a0\u00a0J\u00a0\u00a0Jessica\u00a0\u00a0P\u00a0\u00a0Paul\u00a0

"},{"location":"blog/2023/03/22/textual-0160-adds-tabbedcontent-and-border-titles/#border-titles","title":"Border titles","text":"

The second highlight is a frequently requested feature (FRF?). Widgets now have the two new string properties, border_title and border_subtitle, which will be displayed within the widget's border.

You can set the alignment of these titles via border-title-align and border-subtitle-align. Titles may contain Console Markup, so you can add additional color and style to the labels.

Here's an example of a widget with a title:

BorderApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 ascii \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 none \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550double\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557 hidden\u2551\u2551 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2551\u2551 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2551I\u00a0must\u00a0not\u00a0fear.\u2551 blank\u2551Fear\u00a0is\u00a0the\u00a0mind-killer.\u2551 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2551Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2551 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2551I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2551 round\u2551I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2551 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2551And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0\u2551 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2551path.\u2551 solid\u2586\u2586\u2551Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0\u2551 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2551remain.\u2551 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2551\u2551 double\u2551\u2551 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 dashed \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 heavy \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

BTW the above is a command you can run to see the various border styles you can apply to widgets.

textual borders\n
"},{"location":"blog/2023/03/22/textual-0160-adds-tabbedcontent-and-border-titles/#container-changes","title":"Container changes","text":"

Breaking change

If you have an app that uses any container classes, you should read this section.

We've made a change to containers in this release. Previously all containers had auto scrollbars, which means that any container would scroll if its children didn't fit. With nested layouts, it could be tricky to understand exactly which containers were scrolling. In 0.16.0 we split containers in to scrolling and non-scrolling versions. So Horizontal will now not scroll by default, but HorizontalScroll will have automatic scrollbars.

"},{"location":"blog/2023/03/22/textual-0160-adds-tabbedcontent-and-border-titles/#what-else","title":"What else?","text":"

As always, see the release notes for the full details on this update.

If you want to talk about this update or anything else Textual related, join us on our Discord server.

"},{"location":"blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/","title":"Textual 0.17.0 adds translucent screens and Option List","text":"

This is a surprisingly large release, given it has been just 7 days since the last version (and we were down a developer for most of that time).

What's new in this release?

There are two new notable features I want to cover. The first is a compositor effect.

"},{"location":"blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/#translucent-screens","title":"Translucent screens","text":"

Textual has a concept of \"screens\" which you can think of as independent UI modes, each with their own user interface and logic. The App class keeps a stack of these screens so you can switch to a new screen and later return to the previous screen.

Screens

See the guide to learn more about the screens API.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN1cXOtT20hcdTAwMTL/nr+C4r7sVcWz0z3vrbq6XHUwMDAyXHUwMDEyXHUwMDEyQnhsyObB3VZK2MLW4ddaMsZs5X+/XHUwMDFlhSDJQorBxnHiXHUwMDBmXHUwMDE4a+RRa+bX3b9+yH8/2djYTKbDcPO3jc3wqlx1MDAxOXSj1iiYbD71xy/DUVx1MDAxY1xy+jSE6ed4MFx1MDAxZTXTMztJMox/+/XXXjC6XGKTYTdohuwyisdBN07GrWjAmoPer1FcdTAwMTL24n/7v4dBL/zXcNBrJSOWXaRcdTAwMTG2omQw+nKtsFx1MDAxYvbCflx1MDAxMtPs/6HPXHUwMDFiXHUwMDFif6d/c9K1oqA36LfS09OBnHhazFx1MDAxZT1cdTAwMWP0U1FBXHUwMDBiLZW28vaEKH5GXHUwMDE3S8JcdTAwMTaNnpPAYTbiXHUwMDBmbcrRNNFcdTAwMDae897Fx119cmhcdTAwMGZOxyfZVc+jbvckmXZTmeJcdTAwMDHdSjZcdTAwMTYno8FF+D5qJVx1MDAxZH/pmeNV31x1MDAxYVxyxu1OP4zjwndcdTAwMDbDoFx1MDAxOSVTOqb47cGg306nyI5cXNGnXHUwMDA2cs6M0Vx1MDAxNqTiXHUwMDEy6G7V7fiXXHRcdTAwMDQz1lx1MDAxOFx1MDAwNUJcdTAwMWEhXHUwMDA1qFx1MDAxOcl2XHUwMDA2XdpcdTAwMDeS7Fx1MDAxZjx9ZbKdXHUwMDA1zYs2XHTYb2XngFxugrPz7JzJzf1Kp5i0Uphs+k5cdTAwMTi1O4nfIauZNcBdfjRcdTAwMGXTTXCgpJNaZlvkrzjca6Vg+HN2XHUwMDE1O8FoeLNam7H/kJPWXHUwMDBi+nxcdTAwMTZJeTTl9lm8grPdXHUwMDEwYKe1v/3X85NcdTAwMDP5+2CrfztXXHUwMDAxesFoNJhs3o58vvkvXHUwMDEzbTxsXHUwMDA1X1x1MDAxMFx1MDAwNVpLa43TXHUwMDEyTVx1MDAwNspu1L+gwf64282OXHKaXHUwMDE3XHUwMDE5XGLTo5+f3lx1MDAxYvp0mSroo+OO0KD03NBcdTAwMGbHU3ux39vnfHz+ctLeiyb6hfue0Fx1MDAwN/5N7IPTTFx1MDAxOSNRc1x1MDAwZVx1MDAwMoyyXHUwMDA17EuBXGalQYKeddo4vlx1MDAxOPbPgzPO1Vx1MDAxMrGPQipwlq9cdTAwMTb7vd45n2zx5NlhNFxmwz9eXHUwMDFlbb86iJeEfVx1MDAwYlxccG6Whf0kvEruXHUwMDAyvkVdXHUwMDA1fFx1MDAxMNZx5NLh3Mh/d941l1fDy5fT3taHwfjj8PiF2F1v5CMqprRBXHUwMDA0dEZ6XHUwMDBiWlx1MDAwML7lwMhcdTAwMDRJclxi1iHkrMBDcG+c4udYxj1wW1x1MDAwNryBWZhrgdL7pp/IxDtcdTAwMGJK2PvAPEPToJ+cRNepjbaFo7tBL+pOXHUwMDBikEjxT1x1MDAwMp40R2HY34D/9n/pRK1W2P9nfsfikK7vJ9TFb251o7bXls1ueF5UoyRcIlx1MDAxZXY7nFxmcmvcJElcdTAwMDKabrTXmr2jwShqR/2g+7Zaqlpt/rLMd6gzUVx1MDAxM5w9nNNnIHojxPz6XFy/8/fQZ5zF5uPps3HMSFx1MDAwMGm4pXdbVGfjJFx1MDAwM81cdTAwMWRcdTAwMWGU6EjjXHUwMDFmRZ1cdTAwMWQyrogwS2MsR9TuXHUwMDBl5XZMIzk6KVFcdTAwMDHXOlx1MDAwM/BXlyaFv4VcdTAwMDeoeipkjao/ijLGSTBKtqN+K+q3aTCzXCJfQ5K9OVx1MDAxY0Sqvs2xl5IzhVxcS8FpI5UgL1x1MDAwNLmT2sHQLyFcdTAwMDMgTqLJZKNVTtibXHUwMDEzPt9cblx1MDAxNfZb31x1MDAxNqk+UMmJ1OBcZml5nPV7pjhqilx1MDAxM0pCSaZcdTAwMDVIXHUwMDA3nITiTlmnSlJ1gzjZXHUwMDE59HpRQmt/PIj6yexcdTAwMWGni7nldbxcdTAwMTNcdTAwMDYl40F3lVx1MDAxZps1XHUwMDA2Qz9j0aZn/21k2pJ+uP3/z6d3nt2ohHI6WkJxNt+T/PtcdTAwMDNcdTAwMTi5sJWGXGYtqVx1MDAwNlx1MDAxMZdM8b/JyM9G2NpcdTAwMGKuw/1nW1x1MDAwN89f9LWNIVhvXlwinGOOTFx1MDAxNVxiNN5y68xcdTAwMTJ8XHRGLeNcdTAwMDLAOeK+ZCaEmJHsIcGo1suj5ESVkKJcYs5cdTAwMWaBrNRYMECFuIqIkYLtSkeLZLhcdTAwMDVwNb+jPdzpXHUwMDFjXGZfXHUwMDFjvdtcdTAwMWRcdTAwMWNfj9xhcjQ+XHUwMDFmivVcdTAwMDao5IJcdJ9cdFGWdFWqYrJEXG7NuOPGXHUwMDEyQqVcdTAwMDZcXFxmnsuOXHUwMDE3SVx1MDAxZaE4mTS7fHDWMenXZ8rufYyvOsPx+MNWdPomuVx1MDAxOMllXHUwMDA1jIQ3yHG7x4O+XHUwMDE1qlxu+k6AUkS65jfN76eHb9o9t3+0r/76NH32abtzKI+WivxWXHUwMDEwd8IlQ98xolx1MDAxZYJcdTAwMTONXHUwMDA0YpGuXHUwMDAwfaGQXHUwMDExKZFcdTAwMDY5cDBcXC9GMi02XHUwMDFkhGqZ6CdcdTAwMTNpSTS+4nSJXHLDl+PX19fds1P96VxcJJGYnvL50P+0bt5Y7U+v4+3jyejgdO/Vofj0x9SeLWHe8+H7ydvG5Ni+v+7Fp1x1MDAxN83wI3ZcdTAwMGaWMC9cdTAwMWab//VO3mLyprl9XHUwMDE1NT/uvlx1MDAxMc1oWfE0J+S5pTnAqrSRrktcdTAwMWKRWjiKvOzcNuAsnuKOONpcdTAwMDXV3u6Mmq33h+r6dL3DTGlcdTAwMWSzSkvNOZleNZMuXHUwMDA1QaNOcrKERJ3JXHUwMDE3zlxudj9cdTAwMTNA6npcdTAwMTbewc1ErqZxq/myrO9CWqIrhj+Ct6tBolx1MDAwMan1fZCYbXiW2Vx1MDAxMVx1MDAxNOWlgYfnwDZcdTAwMTdJXHUwMDE38jzZVb7meYLhkFxyXHUwMDA3w09xmln55e4sj9CF7z12lqckU63qVeZ4dHVkROxXXHUwMDEz71Zyft2rN57L0L1H8L8gmERUXHUwMDE0ZiOZOzFTq7CGIVx1MDAxN9pcYqPJXHUwMDE3w2Ip2yrd40RvXHUwMDAxkVgwXHUwMDAx06E1QpZ1XHUwMDExuPGCmpRvXHUwMDAyXHUwMDE5XHUwMDA0V9ZNQjUpTC7JvopEXHUwMDBmcFqcx0z01NO6jUJWxVEsXHUwMDBmjmyoRE2rmUsx3CRVXHUwMDE0U1x1MDAxNFx1MDAwMyuOdFx1MDAwMtpvZnqKt/EjZVtqQJWOl/GUTfkk/35vo2JyRYVZo0JwXHUwMDAx8iD3yFx1MDAxYtczp/U0Ko5LZi1ZXGZDsaySZtaoXGJmXHUwMDA0V4pWnlxmXHUwMDBizEZcdTAwMWLLMipoOWghfSmfLpIrRuVsimWSglx1MDAwZetcdTAwMWNcdTAwMTiCXG6WykSgjZXOc5PV2lx1MDAxNKCYXCJbte9oU4DRXHUwMDBl0PIopykqJlx1MDAxZVJOXHUwMDFlXHUwMDAzZ8QmrNTkQoThXFyan9SmVEPKv1x1MDAxYWU0LcuiXHUwMDE08l+zXHUwMDE5XFyurZS+nDm3SalPnaynSdHKMCO55lx1MDAxMiVIk+tk8d/XXHUwMDAyXHUwMDE4+X3iMd6x4YItXHUwMDE1VSaFdMFcdTAwMTkjyWhJWnXSiez+b02KU1xmpXbKcsFccqrcrtxYXHUwMDE0dPRdhfpcdTAwMDFcdTAwMDHEQiSFK5fJ8nCDMqt8P4FaN6r3NVx1MDAxZC5t6T3Vuq5XSldTXHUwMDA1Tv5cdTAwMTM06vmpgn6m8FX0cnLZfvfhQrevT49+j79rn+C31ZrIMzBcbj6QTCatr5lpXHUwMDE5UUAkTSluXHUwMDFkUSaXL8ivR2VGeTOvVptcZlhdJ59cdTAwMTbVXlx1MDAwN4Rwvk1s/ui4cbU/tn+1wsvOycdLXHUwMDExTl87PNhZe3Rq5uNcdTAwMDdB2DMobLFwSG6XkS9cIuRcbq2EQbdcdTAwMTA6l16YcUIgUTfzgHB4kdR0I57s7CB2I9lcdTAwMTZcdTAwMWZa4z+O48nFslKyVlpuga9cdTAwMDD7NuegS1xy3IAkjJyfcMX93dFe7+L5azHF8L1otTvDg9Z6Q79cdTAwMDHWMrCKmKXTzmlcdTAwMGVcdTAwMDXoXHUwMDBiKYlcZiuL3KLv6l1cYvlfyjLLLElcdTAwMTJHXHUwMDA0XHUwMDEy7CcpypxtPT9/XHUwMDFmbPHD9rv9t1x1MDAwN83rQZDE46X1xqK0uDSNqlxmYWpcbp2gfHVbajN/W3h92Wd9I1x1MDAxOFxupYVcdTAwMDRSXHUwMDFiXHUwMDAxekafiOiQWfHxi6b9WKzOWVx1MDAxZMBcdTAwMThFXHUwMDExlKPolYiLJZ9VVizincxnZZRcIq/mfLmjpF5cdTAwMTTqy2Jss5pcdTAwMTCG1P1BNZBl50Q448AlxXBKXHUwMDE5jeA4N3d2r/neNk3RKsWlZJBuTvjZklwijWpQfVx1MDAxOS7hKZvxSf79vnVTqWD26C071cjR3ec5k6vXoVx1MDAxY530XHUwMDFiz/u6od1+6/CV6thcboPSXHSanfEoXFxcdTAwMDNcdTAwMWZN+GJcdTAwMDZ89yTB0T/jUEy0XHUwMDFhJ1x1MDAxOHF0X1x1MDAwZVx1MDAxMMpavlD1Jlx1MDAxOVx1MDAwNf14XHUwMDE4jEhd7jAsucxpTdO9XHUwMDE0qJw1YsWM9DGfLfHZXHUwMDAy41bedI9r2XSPizfdc1fdXHJBtsOSf7zHk5P1O38vtV5dP0SDXHUwMDAySlx1MDAwNspYIynYXHUwMDE3zuqZxnuLjLyhptPQmVxccWXpWlxyilx0XHUwMDAwKYiekW1x7q5cdTAwMTJcblx1MDAwMpM+XHLCLYVMoPJcdTAwMWRaN3TBu1x1MDAwN4fwkCzJXCJ0gTyzclx1MDAwZtHLOelCvcvYKDa7k+8z5CONTOvo5bIscEaLJITl3JdR9Ndi5D1cdTAwMWLw61x1MDAxZpcsUFx1MDAxOFx1MDAxMCCNpZ3zXHUwMDE5XHUwMDAyJY0uyWRcdTAwMTjSgCR648jGXHUwMDE5xJJMP1x1MDAxMk+pXHUwMDA2s381yjheXHUwMDE2TVx1MDAxMZWJXHUwMDA0XHUwMDA0n2+mUHV+nlwiPlxcyNa1fHn54tnhm9Z04sK+qirdrFx1MDAwZk9cdTAwMDHBmSFoI7FD32ePRYOGqH1cdTAwMDXRKCAuY6VSXHUwMDBipVx1MDAxM75BVO5o8ypcdTAwMTNcdTAwMTWyXHUwMDFjUlx0IdWPw1Se1s37mFx1MDAxOVx1MDAwNLK0Wq+eXHUwMDAxXHTiXHUwMDFhl1FcdTAwMWOddcN1okBcdTAwMDWxXHUwMDFlxoFcdTAwMTRWXHUwMDA2NuAsoZK4/vxN4fVbv65cdTAwMTTIP/IglFTeLKBRYqYtXHUwMDFjkFx0Ylx1MDAxNVx1MDAxNoUherTYXHUwMDAzO7X2XHUwMDAyXHUwMDFkOUokj01Ow3IpsyvdWlx1MDAwZu3du/TJXHUwMDAwXHS0MVjOl/jSXHUwMDAxRaSr7iF5sFrOSYDqfVGRXHUwMDAwgdCeXHUwMDA3Ulx1MDAxNFxuXHUwMDE2yeHlclx1MDAwNF9cdTAwMTmQZNJvJaBcdTAwMTJcdTAwMWOMeegziPW59qJUXFxcbmPJISljtNEu94NcdTAwMWa3YllmNHFcdTAwMDPaWue4MkKWpPqRSFAlnP2rXHUwMDA05CUxIFGdqCFhUEtcdTAwMGbXue3ZyfXlKzWNj6ZHL+LB5OPWZft479O6MyCK1ohcdTAwMDGh4MSCjK9ZXHUwMDE0XHUwMDEzNUJcdTAwMThGXHUwMDExgnRElFxibvlfUPlOmVx1MDAxYSBcdTAwMWHmIFx1MDAxN1x1MDAxNfzotUPHXHUwMDA1t8LlbmiFmZo15Cm4OE8xrlqvwVx1MDAxN1xyrTXzN6/Ub/2a8lx1MDAxNFxuKsH/mFx1MDAwZjk3w41cdTAwMTC5eCFtXHUwMDEwXHUwMDAwzdAnckxcdTAwMWHai8VcdTAwMWXdrNVr7dtotPDP9/tcdTAwMDdcdTAwMDfhXHUwMDBlLdeGOeu7XHUwMDE2NddWlVx1MDAxYdP8U2zKoV3l7yQspJVz0pR6h7FRKOs4RS9OXHUwMDFiXHUwMDA1WphcXGfyRpZcdTAwMTORXGLCao1cdTAwMWH8XHUwMDEzYuWfJJiLpNT3wszIRFxcSCvyyEJq8lx1MDAxZqIkXHUwMDEzkmfx7Wv+4XY0d7X0/0hcdTAwMTSlXHUwMDEyyOlgXHUwMDExwlVcdTAwMDTlyc3s/jGhk4TwdrtcdTAwMTVcdTAwMDTpqHVjyrNb3LyMwsn2XT056cvbx3QxvVx1MDAxNVxu/Y3+/fnJ5/9cdTAwMDPV4pXXIn0= Screen 1(hidden)app.pop_screen()Screen 2(hidden)Screen 3(visible)Screen 2(visible)

Screens can be used to build modal dialogs by pushing a screen with controls / buttons, and popping the screen when the user has finished with it. The problem with this approach is that there was nothing to indicate to the user that the original screen was still there, and could be returned to.

In this release we have added alpha support to the Screen's background color which allows the screen underneath to show through, typically blended with a little color. Applying this to a screen makes it clear than the user can return to the previous screen when they have finished interacting with the modal.

Here's how you can enable this effect with CSS:

DialogScreen {\nalign: center middle;\nbackground: $primary 30%;\n}\n

Setting the background to $primary will make the background blue (with the default theme). The addition of 30% sets the alpha so that it will be blended with the background. Here's the kind of effect this creates:

DialogApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u2588\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2588 Fear\u00a0is\u00a0the\u00a0mind-killer\u2588\u2588 Fear\u00a0is\u00a0the\u00a0little-deat\u2588\u2588 I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2588\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2588 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pas\u2588\u2588 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0pa\u2588Good\u00a0for\u00a0natural\u00a0breaks\u00a0in\u00a0the\u00a0content,\u00a0that\u00a0don't\u00a0require\u00a0another\u2588 Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u2588header.\u2588 Fear\u00a0is\u00a0the\u00a0mind-killer\u2588\u2588\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2588 Fear\u00a0is\u00a0the\u00a0little-deat\u2588\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2588\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2588 I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2588\u258e\u258b\u2588\u258b\u2588 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pas\u2588\u258eLists\u258b\u2588\u258b\u2588 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0pa\u2588\u258e\u258b\u2582\u2582\u2588\u258b\u2588 Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u2588\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2588\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2582\u2582\u2588 Fear\u00a0is\u00a0the\u00a0mind-killer\u2588\u00a01.\u00a0Lists\u00a0can\u00a0be\u00a0ordered\u2588down\u00a0widgets.\u2588 Fear\u00a0is\u00a0the\u00a0little-deat\u2588\u00a02.\u00a0Lists\u00a0can\u00a0be\u00a0unordered\u2588\u2588 I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2588\u25cf\u00a0I\u00a0must\u00a0not\u00a0fear.\u2588\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2588 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pas\u2588\u25aa\u00a0Fear\u00a0is\u00a0the\u00a0mind-killer.\u2584\u2584\u2588\u258b\u2588 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0pa\u2588\u2023\u00a0Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2588\u258b\u2588\u2580\u2580\u2580\u2580\u2588 Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u2588\u2022\u00a0I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2588\u258b\u2588\u2588 Fear\u00a0is\u00a0the\u00a0mind-killer\u2588\u2b51\u00a0I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2588\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2588\u2588 Fear\u00a0is\u00a0the\u00a0little-deat\u2588\u25aa\u00a0And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0\u2588\u2588\u2588 I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2588see\u00a0its\u00a0path.\u2588\u2588\u2588 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pas\u2588\u25cf\u00a0Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0\u2588\u2588\u2582\u2582\u2588 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0pa\u2588remain.\u2588\u2588\u2588 Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u2588\u2588\u2588\u2588 Fear\u00a0is\u00a0the\u00a0mind-killer\u2588Longer\u00a0list\u2588\u2588\u2588 Fear\u00a0is\u00a0the\u00a0little-deat\u2588\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2588\u2588\u2588 I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2588\u00a0\u00a01.\u00a0Duke\u00a0Leto\u00a0I\u00a0Atreides,\u00a0head\u00a0of\u00a0House\u00a0Atreides\u2588\u2588\u2588 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pas\u2588\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2588\u00a0headings.\u2588\u2588 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u2588\u2588\u2588 Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0O\u2588This\u00a0is\u00a0H5\u2588\u2588 Fear\u00a0is\u00a0the\u00a0mind-killer.\u2588\u2588\u2588 Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0oblit\u2588Header\u00a0level\u00a05\u00a0content.\u2588\u2588 I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2588\u2588\u2588 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2588This\u00a0is\u00a0H6\u2588\u2588 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u2588\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2588\u2588 Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u2588This\u00a0is\u00a0H4\u2588 Fear\u00a0is\u00a0the\u00a0mind-killer.\u2588\u2588 Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliterati\u2588Header\u00a0level\u00a04\u00a0content.\u00a0Drilling\u00a0down\u00a0in\u00a0to\u00a0finer\u00a0headings.\u2588 I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2588\u2588 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2588This\u00a0is\u00a0H5\u2588 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0\u2588\u2588 Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u2588Header\u00a0level\u00a05\u00a0content.\u2588 Fear\u00a0is\u00a0the\u00a0mind-killer.\u2588\u2588 Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliterati\u2588This\u00a0is\u00a0H6\u2588 I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2588\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2588 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer.

There are 4 screens in the above screenshot, one for the base screen and one for each of the three dialogs. Note how each screen modifies the color of the screen below, but leaves everything visible.

See the docs on screen opacity if you want to add this to your apps.

"},{"location":"blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/#option-list","title":"Option list","text":"

Textual has had a ListView widget for a while, which is an excellent way of navigating a list of items (actually other widgets). In this release we've added an OptionList which is similar in appearance, but uses the line api under the hood. The Line API makes it more efficient when you approach thousands of items.

OptionListApp \u2b58OptionListApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aAerilon\u258e \u258aAquaria\u258e \u258aCanceron\u258e \u258aCaprica\u258e \u258aGemenon\u258e \u258aLeonis\u258e \u258aLibran\u258e \u258aPicon\u258e \u258aSagittaron\u258e \u258aScorpia\u258e \u258aTauron\u258e \u258aVirgon\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

The Options List accepts Rich renderable, which means that anything Rich can render may be displayed in a list. Here's an Option List of tables:

OptionListApp \u2b58OptionListApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Data\u00a0for\u00a0Aerilon\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258e \u258a\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u258e \u258a\u2503Patron\u00a0God\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Population\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Capital\u00a0City\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503\u258e \u258a\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\u258e \u258a\u2502Demeter\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u25021.2\u00a0Billion\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502Gaoth\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u258e \u258a\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u258e \u258a\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Data\u00a0for\u00a0Aquaria\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2582\u2582\u258e \u258a\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u258e \u258a\u2503Patron\u00a0God\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Population\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Capital\u00a0City\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503\u258e \u258a\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\u258e \u258a\u2502Hermes\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250275,000\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502None\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u258e \u258a\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u258e \u258a\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Data\u00a0for\u00a0Canceron\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258e \u258a\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u258e \u258a\u2503Patron\u00a0God\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Population\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Capital\u00a0City\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503\u258e \u258a\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\u258e \u258a\u2502Hephaestus\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u25026.7\u00a0Billion\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502Hades\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u258e \u258a\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u258e \u258a\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Data\u00a0for\u00a0Caprica\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258e \u258a\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u258e \u258a\u2503Patron\u00a0God\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Population\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Capital\u00a0City\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503\u258e \u258a\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

We plan to build on the OptionList widget to implement drop-downs, menus, check lists, etc. But it is still very useful as it is, and you can add it to apps now.

"},{"location":"blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/#what-else","title":"What else?","text":"

There are a number of fixes regarding refreshing in this release. If you had issues with parts of the screen not updating, the new version should resolve it.

There's also a new logging handler, and a \"thick\" border type.

See release notes for the full details.

"},{"location":"blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/#next-week","title":"Next week","text":"

Next week we plan to take a break from building Textual to building apps with Textual. We do this now and again to give us an opportunity to step back and understand things from the perspective of a developer using Textual. We will hopefully have something interesting to show from the exercise, and new Open Source apps to share.

"},{"location":"blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/#join-us","title":"Join us","text":"

If you want to talk about this update or anything else Textual related, join us on our Discord server.

"},{"location":"blog/2023/04/04/textual-0180-adds-api-for-managing-concurrent-workers/","title":"Textual 0.18.0 adds API for managing concurrent workers","text":"

Less than a week since the last release, and we have a new API to show you.

This release adds a new Worker API designed to manage concurrency, both asyncio tasks and threads.

An API to manage concurrency may seem like a strange addition to a library for building user interfaces, but on reflection it makes a lot of sense. People are building Textual apps to interface with REST APIs, websockets, and processes; and they are running into predictable issues. These aren't specifically Textual problems, but rather general problems related to async tasks and threads. It's not enough for us to point users at the asyncio docs, we needed a better answer.

The new run_worker method provides an easy way of launching \"Workers\" (a wrapper over async tasks and threads) which also manages their lifetime.

One of the challenges I've found with tasks and threads is ensuring that they are shut down in an orderly manner. Interestingly enough, Textual already implemented an orderly shutdown procedure to close the tasks that power widgets: children are shut down before parents, all the way up to the App (the root node). The new API piggybacks on to that existing mechanism to ensure that worker tasks are also shut down in the same order.

Tip

You won't need to worry about this gnarly issue with the new Worker API.

I'm particularly pleased with the new @work decorator which can turn a coroutine OR a regular function into a Textual Worker object, by scheduling it as either an asyncio task or a thread. I suspect this will solve 90% of the concurrency issues we see with Textual apps.

See the Worker API for the details.

"},{"location":"blog/2023/04/04/textual-0180-adds-api-for-managing-concurrent-workers/#join-us","title":"Join us","text":"

If you want to talk about this update or anything else Textual related, join us on our Discord server.

"},{"location":"blog/2023/05/03/textual-0230-improves-message-handling/","title":"Textual 0.23.0 improves message handling","text":"

It's been a busy couple of weeks at Textualize. We've been building apps with Textual, as part of our dog-fooding week. The first app, Frogmouth, was released at the weekend and already has 1K GitHub stars! Expect two more such apps this month.

Frogmouth /Users/willmcgugan/projects/textual/FAQ.md ContentsLocalBookmarksHistory\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u258e\u258a \u258eHow\u00a0do\u00a0I\u00a0pass\u00a0arguments\u00a0to\u00a0an\u00a0app?\u258a \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u258e\u258a \u2503\u25bc\u00a0\u2160\u00a0Frequently\u00a0Asked\u00a0Questions\u2503\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0Does\u00a0Textual\u00a0support\u00a0images?\u2503When\u00a0creating\u00a0your\u00a0App\u00a0class,\u00a0override\u00a0__init__\u00a0as\u00a0you\u00a0would\u00a0wheninheriting\u00a0normally.\u00a0For\u00a0example: \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0can\u00a0I\u00a0fix\u00a0ImportError\u00a0cannot\u00a0i\u2503 \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0can\u00a0I\u00a0select\u00a0and\u00a0copy\u00a0text\u00a0in\u00a0\u2503 \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0can\u00a0I\u00a0set\u00a0a\u00a0translucent\u00a0app\u00a0ba\u2503fromtextual.appimportApp,ComposeResult \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0do\u00a0I\u00a0center\u00a0a\u00a0widget\u00a0in\u00a0a\u00a0scre\u2503fromtextual.widgetsimportStatic \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0do\u00a0I\u00a0pass\u00a0arguments\u00a0to\u00a0an\u00a0app?\u2503 \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0Why\u00a0do\u00a0some\u00a0key\u00a0combinations\u00a0never\u2503classGreetings(App[None]): \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0Why\u00a0doesn't\u00a0Textual\u00a0look\u00a0good\u00a0on\u00a0m\u2503\u2502\u00a0\u00a0\u00a0 \u2503\u2514\u2500\u2500\u00a0\u2161\u00a0Why\u00a0doesn't\u00a0Textual\u00a0support\u00a0ANSI\u00a0t\u2503\u2502\u00a0\u00a0\u00a0def__init__(self,greeting:str=\"Hello\",to_greet:str=\"World\")->None: \u2503\u2503\u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0self.greeting=greeting \u2503\u2503\u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0self.to_greet=to_greet \u2503\u2503\u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0super().__init__() \u2503\u2503\u2502\u00a0\u00a0\u00a0 \u2503\u2503\u2502\u00a0\u00a0\u00a0defcompose(self)->ComposeResult: \u2503\u2503\u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0yieldStatic(f\"{self.greeting},\u00a0{self.to_greet}\") \u2503\u2503 \u2503\u2503 \u2503\u2503Then\u00a0the\u00a0app\u00a0can\u00a0be\u00a0run,\u00a0passing\u00a0in\u00a0various\u00a0arguments;\u00a0for\u00a0example: \u2503\u2503\u2585\u2585 \u2503\u2503 \u2503\u2503#\u00a0Running\u00a0with\u00a0default\u00a0arguments. \u2503\u2503Greetings().run() \u2503\u2503 \u2503\u2503#\u00a0Running\u00a0with\u00a0a\u00a0keyword\u00a0arguyment. \u2503\u2503Greetings(to_greet=\"davep\").run()\u2585\u2585 \u2503\u2503 \u2503\u2503#\u00a0Running\u00a0with\u00a0both\u00a0positional\u00a0arguments. \u2503\u2503Greetings(\"Well\u00a0hello\",\"there\").run() \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2503\u2589\u2503\u258e\u258a \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u00a0F1\u00a0\u00a0Help\u00a0\u00a0F2\u00a0\u00a0About\u00a0\u00a0CTRL+N\u00a0\u00a0Navigation\u00a0\u00a0CTRL+Q\u00a0\u00a0Quit\u00a0

Tip

Join our mailing list if you would like to be the first to hear about our apps.

We haven't stopped developing Textual in that time. Today we released version 0.23.0 which has a really interesting API update I'd like to introduce.

Textual widgets can send messages to each other. To respond to those messages, you implement a message handler with a naming convention. For instance, the Button widget sends a Pressed event. To handle that event, you implement a method called on_button_pressed.

Simple enough, but handler methods are called to handle pressed events from all Buttons. To manage multiple buttons you typically had to write a large if statement to wire up each button to the code it should run. It didn't take many Buttons before the handler became hard to follow.

"},{"location":"blog/2023/05/03/textual-0230-improves-message-handling/#on-decorator","title":"On decorator","text":"

Version 0.23.0 introduces the @on decorator which allows you to dispatch events based on the widget that initiated them.

This is probably best explained in code. The following two listings respond to buttons being pressed. The first uses a single message handler, the second uses the decorator approach:

on_decorator01.pyon_decorator02.pyOutput on_decorator01.py
from textual import on\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Button\nclass OnDecoratorApp(App):\nCSS_PATH = \"on_decorator.tcss\"\ndef compose(self) -> ComposeResult:\n\"\"\"Three buttons.\"\"\"\nyield Button(\"Bell\", id=\"bell\")\nyield Button(\"Toggle dark\", classes=\"toggle dark\")\nyield Button(\"Quit\", id=\"quit\")\ndef on_button_pressed(self, event: Button.Pressed) -> None:  # (1)!\n\"\"\"Handle all button pressed events.\"\"\"\nif event.button.id == \"bell\":\nself.bell()\nelif event.button.has_class(\"toggle\", \"dark\"):\nself.dark = not self.dark\nelif event.button.id == \"quit\":\nself.exit()\nif __name__ == \"__main__\":\napp = OnDecoratorApp()\napp.run()\n
  1. The message handler is called when any button is pressed
on_decorator02.py
from textual import on\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Button\nclass OnDecoratorApp(App):\nCSS_PATH = \"on_decorator.tcss\"\ndef compose(self) -> ComposeResult:\n\"\"\"Three buttons.\"\"\"\nyield Button(\"Bell\", id=\"bell\")\nyield Button(\"Toggle dark\", classes=\"toggle dark\")\nyield Button(\"Quit\", id=\"quit\")\n@on(Button.Pressed, \"#bell\")  # (1)!\ndef play_bell(self):\n\"\"\"Called when the bell button is pressed.\"\"\"\nself.bell()\n@on(Button.Pressed, \".toggle.dark\")  # (2)!\ndef toggle_dark(self):\n\"\"\"Called when the 'toggle dark' button is pressed.\"\"\"\nself.dark = not self.dark\n@on(Button.Pressed, \"#quit\")  # (3)!\ndef quit(self):\n\"\"\"Called when the quit button is pressed.\"\"\"\nself.exit()\nif __name__ == \"__main__\":\napp = OnDecoratorApp()\napp.run()\n
  1. Matches the button with an id of \"bell\" (note the # to match the id)
  2. Matches the button with class names \"toggle\" and \"dark\"
  3. Matches the button with an id of \"quit\"

OnDecoratorApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Bell\u00a0\u00a0Toggle\u00a0dark\u00a0\u00a0Quit\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

The decorator dispatches events based on a CSS selector. This means that you could have a handler per button, or a handler for buttons with a shared class, or parent.

We think this is a very flexible mechanism that will help keep code readable and maintainable.

"},{"location":"blog/2023/05/03/textual-0230-improves-message-handling/#why-didnt-we-do-this-earlier","title":"Why didn't we do this earlier?","text":"

It's a reasonable question to ask: why didn't we implement this in an earlier version? We were certainly aware there was a deficiency in the API.

The truth is simply that we didn't have an elegant solution in mind until recently. The @on decorator is, I believe, an elegant and powerful mechanism for dispatching handlers. It might seem obvious in hindsight, but it took many iterations and brainstorming in the office to come up with it!

"},{"location":"blog/2023/05/03/textual-0230-improves-message-handling/#join-us","title":"Join us","text":"

If you want to talk about this update or anything else Textual related, join us on our Discord server.

"},{"location":"blog/2023/05/08/textual-0240-adds-a-select-control/","title":"Textual 0.24.0 adds a Select control","text":"

Coming just 5 days after the last release, we have version 0.24.0 which we are crowning the King of Textual releases. At least until it is deposed by version 0.25.0.

The highlight of this release is the new Select widget: a very familiar control from the web and desktop worlds. Here's a screenshot and code:

Output (expanded)select_widget.pyselect.css

SelectApp \u2b58SelectApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aSelect\u25b2\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aSelect\u258e \u258aI\u00a0must\u00a0not\u00a0fear.\u258e \u258aFear\u00a0is\u00a0the\u00a0mind-killer.\u258e \u258aFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0\u258e \u258aobliteration.\u258e \u258aI\u00a0will\u00a0face\u00a0my\u00a0fear.\u258e \u258aI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

from textual import on\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Header, Select\nLINES = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\"\"\".splitlines()\nclass SelectApp(App):\nCSS_PATH = \"select.tcss\"\ndef compose(self) -> ComposeResult:\nyield Header()\nyield Select((line, line) for line in LINES)\n@on(Select.Changed)\ndef select_changed(self, event: Select.Changed) -> None:\nself.title = str(event.value)\nif __name__ == \"__main__\":\napp = SelectApp()\napp.run()\n
\n
"},{"location":"blog/2023/05/08/textual-0240-adds-a-select-control/#new-styles","title":"New styles","text":"

This one required new functionality in Textual itself. The \"pull-down\" overlay with options presented a difficulty with the previous API. The overlay needed to appear over any content below it. This is possible (using layers), but there was no simple way of positioning it directly under the parent widget.

We solved this with a new \"overlay\" concept, which can considered a special layer for user interactions like this Select, but also pop-up menus, tooltips, etc. Widgets styled to use the overlay appear in their natural place in the \"document\", but on top of everything else.

A second problem we tackled was ensuring that an overlay widget was never clipped. This was also solved with a new rule called \"constrain\". Applying constrain to a widget will keep the widget within the bounds of the screen. In the case of Select, if you expand the options while at the bottom of the screen, then the overlay will be moved up so that you can see all the options.

These new rules are currently undocumented as they are still subject to change, but you can see them in the Select source if you are interested.

In a future release these will be finalized and you can confidently use them in your own projects.

"},{"location":"blog/2023/05/08/textual-0240-adds-a-select-control/#fixes-for-the-on-decorator","title":"Fixes for the @on decorator","text":"

The new @on decorator is proving popular. To recap, it is a more declarative and finely grained way of dispatching messages. Here's a snippet from the calculator example which uses @on:

    @on(Button.Pressed, \"#plus,#minus,#divide,#multiply\")\ndef pressed_op(self, event: Button.Pressed) -> None:\n\"\"\"Pressed one of the arithmetic operations.\"\"\"\nself.right = Decimal(self.value or \"0\")\nself._do_math()\nassert event.button.id is not None\nself.operator = event.button.id\n

The decorator arranges for the method to be called when any of the four math operation buttons are pressed.

In 0.24.0 we've fixed some missing attributes which prevented the decorator from working with some messages. We've also extended the decorator to use keywords arguments, so it will match attributes other than control.

"},{"location":"blog/2023/05/08/textual-0240-adds-a-select-control/#other-fixes","title":"Other fixes","text":"

There is a surprising number of fixes in this release for just 5 days. See CHANGELOG.md for details.

"},{"location":"blog/2023/05/08/textual-0240-adds-a-select-control/#join-us","title":"Join us","text":"

If you want to talk about this update or anything else Textual related, join us on our Discord server.

"},{"location":"blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/","title":"Textual adds Sparklines, Selection list, Input validation, and tool tips","text":"

It's been 12 days since the last Textual release, which is longer than our usual release cycle of a week.

We've been a little distracted with our \"dogfood\" projects: Frogmouth and Trogon. Both of which hit 1000 Github stars in 24 hours. We will be maintaining / updating those, but it is business as usual for this Textual release (and it's a big one). We have such sights to show you.

"},{"location":"blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/#sparkline-widget","title":"Sparkline widget","text":"

A Sparkline is essentially a mini-plot. Just detailed enough to keep an eye on time-series data.

SparklineColorsApp \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582 \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582 \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582 \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582 \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582 \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582 \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582 \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582 \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582 \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582

Colors are configurable, and all it takes is a call to set_interval to make it animate.

"},{"location":"blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/#selection-list","title":"Selection list","text":"

Next up is the SelectionList widget. Essentially a scrolling list of checkboxes. Lots of use cases for this one.

SelectionListApp \u2b58SelectionListApp \u250c\u2500\u00a0Shall\u00a0we\u00a0play\u00a0some\u00a0games?\u00a0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502 \u2502\u2590X\u258cFalken's\u00a0Maze\u2502 \u2502\u2590X\u258cBlack\u00a0Jack\u2502 \u2502\u2590X\u258cGin\u00a0Rummy\u2502 \u2502\u2590X\u258cHearts\u2502 \u2502\u2590X\u258cBridge\u2502 \u2502\u2590X\u258cCheckers\u2502 \u2502\u2590X\u258cChess\u2502 \u2502\u2590X\u258cPoker\u2502 \u2502\u2590X\u258cFighter\u00a0Combat\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"},{"location":"blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/#tooltips","title":"Tooltips","text":"

We've added tooltips to Textual widgets.

The API couldn't be simpler: simply assign a string to the tooltip property on any widget. This string will be displayed after 300ms when you hover over the widget.

TooltipApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Click\u00a0me \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear.

As always, you can configure how the tooltips will be displayed with CSS.

"},{"location":"blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/#input-updates","title":"Input updates","text":"

We have some quality of life improvements for the Input widget.

You can now use a simple declarative API to validating input.

InputApp Enter\u00a0an\u00a0even\u00a0number\u00a0between\u00a01\u00a0and\u00a0100\u00a0that\u00a0is\u00a0also\u00a0a\u00a0palindrome. \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258afoo\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e ['Must\u00a0be\u00a0a\u00a0valid\u00a0number.',\u00a0'Value\u00a0is\u00a0not\u00a0even.',\u00a0\"That's\u00a0not\u00a0a\u00a0palindrome\u00a0:/\"]

Also in this release is a suggestion API, which will suggest auto completions as you type. Hit right to accept the suggestion.

Here's a screenshot:

FruitsApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258astrawberry\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

You could use this API to offer suggestions from a fixed list, or even pull the data from a network request.

"},{"location":"blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/#join-us","title":"Join us","text":"

Development on Textual is fast. We're very responsive to issues and feature requests.

If you have any suggestions, jump on our Discord server and you may see your feature in the next release!

"},{"location":"blog/2023/07/03/textual-0290-refactors-dev-tools/","title":"Textual 0.29.0 refactors dev tools","text":"

It's been a slow week or two at Textualize, with Textual devs taking well-earned annual leave, but we still managed to get a new version out.

Version 0.29.0 has shipped with a number of fixes (see the release notes for details), but I'd like to use this post to explain a change we made to how Textual developer tools are distributed.

Previously if you installed textual[dev] you would get the Textual dev tools plus the library itself. If you were distributing Textual apps and didn't need the developer tools you could drop the [dev].

We did this because the less dependencies a package has, the fewer installation issues you can expect to get in the future. And Textual is surprisingly lean if you only need to run apps, and not build them.

Alas, this wasn't quite as elegant solution as we hoped. The dependencies defined in extras wouldn't install commands, so textual was bundled with the core library. This meant that if you installed the Textual package without the [dev] you would still get the textual command on your path but it wouldn't run.

We solved this by creating two packages: textual contains the core library (with minimal dependencies) and textual-dev contains the developer tools. If you are building Textual apps, you should install both as follows:

pip install textual textual-dev\n

That's the only difference. If you run in to any issues feel free to ask on the Discord server!

"},{"location":"blog/2023/07/17/textual-0300-adds-desktop-style-notifications/","title":"Textual 0.30.0 adds desktop-style notifications","text":"

We have a new release of Textual to talk about, but before that I'd like to cover a little Textual news.

By sheer coincidence we reached 20,000 stars on GitHub today. Now stars don't mean all that much (at least until we can spend them on coffee), but its nice to know that twenty thousand developers thought Textual was interesting enough to hit the \u2605 button. Thank you!

In other news: we moved office. We are now a stone's throw away from Edinburgh Castle. The office is around three times as big as the old place, which means we have room for wide standup desks and dual monitors. But more importantly we have room for new employees. Don't send your CVs just yet, but we hope to grow the team before the end of the year.

Exciting times.

"},{"location":"blog/2023/07/17/textual-0300-adds-desktop-style-notifications/#new-release","title":"New Release","text":"

And now, for the main feature. Version 0.30 adds a new notification system. Similar to desktop notifications, it displays a small window with a title and message (called a toast) for a pre-defined number of seconds.

Notifications are great for short timely messages to add supplementary information for the user. Here it is in action:

The API is super simple. To display a notification, call notify() with a message and an optional title.

def on_mount(self) -> None:\nself.notify(\"Hello, from Textual!\", title=\"Welcome\")\n
"},{"location":"blog/2023/07/17/textual-0300-adds-desktop-style-notifications/#textualize-video-channel","title":"Textualize Video Channel","text":"

In case you missed it; Textualize now has a YouTube channel. Our very own Rodrigo has recorded a video tutorial series on how to build Textual apps. Check it out!

We will be adding more videos in the near future, covering anything from beginner to advanced topics.

Don't worry if you prefer reading to watching videos. We will be adding plenty more content to the Textual docs in the near future. Watch this space.

As always, if you want to discuss anything with the Textual developers, join us on the Discord server.

"},{"location":"blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/","title":"Textual 0.38.0 adds a syntax aware TextArea","text":"

This is the second big feature release this month after last week's command palette.

The TextArea has finally landed. I know a lot of folk have been waiting for this one. Textual's TextArea is a fully-featured widget for editing code, with syntax highlighting and line numbers. It is highly configurable, and looks great.

Darren Burns (the author of this widget) has penned a terrific write-up on the TextArea. See Things I learned while building Textual's TextArea for some of the challenges he faced.

"},{"location":"blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/#scoped-css","title":"Scoped CSS","text":"

Another notable feature added in 0.38.0 is scoped CSS. A common gotcha in building Textual widgets is that you could write CSS that impacted styles outside of that widget.

Consider the following widget:

class MyWidget(Widget):\nDEFAULT_CSS = \"\"\"\n    MyWidget {\n        height: auto;\n        border: magenta;\n    }\n    Label {\n        border: solid green;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Label(\"foo\")\nyield Label(\"bar\")\n

The author has intended to style the labels in that widget by adding a green border. This does work for the widget in question, but (prior to 0.38.0) the Label rule would style all Labels (including any outside of the widget) \u2014 which was probably not intended.

With version 0.38.0, the CSS is scoped so that only the widget's labels will be styled. This is almost always what you want, which is why it is enabled by default. If you do want to style something outside of the widget you can set SCOPED_CSS=False (as a classvar).

"},{"location":"blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/#light-and-dark-pseudo-selectors","title":"Light and Dark pseudo selectors","text":"

We've also made a slight quality of life improvement to the CSS, by adding :light and :dark pseudo selectors. This allows you to change styles depending on whether you have dark mode enabled or not.

This was possible before, just a little verbose. Here's how you would do it in 0.37.0:

App.-dark-mode MyWidget Label {\n...\n}\n

In 0.38.0 it's a little more concise and readable:

MyWidget:dark Label {\n...\n}\n
"},{"location":"blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/#testing-guide","title":"Testing guide","text":"

Not strictly part of the release, but we've added a guide on testing Textual apps.

As you may know, we are on a mission to make TUIs a serious proposition for critical apps, which makes testing essential. We've extracted and documented our internal testing tools, including our snapshot tests pytest plugin pytest-textual-snapshot.

This gives devs powerful tools to ensure the quality of their apps. Let us know your thoughts on that!

"},{"location":"blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/#release-notes","title":"Release notes","text":"

See the release page for the full details on this release.

"},{"location":"blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/#whats-next","title":"What's next?","text":"

There's lots of features planned over the next few months. One feature I am particularly excited by is a widget to generate plots by wrapping the awesome Plotext library. Check out some early work on this feature:

"},{"location":"blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/#join-us","title":"Join us","text":"

Join our Discord server if you want to discuss Textual with the Textualize devs, or the community.

"},{"location":"blog/2022/11/08/version-040/","title":"Version 0.4.0","text":"

We've released version 0.4.0 of Textual.

As this is the first post tagged with release let me first explain where the blog fits in with releases. We plan on doing a post for every note-worthy release. Which likely means all but the most trivial updates (typos just aren't that interesting). Blog posts will be supplementary to release notes which you will find on the Textual repository.

Blog posts will give a little more background for the highlights in a release, and a rationale for changes and new additions. We embrace building in public, which means that we would like you to be as up-to-date with new developments as if you were sitting in our office. It's a small office, and you might not be a fan of the Scottish weather (it's dreich), but you can at least be here virtually.

Release 0.4.0 follows 0.3.0, released on October 31st. Here are the highlights of the update.

"},{"location":"blog/2022/11/08/version-040/#updated-mount-method","title":"Updated Mount Method","text":"

The mount method has seen some work. We've dropped the ability to assign an id via keyword attributes, which wasn't terribly useful. Now, an id must be assigned via the constructor.

The mount method has also grown before and after parameters which tell Textual where to add a new Widget (the default was to add it to the end). Here are a few examples:

# Mount at the start\nself.mount(Button(id=\"Buy Coffee\"), before=0)\n# Mount after a selector\nself.mount(Static(\"Password is incorrect\"), after=\"Dialog Input.-error\")\n# Mount after a specific widget\ntweet = self.query_one(\"Tweet\")\nself.mount(Static(\"Consider switching to Mastodon\"), after=tweet)\n

Textual needs much of the same kind of operations as the JS API exposed by the browser. But we are determined to make this way more intuitive. The new mount method is a step towards that.

"},{"location":"blog/2022/11/08/version-040/#faster-updates","title":"Faster Updates","text":"

Textual now writes to stdout in a thread. The upshot of this is that Textual can work on the next update before the terminal has displayed the previous frame.

This means smoother updates all round! You may notice this when scrolling and animating, but even if you don't, you will have more CPU cycles to play with in your Textual app.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN2ca3Oa3Fx1MDAxNsff91NkPG8r3fdLZ86cybW5t401SXPmmVx1MDAwZUFUXCKK4WIunX73Z0FsXHUwMDAwI2pcdTAwMTKDTPLCKlx1MDAxYvZebNaP/1prQ39/WFurhXdDu/Z5rWbfWqbrtHzzpvYx3j6y/cDxXHUwMDA20ESS34FcdTAwMTf5VrJnN1xmh8HnT5/6pt+zw6FrWrYxcoLIdIMwajmeYXn9T05o94P/xZ/HZt/+79Drt0LfSFx1MDAwN6nbLSf0/IexbNfu24MwgN7/XHUwMDBmv9fWfiefXHUwMDE563zbXG7NQce1k1x1MDAwM5Km1ECK1eTWY2+QXHUwMDE4SzDDVFx1MDAxMk7Q41x1MDAxZU6wXHUwMDA144V2XHUwMDBimttgs522xJtq55fW8dVgOzo971xuWd857UaDzct02Lbjuo3wzn2YXG7T6kZ+xqgg9L2efea0wi6044ntj8dcdTAwMDVcdTAwMWXMQnqU70Wd7sBcdTAwMGWC3DHe0LSc8C7ehlLzXHUwMDFmZuHzWrrlXHUwMDE2filuKClcdTAwMTVcdTAwMTVCYc6kXHUwMDE0j63J8ZxcdTAwMWGEU41cdTAwMDThXGL+Ju3a9FxcuFx1MDAxNGDXf6hgbUumll2aVq9cdTAwMDPmXHJa6T6EqEtbKJXudfP3fJkyiJSUUclcdTAwMWZcdTAwMWK7ttPphtDKkKGlXCJgI5KYXHUwMDExRVMr7ORqxC2USMZcdTAwMWVcdTAwMWLioYd7rcQx/pmczK7pXHUwMDBmx5NWXHUwMDBi4lx1MDAxZlx1MDAxObNji7cnvSrrWZlcdTAwMGJ+2T5qXHUwMDFjOMdcciFcdTAwMWPinp9uXHUwMDFmXHUwMDA218HPx75yblx1MDAxONq3Ye2x4c/H6d3m9v646IBpt+Nv6Vx0R8OW+eCwWFxiqWGKJaY09Vx1MDAwMNdcdTAwMTn0oHFcdTAwMTC5brrNs3qpj3/I2PtMuDQthotKXHUwMDA03pa5YPPgut9ccr5aR0PyvVlf92+PLbp/9l1UXHUwMDFkrjp4q0GRXHUwMDE2XHUwMDA0cy01opzk8FwiXGJcdTAwMWKEUnBuJqTChFx1MDAxN+LF27Rlsdl42YJphabhRVxiM4RmkjMmgFx1MDAxZq5ewFx1MDAxONxcdTAwMGaVwHBcdTAwMDaqXFzIml/I/p55MrpAw9b1tT/sbl37J29cdNn0XHUwMDAxn1x1MDAwM1x1MDAxOUwkUrxcdTAwMTTIXHUwMDE4xkWQYSm4RIhxsTBkw1x1MDAxZt++7l1cdTAwMWW1aGRcdTAwMWTgId9wej/vulWHjFx0XHUwMDBljGFcdTAwMDJcdTAwMTOviKSgXCJcdTAwMTNcdTAwMTImXGbMlVx1MDAxNFx1MDAxOCeuzYohW6mGYaQg2lx1MDAxMFx1MDAxNJNy+dq17qXd22t2b2lvyLqX9uHoXHUwMDFifku+plx1MDAwZlhRXHUwMDExY1pcdTAwMTbxJZhcIkhgtLiG3e2qKPq1dVx1MDAxZlx1MDAxYzXNkfn91t05cdpVx4tQaVx1MDAwMFVcdTAwMTju/pJzzSfo0spA0FwiXHUwMDE555RLnLnZVE/CXGLinCnKsChcdTAwMTexg63v6Ng+6nxcdTAwMGbCbvO62d7mqH/2lohNXHUwMDFmcHWI5ezMJoikXGIuTFx1MDAxOIeQnujFxWt2oFBRuupCXHUwMDE4TIBzM1xuai2QyONFkDZcYokjyPiCKKqXXHUwMDExIT6FKyOZjzRlg9ExPlx1MDAwMJVcdTAwMTBYv4VCLTOaSi+5N1xiXHUwMDFizn2ScaDc1lx1MDAxZLPvuHe5q5b4aOxGyfjZeVxmbFx1MDAxODNxSpXbe911OrFcdTAwMTfXLDhcdTAwMGLbzzl46Fim+7hD32m1svpigVx0JvTp7y2S23i+03FcdTAwMDam+yNv4YuIK6x3MKmpQGRxMZud/FZcdTAwMTQ3TJmBsODgUFxcQFx1MDAwMqry+Vx1MDAxOFx1MDAxNpCPMVxu07BENXuKWyZcdTAwMGKcgVx1MDAxYqaEXHUwMDBirbF8XHUwMDAztVrmnf91uJ35Tsm0zSnTTdL2YOCLYCvOzTjHXHUwMDFhJluphXGbXHUwMDFkQ1RcdTAwMTQ3iqnBXHUwMDE1Y5prXHUwMDA0mZGarH5cYlx1MDAwM0muYFx1MDAxZYA1ls18ViFuXHUwMDEwNCrMdbniVjJtK1x1MDAxMLc5SU8p4iYgZ6BU4cWDydlZcUVx44BcdTAwMWKjWsPpclx1MDAxOVdCJsSNXHUwMDE5XHUwMDE4JE/qh1xmSqo3wW1BcVx1MDAwMyvB0pJDyXevbXNcbngv0LbZ1ZHMZE4gJ1x0QpqpZ1x1MDAwNJT7I3SAdjZa/kE/XHUwMDFhnUdHfp1wXXXkMKKGXHUwMDAy5WJcYkI1hjjOIcewNFx1MDAwNChcdTAwMWZCY5ErJG7FpUfQZ1x1MDAwMVx1MDAwMW/JdVx1MDAxMeF87TaO2DpB9+bJSe8wwvhm9Jq6yFx1MDAxYnU7r9wyfcC02/G3alQ0OS1cXJbjRGhCsmvAc2Wy8+XQPJDfnOZF65c2zZsrc++u6szWMUlcdTAwMTa9OTi8oFwiXHUwMDBlTfPUXG6QUak4pH/JqncxtasvaWKCQEhFdpGnXHUwMDE0dCN2u3s9ijbcXHUwMDAzT9zwweHVcc/uvVx1MDAxZd2ldztcdTAwMGbd6Vx1MDAwM1ZcdTAwMTVdrYvQJZpoXHIh7uJye9A7adCGa91ftK9+7Gy2T5v3vdOqo0spMTAnXHUwMDAwLlx1MDAxMVxmXCJZzfLkYmFcYlx1MDAxZcdcdTAwMWRcdTAwMGbAZG51lVx1MDAxMlxcolC85l82tPvWyW5k4u0uP6xcdTAwMWaMblx1MDAwNlx1MDAwM1Cr6PXQLr3bedBOXHUwMDFmsKLQSsSLoMVcdTAwMWHFSal+XHUwMDA2tdH2VXR4tyt/ycPG9lx1MDAxNW9IsnfxperUYsRcZsqFwpJSXHUwMDExL2boPLWgt5CfYyrwPGorILiSY1x1MDAxNa9Olczu1sXXy0NN7ky457Gj4O6871xmL1/P7tK7ncfu9Fx1MDAwMVfHbmHtVlx1MDAxND9YoyA4XHUwMDA0aJ+R286OayqKbZ0jXHUwMDAzUYRcdTAwMTVcdTAwMDI448WSiYdDmcRcdTAwMDZcdTAwMTNSUPqQ3L7Rasli9du4sqWIxO+7orSC+u2cXHUwMDE0b5n1W4xcdTAwMGLjWy21ZOCKiyM3u1xuUFHkMMeGjlx1MDAxN/0gxGVxOWmCOEhcXLlcdTAwMDYh1VgnXG62yvXJeMVcdTAwMDRcdKrouyau/Fx1MDAxYe6cOuhcdTAwMTLXJ8lcZt5cdTAwMTSDm756xuM3syOJqvKmkEFcdTAwMTDDXHUwMDFh4jnIx8hEOlx0XHUwMDAyJyghT+PF8vVNQ85cdTAwMGKRqX7XXHUwMDBmXHUwMDAzrEDe5iRUS5U3XZxcdFIpiFr8VaPZKXdFYaMxbCBtTGGqp1x1MDAxNG/i7IwhJMRY2+Sb4Lbg+iRcdTAwMDfkJc247jvErXxtm1N0XFyitlx1MDAxNT5cdTAwMGKAXHUwMDA1XCKCaJEpy8yj7eJLdK+Q02tcXHX3XHUwMDFhO1dnW0PvdLfqtGFFXGZcdTAwMTRnZVx1MDAwNCdcdTAwMGZcdTAwMDPofCippVx1MDAwMUmdYip+qlx1MDAxM9LZV7FWXFwoXHUwMDE1U2osT2mjXHUwMDA0bn/wUX4kSXRptG3Ybc9/XHUwMDFlbq7dXHUwMDBlZ8BcdTAwMTZ6w1wi0nJnMYnV2JJcdTAwMTdxVfg6XHUwMDA0ZkzHIUqmejePK9usb1x1MDAwNH3xrd+MzlG9j++3tjevK8+VIIZcdTAwMDBqXG64ojxeoVx1MDAxMFxuKVx1MDAxND9BOut1o9eRlcn9ZpAlKKVSKVa+jpVI1no7J0qrXHUwMDAz68GQmVxcmb7v3UxNxlDhujxcdTAwMDQjSHKK9OJoscb53k7n+HJ9py72/P3dU7y19cKHaSafynzDV424QWAvTjWjkIzhiXSMXCJlcFx1MDAwMOLvXHUwMDAzbK9Lx4rJgsFccqD872NymE5J0LChMFx1MDAwNKtiSuSIOFFcdTAwMWGJXHUwMDE3vIueWFg2cUFo+uGGM2g5g87kIfagVdDimkG46fX7TlxiZnzznEE4uUfS73rs7V3bfEJcdTAwMGX0nG2rhb4zsYI2jDvNL4uk39ZSz0l+PH7/5+PUvVx0llx1MDAwNlI4juhxPreI/+rI0EhcdTAwMDGAmPB5PVx1MDAxNTtH0lPqXHUwMDE3aUdcdTAwMWay/z5Xb0XxS/SIKU3gfNLLPrdGc/Szuf+r88VcdTAwMTXO6c9r+/asqe2rqt9cdTAwMTXqXFxcdTAwMTlcdTAwMTBcdTAwMWRcblx1MDAxZf+PXHUwMDFjQiOevytAKG9gzcZcdTAwMDXRV69CzLgtLCS4mFx0grR8529t/HD6lYhkXHUwMDEzO1x1MDAxZbj6MEa2Zlx1MDAwZYeNMC7SfFx1MDAxZVNcdTAwMDZcdTAwMTfAaY1PMe2tNnLsm40pXHUwMDFl0E7+4l5cdTAwMTNWYyrsePp///nw51+TJY25In0= UpdateWriteUpdateWriteUpdateWriteUpdateWriteBeforeAfterTime"},{"location":"blog/2022/11/08/version-040/#multiple-css-paths","title":"Multiple CSS Paths","text":"

Up to version 0.3.0, Textual would only read a single CSS file set in the CSS_PATH class variable. You can now supply a list of paths if you have more than one CSS file.

This change was prompted by tuilwindcss which brings a TailwindCSS like approach to building Textual Widgets. Also check out calmcode.io by the same author, which is an amazing resource.

"},{"location":"blog/2022/12/11/version-060/","title":"Textual 0.6.0 adds a treemendous new widget","text":"

A new release of Textual lands 3 weeks after the previous release -- and it's a big one.

Information

If you're new here, Textual is TUI framework for Python.

"},{"location":"blog/2022/12/11/version-060/#tree-control","title":"Tree Control","text":"

The headline feature of version 0.6.0 is a new tree control built from the ground-up. The previous Tree control suffered from an overly complex API and wasn't scalable (scrolling slowed down with 1000s of nodes).

This new version has a simpler API and is highly scalable (no slowdown with larger trees). There are also a number of visual enhancements in this version.

Here's a very simple example:

Outputtree.py

TreeApp \u25bc\u00a0Dune \u2517\u2501\u2501\u00a0\u25bc\u00a0Characters \u2523\u2501\u2501\u00a0Paul \u2523\u2501\u2501\u00a0Jessica \u2517\u2501\u2501\u00a0Chani

from textual.app import App, ComposeResult\nfrom textual.widgets import Tree\nclass TreeApp(App):\ndef compose(self) -> ComposeResult:\ntree: Tree[dict] = Tree(\"Dune\")\ntree.root.expand()\ncharacters = tree.root.add(\"Characters\", expand=True)\ncharacters.add_leaf(\"Paul\")\ncharacters.add_leaf(\"Jessica\")\ncharacters.add_leaf(\"Chani\")\nyield tree\nif __name__ == \"__main__\":\napp = TreeApp()\napp.run()\n

Here's the tree control being used to navigate some JSON (json_tree.py in the examples directory).

I'm biased of course, but I think this terminal based tree control is more usable (and even prettier) than just about anything I've seen on the web or desktop. So much of computing tends to organize itself in to a tree that I think this widget will find a lot of uses.

The Tree control forms the foundation of the DirectoryTree widget, which has also been updated. Here it is used in the code_browser.py example:

"},{"location":"blog/2022/12/11/version-060/#list-view","title":"List View","text":"

We have a new ListView control to navigate and select items in a list. Items can be widgets themselves, which makes this a great platform for building more sophisticated controls.

Outputlist_view.pylist_view.css

ListViewExample One Two Three

from textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Label, ListItem, ListView\nclass ListViewExample(App):\nCSS_PATH = \"list_view.tcss\"\ndef compose(self) -> ComposeResult:\nyield ListView(\nListItem(Label(\"One\")),\nListItem(Label(\"Two\")),\nListItem(Label(\"Three\")),\n)\nyield Footer()\nif __name__ == \"__main__\":\napp = ListViewExample()\napp.run()\n
\n
"},{"location":"blog/2022/12/11/version-060/#placeholder","title":"Placeholder","text":"

The Placeholder widget was broken since the big CSS update. We've brought it back and given it a bit of a polish.

Use this widget in place of custom widgets you have yet to build when designing your UI. The colors are automatically cycled to differentiate one placeholder from the next. You can click a placeholder to cycle between its ID, size, and lorem ipsum text.

Outputplaceholder.pyplaceholder.css

PlaceholderApp Placeholder\u00a0p2\u00a0here! This\u00a0is\u00a0a\u00a0custom\u00a0label\u00a0for\u00a0p1. #p4 #p3 #p5Placeholder Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0 consectetur\u00a0adipiscing\u00a0elit.\u00a0 Etiam\u00a0feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0 accumsan.\u00a0Suspendisse\u00a0bibendum\u00a0 33\u00a0x\u00a011nec\u00a0libero\u00a0quis\u00a0gravida.\u00a034\u00a0x\u00a011 Phasellus\u00a0id\u00a0eleifend\u00a0ligula.\u00a0 Nullam\u00a0imperdiet\u00a0sem\u00a0tellus,\u00a0 sed\u00a0vehicula\u00a0nisl\u00a0faucibus\u00a0sit\u00a0 amet.\u00a0Praesent\u00a0iaculis\u00a0tempor\u00a0 Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0consectetur\u00a0 adipiscing\u00a0elit.\u00a0Etiam\u00a0feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0 accumsan.\u00a0Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis\u00a0 gravida.\u00a0Phasellus\u00a0id\u00a0eleifend\u00a0ligula.\u00a0Nullam\u00a0 imperdiet\u00a0sem\u00a0tellus,\u00a0sed\u00a0vehicula\u00a0nisl\u00a0faucibus50\u00a0x\u00a011 sit\u00a0amet.\u00a0Praesent\u00a0iaculis\u00a0tempor\u00a0ultricies.\u00a0Sed lacinia,\u00a0tellus\u00a0id\u00a0rutrum\u00a0lacinia,\u00a0sapien\u00a0sapien congue\u00a0mauris,\u00a0sit\u00a0amet\u00a0pellentesque\u00a0quam\u00a0quam\u00a0 vel\u00a0nisl.\u00a0Curabitur\u00a0vulputate\u00a0erat\u00a0pellentesque\u00a0 mauris\u00a0posuere,\u00a0non\u00a0dictum\u00a0risus\u00a0mattis. Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0consectetur\u00a0Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0consectetur\u00a0 adipiscing\u00a0elit.\u00a0Etiam\u00a0feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0adipiscing\u00a0elit.\u00a0Etiam\u00a0feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0 accumsan.\u00a0Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis\u00a0accumsan.\u00a0Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis\u00a0 gravida.\u00a0Phasellus\u00a0id\u00a0eleifend\u00a0ligula.\u00a0Nullam\u00a0gravida.\u00a0Phasellus\u00a0id\u00a0eleifend\u00a0ligula.\u00a0Nullam\u00a0 imperdiet\u00a0sem\u00a0tellus,\u00a0sed\u00a0vehicula\u00a0nisl\u00a0faucibusimperdiet\u00a0sem\u00a0tellus,\u00a0sed\u00a0vehicula\u00a0nisl\u00a0faucibus sit\u00a0amet.\u00a0Praesent\u00a0iaculis\u00a0tempor\u00a0ultricies.\u00a0Sedsit\u00a0amet.\u00a0Praesent\u00a0iaculis\u00a0tempor\u00a0ultricies.\u00a0Sed lacinia,\u00a0tellus\u00a0id\u00a0rutrum\u00a0lacinia,\u00a0sapien\u00a0sapienlacinia,\u00a0tellus\u00a0id\u00a0rutrum\u00a0lacinia,\u00a0sapien\u00a0sapien congue\u00a0mauris,\u00a0sit\u00a0amet\u00a0pellentesque\u00a0quam\u00a0quam\u00a0congue\u00a0mauris,\u00a0sit\u00a0amet\u00a0pellentesque\u00a0quam\u00a0quam\u00a0 vel\u00a0nisl.\u00a0Curabitur\u00a0vulputate\u00a0erat\u00a0pellentesque\u00a0vel\u00a0nisl.\u00a0Curabitur\u00a0vulputate\u00a0erat\u00a0pellentesque\u00a0 mauris\u00a0posuere,\u00a0non\u00a0dictum\u00a0risus\u00a0mattis.mauris\u00a0posuere,\u00a0non\u00a0dictum\u00a0risus\u00a0mattis.

from textual.app import App, ComposeResult\nfrom textual.containers import Container, Horizontal, VerticalScroll\nfrom textual.widgets import Placeholder\nclass PlaceholderApp(App):\nCSS_PATH = \"placeholder.tcss\"\ndef compose(self) -> ComposeResult:\nyield VerticalScroll(\nContainer(\nPlaceholder(\"This is a custom label for p1.\", id=\"p1\"),\nPlaceholder(\"Placeholder p2 here!\", id=\"p2\"),\nPlaceholder(id=\"p3\"),\nPlaceholder(id=\"p4\"),\nPlaceholder(id=\"p5\"),\nPlaceholder(),\nHorizontal(\nPlaceholder(variant=\"size\", id=\"col1\"),\nPlaceholder(variant=\"text\", id=\"col2\"),\nPlaceholder(variant=\"size\", id=\"col3\"),\nid=\"c1\",\n),\nid=\"bot\",\n),\nContainer(\nPlaceholder(variant=\"text\", id=\"left\"),\nPlaceholder(variant=\"size\", id=\"topright\"),\nPlaceholder(variant=\"text\", id=\"botright\"),\nid=\"top\",\n),\nid=\"content\",\n)\nif __name__ == \"__main__\":\napp = PlaceholderApp()\napp.run()\n
\n
"},{"location":"blog/2022/12/11/version-060/#fixes","title":"Fixes","text":"

As always, there are a number of fixes in this release. Mostly related to layout. See CHANGELOG.md for the details.

"},{"location":"blog/2022/12/11/version-060/#whats-next","title":"What's next?","text":"

The next release will focus on pain points we discovered while in a dog-fooding phase (see the DevLog for details on what Textual devs have been building).

"},{"location":"blog/2023/09/15/textual-0370-adds-a-command-palette/","title":"Textual 0.37.0 adds a command palette","text":"

Textual version 0.37.0 has landed! The highlight of this release is the new command palette.

A command palette gives users quick access to features in your app. If you hit ctrl+backslash in a Textual app, it will bring up the command palette where you can start typing commands. The commands are matched with a fuzzy search, so you only need to type two or three characters to get to any command.

Here's a video of it in action:

Adding your own commands to the command palette is a piece of cake. Here's the (command) Provider class used in the example above:

class ColorCommands(Provider):\n\"\"\"A command provider to select colors.\"\"\"\nasync def search(self, query: str) -> Hits:\n\"\"\"Called for each key.\"\"\"\nmatcher = self.matcher(query)\nfor color in COLOR_NAME_TO_RGB.keys():\nscore = matcher.match(color)\nif score > 0:\nyield Hit(\nscore,\nmatcher.highlight(color),\npartial(self.app.post_message, SwitchColor(color)),\n)\n

And here is how you add a provider to your app:

class ColorApp(App):\n\"\"\"Experiment with the command palette.\"\"\"\nCOMMANDS = App.COMMANDS | {ColorCommands}\n

We're excited about this feature because it is a step towards bringing a common user interface to Textual apps.

Quote

It's a Textual app. I know this.

\u2014 You, maybe.

The goal is to be able to build apps that may look quite different, but take no time to learn, because once you learn how to use one Textual app, you can use them all.

See the Guide for details on how to work with the command palette.

"},{"location":"blog/2023/09/15/textual-0370-adds-a-command-palette/#what-else","title":"What else?","text":"

Also in 0.37.0 we have a new Collapsible widget, which is a great way of adding content while avoiding a cluttered screen.

And of course, bug fixes and other updates. See the release page for the full details.

"},{"location":"blog/2023/09/15/textual-0370-adds-a-command-palette/#whats-next","title":"What's next?","text":"

Coming very soon, is a new TextEditor widget. This is a super powerful widget to enter arbitrary text, with beautiful syntax highlighting for a number of languages. We're expecting that to land next week. Watch this space, or join the Discord server if you want to be the first to try it out.

"},{"location":"blog/2023/09/15/textual-0370-adds-a-command-palette/#join-us","title":"Join us","text":"

Join our Discord server if you want to discuss Textual with the Textualize devs, or the community.

"},{"location":"blog/2022/12/07/letting-your-cook-multitask-while-bringing-water-to-a-boil/","title":"Letting your cook multitask while bringing water to a boil","text":"

Whenever you are cooking a time-consuming meal, you want to multitask as much as possible. For example, you do not want to stand still while you wait for a pot of water to start boiling. Similarly, you want your applications to remain responsive (i.e., you want the cook to \u201cmultitask\u201d) while they do some time-consuming operations in the background (e.g., while the water heats up).

The animation below shows an example of an application that remains responsive (colours on the left still change on click) even while doing a bunch of time-consuming operations (shown on the right).

In this blog post, I will teach you how to multitask like a good cook.

"},{"location":"blog/2022/12/07/letting-your-cook-multitask-while-bringing-water-to-a-boil/#wasting-time-staring-at-pots","title":"Wasting time staring at pots","text":"

There is no point in me presenting a solution to a problem if you don't understand the problem I am trying to solve. Suppose we have an application that needs to display a huge amount of data that needs to be read and parsed from a file. The first time I had to do something like this, I ended up writing an application that \u201cblocked\u201d. This means that while the application was reading and parsing the data, nothing else worked.

To exemplify this type of scenario, I created a simple application that spends five seconds preparing some data. After the data is ready, we display a Label on the right that says that the data has been loaded. On the left, the app has a big rectangle (a custom widget called ColourChanger) that you can click and that changes background colours randomly.

When you start the application, you can click the rectangle on the left to change the background colour of the ColourChanger, as the animation below shows:

However, as soon as you press l to trigger the data loading process, clicking the ColourChanger widget doesn't do anything. The app doesn't respond because it is busy working on the data. This is the code of the app so you can try it yourself:

import time\nfrom random import randint\nfrom textual.app import App, ComposeResult\nfrom textual.color import Color\nfrom textual.containers import Grid, VerticalScroll\nfrom textual.widget import Widget\nfrom textual.widgets import Footer, Label\nclass ColourChanger(Widget):  # (1)!\ndef on_click(self) -> None:\nself.styles.background = Color(\nrandint(1, 255),\nrandint(1, 255),\nrandint(1, 255),\n)\nclass MyApp(App[None]):\nBINDINGS = [(\"l\", \"load\", \"Load data\")]  # (2)!\nCSS = \"\"\"\n    Grid {\n        grid-size: 2;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Grid(\nColourChanger(),\nVerticalScroll(id=\"log\"),\n)\nyield Footer()\ndef action_load(self) -> None:  # (3)!\ntime.sleep(5)  # (4)!\nself.query_one(\"#log\").mount(Label(\"Data loaded \u2705\"))\nMyApp().run()\n
  1. The widget ColourChanger changes colours, randomly, when clicked.
  2. We create a binding to the key l that runs an action that we know will take some time (for example, reading and parsing a huge file).
  3. The method action_load is responsible for starting our time-consuming task and then reporting back.
  4. To simplify things a bit, our \u201ctime-consuming task\u201d is just standing still for 5 seconds.

I think it is easy to understand why the widget ColourChanger stops working when we hit the time.sleep call if we consider the cooking analogy I have written about before in my blog. In short, Python behaves like a lone cook in a kitchen:

  • the cook can be clever and multitask. For example, while water is heating up and being brought to a boil, the cook can go ahead and chop some vegetables.
  • however, there is only one cook in the kitchen, so if the cook is chopping up vegetables, they can't be seasoning a salad.

Things like \u201cchopping up vegetables\u201d and \u201cseasoning a salad\u201d are blocking, i.e., they need the cook's time and attention. In the app that I showed above, the call to time.sleep is blocking, so the cook can't go and do anything else until the time interval elapses.

"},{"location":"blog/2022/12/07/letting-your-cook-multitask-while-bringing-water-to-a-boil/#how-can-a-cook-multitask","title":"How can a cook multitask?","text":"

It makes a lot of sense to think that a cook would multitask in their kitchen, but Python isn't like a smart cook. Python is like a very dumb cook who only ever does one thing at a time and waits until each thing is completely done before doing the next thing. So, by default, Python would act like a cook who fills up a pan with water, starts heating the water, and then stands there staring at the water until it starts boiling instead of doing something else. It is by using the module asyncio from the standard library that our cook learns to do other tasks while awaiting the completion of the things they already started doing.

Textual is an async framework, which means it knows how to interoperate with the module asyncio and this will be the solution to our problem. By using asyncio with the tasks we want to run in the background, we will let the application remain responsive while we load and parse the data we need, or while we crunch the numbers we need to crunch, or while we connect to some slow API over the Internet, or whatever it is you want to do.

The module asyncio uses the keyword async to know which functions can be run asynchronously. In other words, you use the keyword async to identify functions that contain tasks that would otherwise force the cook to waste time. (Functions with the keyword async are called coroutines.)

The module asyncio also introduces a function asyncio.create_task that you can use to run coroutines concurrently. So, if we create a coroutine that is in charge of doing the time-consuming operation and then run it with asyncio.create_task, we are well on our way to fix our issues.

However, the keyword async and asyncio.create_task alone aren't enough. Consider this modification of the previous app, where the method action_load now uses asyncio.create_task to run a coroutine who does the sleeping:

import asyncio\nimport time\nfrom random import randint\nfrom textual.app import App, ComposeResult\nfrom textual.color import Color\nfrom textual.containers import Grid, VerticalScroll\nfrom textual.widget import Widget\nfrom textual.widgets import Footer, Label\nclass ColourChanger(Widget):\ndef on_click(self) -> None:\nself.styles.background = Color(\nrandint(1, 255),\nrandint(1, 255),\nrandint(1, 255),\n)\nclass MyApp(App[None]):\nBINDINGS = [(\"l\", \"load\", \"Load data\")]\nCSS = \"\"\"\n    Grid {\n        grid-size: 2;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Grid(\nColourChanger(),\nVerticalScroll(id=\"log\"),\n)\nyield Footer()\ndef action_load(self) -> None:  # (1)!\nasyncio.create_task(self._do_long_operation())  # (2)!\nasync def _do_long_operation(self) -> None:  # (3)!\ntime.sleep(5)\nself.query_one(\"#log\").mount(Label(\"Data loaded \u2705\"))\nMyApp().run()\n
  1. The action method action_load now defers the heavy lifting to another method we created.
  2. The time-consuming operation can be run concurrently with asyncio.create_task because it is a coroutine.
  3. The method _do_long_operation has the keyword async, so it is a coroutine.

This modified app also works but it suffers from the same issue as the one before! The keyword async tells Python that there will be things inside that function that can be awaited by the cook. That is, the function will do some time-consuming operation that doesn't require the cook's attention. However, we need to tell Python which time-consuming operation doesn't require the cook's attention, i.e., which time-consuming operation can be awaited, with the keyword await.

Whenever we want to use the keyword await, we need to do it with objects that are compatible with it. For many things, that means using specialised libraries:

  • instead of time.sleep, one can use await asyncio.sleep;
  • instead of the module requests to make Internet requests, use aiohttp; or
  • instead of using the built-in tools to read files, use aiofiles.
"},{"location":"blog/2022/12/07/letting-your-cook-multitask-while-bringing-water-to-a-boil/#achieving-good-multitasking","title":"Achieving good multitasking","text":"

To fix the last example application, all we need to do is replace the call to time.sleep with a call to asyncio.sleep and then use the keyword await to signal Python that we can be doing something else while we sleep. The animation below shows that we can still change colours while the application is completing the time-consuming operation.

CodeAnimation
import asyncio\nimport time\nfrom random import randint\nfrom textual.app import App, ComposeResult\nfrom textual.color import Color\nfrom textual.containers import Grid, VerticalScroll\nfrom textual.widget import Widget\nfrom textual.widgets import Footer, Label\nclass ColourChanger(Widget):\ndef on_click(self) -> None:\nself.styles.background = Color(\nrandint(1, 255),\nrandint(1, 255),\nrandint(1, 255),\n)\nclass MyApp(App[None]):\nBINDINGS = [(\"l\", \"load\", \"Load data\")]\nCSS = \"\"\"\n    Grid {\n        grid-size: 2;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Grid(\nColourChanger(),\nVerticalScroll(id=\"log\"),\n)\nyield Footer()\ndef action_load(self) -> None:\nasyncio.create_task(self._do_long_operation())\nasync def _do_long_operation(self) -> None:\nself.query_one(\"#log\").mount(Label(\"Starting \u23f3\"))  # (1)!\nawait asyncio.sleep(5)  # (2)!\nself.query_one(\"#log\").mount(Label(\"Data loaded \u2705\"))  # (3)!\nMyApp().run()\n
  1. We create a label that tells the user that we are starting our time-consuming operation.
  2. We await the time-consuming operation so that the application remains responsive.
  3. We create a label that tells the user that the time-consuming operation has been concluded.

Because our time-consuming operation runs concurrently, everything else in the application still works while we await for the time-consuming operation to finish. In particular, we can keep changing colours (like the animation above showed) but we can also keep activating the binding with the key l to start multiple instances of the same time-consuming operation! The animation below shows just this:

Warning

The animation GIFs in this blog post show low-quality colours in an attempt to reduce the size of the media files you have to download to be able to read this blog post. If you run Textual locally you will see beautiful colours \u2728

"},{"location":"blog/2023/07/27/using-rich-inspect-to-interrogate-python-objects/","title":"Using Rich Inspect to interrogate Python objects","text":"

The Rich library has a few functions that are admittedly a little out of scope for a terminal color library. One such function is inspect which is so useful you may want to pip install rich just for this feature.

The easiest way to describe inspect is that it is Python's builtin help() but easier on the eye (and with a few more features). If you invoke it with any object, inspect will display a nicely formatted report on that object \u2014 which makes it great for interrogating objects from the REPL. Here's an example:

>>> from rich import inspect\n>>> text_file = open(\"foo.txt\", \"w\")\n>>> inspect(text_file)\n

Here we're inspecting a file object, but it could be literally anything. You will see the following output in the terminal:

Rich \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<class'_io.TextIOWrapper'>\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502Character\u00a0and\u00a0line\u00a0based\u00a0layer\u00a0over\u00a0a\u00a0BufferedIOBase\u00a0object,\u00a0buffer.\u2502 \u2502\u2502 \u2502\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u2502 \u2502\u2502<_io.TextIOWrappername='foo.txt'mode='w'encoding='UTF-8'>\u2502\u2502 \u2502\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2502 \u2502\u2502 \u2502buffer\u00a0=<_io.BufferedWritername='foo.txt'>\u2502 \u2502closed\u00a0=False\u2502 \u2502encoding\u00a0='UTF-8'\u2502 \u2502errors\u00a0='strict'\u2502 \u2502line_buffering\u00a0=False\u2502 \u2502mode\u00a0='w'\u2502 \u2502name\u00a0='foo.txt'\u2502 \u2502newlines\u00a0=None\u2502 \u2502write_through\u00a0=False\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

By default, inspect will generate a data-oriented summary with a text representation of the object and its data attributes. You can also add methods=True to show all the methods in the public API. Here's an example:

>>> inspect(text_file, methods=True)\n
Rich \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<class'_io.TextIOWrapper'>\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502Character\u00a0and\u00a0line\u00a0based\u00a0layer\u00a0over\u00a0a\u00a0BufferedIOBase\u00a0object,\u00a0buffer.\u2502 \u2502\u2502 \u2502\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u2502 \u2502\u2502<_io.TextIOWrappername='foo.txt'mode='w'encoding='UTF-8'>\u2502\u2502 \u2502\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2502 \u2502\u2502 \u2502buffer\u00a0=<_io.BufferedWritername='foo.txt'>\u2502 \u2502closed\u00a0=False\u2502 \u2502encoding\u00a0='UTF-8'\u2502 \u2502errors\u00a0='strict'\u2502 \u2502line_buffering\u00a0=False\u2502 \u2502mode\u00a0='w'\u2502 \u2502name\u00a0='foo.txt'\u2502 \u2502newlines\u00a0=None\u2502 \u2502write_through\u00a0=False\u2502 \u2502close\u00a0=def\u00a0close():Flush\u00a0and\u00a0close\u00a0the\u00a0IO\u00a0object.\u2502 \u2502detach\u00a0=def\u00a0detach():Separate\u00a0the\u00a0underlying\u00a0buffer\u00a0from\u00a0the\u00a0TextIOBase\u00a0and\u00a0return\u00a0it.\u2502 \u2502fileno\u00a0=def\u00a0fileno():Returns\u00a0underlying\u00a0file\u00a0descriptor\u00a0if\u00a0one\u00a0exists.\u2502 \u2502flush\u00a0=def\u00a0flush():Flush\u00a0write\u00a0buffers,\u00a0if\u00a0applicable.\u2502 \u2502isatty\u00a0=def\u00a0isatty():Return\u00a0whether\u00a0this\u00a0is\u00a0an\u00a0'interactive'\u00a0stream.\u2502 \u2502read\u00a0=def\u00a0read(size=-1,\u00a0/):Read\u00a0at\u00a0most\u00a0n\u00a0characters\u00a0from\u00a0stream.\u2502 \u2502readable\u00a0=def\u00a0readable():Return\u00a0whether\u00a0object\u00a0was\u00a0opened\u00a0for\u00a0reading.\u2502 \u2502readline\u00a0=def\u00a0readline(size=-1,\u00a0/):Read\u00a0until\u00a0newline\u00a0or\u00a0EOF.\u2502 \u2502readlines\u00a0=def\u00a0readlines(hint=-1,\u00a0/):Return\u00a0a\u00a0list\u00a0of\u00a0lines\u00a0from\u00a0the\u00a0stream.\u2502 \u2502reconfigure\u00a0=def\u00a0reconfigure(*,\u00a0encoding=None,\u00a0errors=None,\u00a0newline=None,\u00a0line_buffering=None,\u00a0\u2502 \u2502write_through=None):Reconfigure\u00a0the\u00a0text\u00a0stream\u00a0with\u00a0new\u00a0parameters.\u2502 \u2502seek\u00a0=def\u00a0seek(cookie,\u00a0whence=0,\u00a0/):Change\u00a0stream\u00a0position.\u2502 \u2502seekable\u00a0=def\u00a0seekable():Return\u00a0whether\u00a0object\u00a0supports\u00a0random\u00a0access.\u2502 \u2502tell\u00a0=def\u00a0tell():Return\u00a0current\u00a0stream\u00a0position.\u2502 \u2502truncate\u00a0=def\u00a0truncate(pos=None,\u00a0/):Truncate\u00a0file\u00a0to\u00a0size\u00a0bytes.\u2502 \u2502writable\u00a0=def\u00a0writable():Return\u00a0whether\u00a0object\u00a0was\u00a0opened\u00a0for\u00a0writing.\u2502 \u2502write\u00a0=def\u00a0write(text,\u00a0/):\u2502 \u2502Write\u00a0string\u00a0to\u00a0stream.\u2502 \u2502Returns\u00a0the\u00a0number\u00a0of\u00a0characters\u00a0written\u00a0(which\u00a0is\u00a0always\u00a0equal\u00a0to\u2502 \u2502the\u00a0length\u00a0of\u00a0the\u00a0string).\u2502 \u2502writelines\u00a0=def\u00a0writelines(lines,\u00a0/):Write\u00a0a\u00a0list\u00a0of\u00a0lines\u00a0to\u00a0stream.\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

The documentation is summarized by default to avoid generating verbose reports. If you want to see the full unabbreviated help you can add help=True:

>>> inspect(text_file, methods=True, help=True)\n
Rich \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<class'_io.TextIOWrapper'>\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502Character\u00a0and\u00a0line\u00a0based\u00a0layer\u00a0over\u00a0a\u00a0BufferedIOBase\u00a0object,\u00a0buffer.\u2502 \u2502\u2502 \u2502encoding\u00a0gives\u00a0the\u00a0name\u00a0of\u00a0the\u00a0encoding\u00a0that\u00a0the\u00a0stream\u00a0will\u00a0be\u2502 \u2502decoded\u00a0or\u00a0encoded\u00a0with.\u00a0It\u00a0defaults\u00a0to\u00a0locale.getencoding().\u2502 \u2502\u2502 \u2502errors\u00a0determines\u00a0the\u00a0strictness\u00a0of\u00a0encoding\u00a0and\u00a0decoding\u00a0(see\u2502 \u2502help(codecs.Codec)\u00a0or\u00a0the\u00a0documentation\u00a0for\u00a0codecs.register)\u00a0and\u2502 \u2502defaults\u00a0to\u00a0\"strict\".\u2502 \u2502\u2502 \u2502newline\u00a0controls\u00a0how\u00a0line\u00a0endings\u00a0are\u00a0handled.\u00a0It\u00a0can\u00a0be\u00a0None,\u00a0'',\u2502 \u2502'\\n',\u00a0'\\r',\u00a0and\u00a0'\\r\\n'.\u00a0\u00a0It\u00a0works\u00a0as\u00a0follows:\u2502 \u2502\u2502 \u2502*\u00a0On\u00a0input,\u00a0if\u00a0newline\u00a0is\u00a0None,\u00a0universal\u00a0newlines\u00a0mode\u00a0is\u2502 \u2502\u00a0\u00a0enabled.\u00a0Lines\u00a0in\u00a0the\u00a0input\u00a0can\u00a0end\u00a0in\u00a0'\\n',\u00a0'\\r',\u00a0or\u00a0'\\r\\n',\u00a0and\u2502 \u2502\u00a0\u00a0these\u00a0are\u00a0translated\u00a0into\u00a0'\\n'\u00a0before\u00a0being\u00a0returned\u00a0to\u00a0the\u2502 \u2502\u00a0\u00a0caller.\u00a0If\u00a0it\u00a0is\u00a0'',\u00a0universal\u00a0newline\u00a0mode\u00a0is\u00a0enabled,\u00a0but\u00a0line\u2502 \u2502\u00a0\u00a0endings\u00a0are\u00a0returned\u00a0to\u00a0the\u00a0caller\u00a0untranslated.\u00a0If\u00a0it\u00a0has\u00a0any\u00a0of\u2502 \u2502\u00a0\u00a0the\u00a0other\u00a0legal\u00a0values,\u00a0input\u00a0lines\u00a0are\u00a0only\u00a0terminated\u00a0by\u00a0the\u00a0given\u2502 \u2502\u00a0\u00a0string,\u00a0and\u00a0the\u00a0line\u00a0ending\u00a0is\u00a0returned\u00a0to\u00a0the\u00a0caller\u00a0untranslated.\u2502 \u2502\u2502 \u2502*\u00a0On\u00a0output,\u00a0if\u00a0newline\u00a0is\u00a0None,\u00a0any\u00a0'\\n'\u00a0characters\u00a0written\u00a0are\u2502 \u2502\u00a0\u00a0translated\u00a0to\u00a0the\u00a0system\u00a0default\u00a0line\u00a0separator,\u00a0os.linesep.\u00a0If\u2502 \u2502\u00a0\u00a0newline\u00a0is\u00a0''\u00a0or\u00a0'\\n',\u00a0no\u00a0translation\u00a0takes\u00a0place.\u00a0If\u00a0newline\u00a0is\u00a0any\u2502 \u2502\u00a0\u00a0of\u00a0the\u00a0other\u00a0legal\u00a0values,\u00a0any\u00a0'\\n'\u00a0characters\u00a0written\u00a0are\u00a0translated\u2502 \u2502\u00a0\u00a0to\u00a0the\u00a0given\u00a0string.\u2502 \u2502\u2502 \u2502If\u00a0line_buffering\u00a0is\u00a0True,\u00a0a\u00a0call\u00a0to\u00a0flush\u00a0is\u00a0implied\u00a0when\u00a0a\u00a0call\u00a0to\u2502 \u2502write\u00a0contains\u00a0a\u00a0newline\u00a0character.\u2502 \u2502\u2502 \u2502\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u2502 \u2502\u2502<_io.TextIOWrappername='foo.txt'mode='w'encoding='UTF-8'>\u2502\u2502 \u2502\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2502 \u2502\u2502 \u2502buffer\u00a0=<_io.BufferedWritername='foo.txt'>\u2502 \u2502closed\u00a0=False\u2502 \u2502encoding\u00a0='UTF-8'\u2502 \u2502errors\u00a0='strict'\u2502 \u2502line_buffering\u00a0=False\u2502 \u2502mode\u00a0='w'\u2502 \u2502name\u00a0='foo.txt'\u2502 \u2502newlines\u00a0=None\u2502 \u2502write_through\u00a0=False\u2502 \u2502close\u00a0=def\u00a0close():\u2502 \u2502Flush\u00a0and\u00a0close\u00a0the\u00a0IO\u00a0object.\u2502 \u2502\u2502 \u2502This\u00a0method\u00a0has\u00a0no\u00a0effect\u00a0if\u00a0the\u00a0file\u00a0is\u00a0already\u00a0closed.\u2502 \u2502detach\u00a0=def\u00a0detach():\u2502 \u2502Separate\u00a0the\u00a0underlying\u00a0buffer\u00a0from\u00a0the\u00a0TextIOBase\u00a0and\u00a0return\u00a0it.\u2502 \u2502\u2502 \u2502After\u00a0the\u00a0underlying\u00a0buffer\u00a0has\u00a0been\u00a0detached,\u00a0the\u00a0TextIO\u00a0is\u00a0in\u00a0an\u2502 \u2502unusable\u00a0state.\u2502 \u2502fileno\u00a0=def\u00a0fileno():\u2502 \u2502Returns\u00a0underlying\u00a0file\u00a0descriptor\u00a0if\u00a0one\u00a0exists.\u2502 \u2502\u2502 \u2502OSError\u00a0is\u00a0raised\u00a0if\u00a0the\u00a0IO\u00a0object\u00a0does\u00a0not\u00a0use\u00a0a\u00a0file\u00a0descriptor.\u2502 \u2502flush\u00a0=def\u00a0flush():\u2502 \u2502Flush\u00a0write\u00a0buffers,\u00a0if\u00a0applicable.\u2502 \u2502\u2502 \u2502This\u00a0is\u00a0not\u00a0implemented\u00a0for\u00a0read-only\u00a0and\u00a0non-blocking\u00a0streams.\u2502 \u2502isatty\u00a0=def\u00a0isatty():\u2502 \u2502Return\u00a0whether\u00a0this\u00a0is\u00a0an\u00a0'interactive'\u00a0stream.\u2502 \u2502\u2502 \u2502Return\u00a0False\u00a0if\u00a0it\u00a0can't\u00a0be\u00a0determined.\u2502 \u2502read\u00a0=def\u00a0read(size=-1,\u00a0/):\u2502 \u2502Read\u00a0at\u00a0most\u00a0n\u00a0characters\u00a0from\u00a0stream.\u2502 \u2502\u2502 \u2502Read\u00a0from\u00a0underlying\u00a0buffer\u00a0until\u00a0we\u00a0have\u00a0n\u00a0characters\u00a0or\u00a0we\u00a0hit\u00a0EOF.\u2502 \u2502If\u00a0n\u00a0is\u00a0negative\u00a0or\u00a0omitted,\u00a0read\u00a0until\u00a0EOF.\u2502 \u2502readable\u00a0=def\u00a0readable():\u2502 \u2502Return\u00a0whether\u00a0object\u00a0was\u00a0opened\u00a0for\u00a0reading.\u2502 \u2502\u2502 \u2502If\u00a0False,\u00a0read()\u00a0will\u00a0raise\u00a0OSError.\u2502 \u2502readline\u00a0=def\u00a0readline(size=-1,\u00a0/):\u2502 \u2502Read\u00a0until\u00a0newline\u00a0or\u00a0EOF.\u2502 \u2502\u2502 \u2502Returns\u00a0an\u00a0empty\u00a0string\u00a0if\u00a0EOF\u00a0is\u00a0hit\u00a0immediately.\u2502 \u2502readlines\u00a0=def\u00a0readlines(hint=-1,\u00a0/):\u2502 \u2502Return\u00a0a\u00a0list\u00a0of\u00a0lines\u00a0from\u00a0the\u00a0stream.\u2502 \u2502\u2502 \u2502hint\u00a0can\u00a0be\u00a0specified\u00a0to\u00a0control\u00a0the\u00a0number\u00a0of\u00a0lines\u00a0read:\u00a0no\u00a0more\u2502 \u2502lines\u00a0will\u00a0be\u00a0read\u00a0if\u00a0the\u00a0total\u00a0size\u00a0(in\u00a0bytes/characters)\u00a0of\u00a0all\u2502 \u2502lines\u00a0so\u00a0far\u00a0exceeds\u00a0hint.\u2502 \u2502reconfigure\u00a0=def\u00a0reconfigure(*,\u00a0encoding=None,\u00a0errors=None,\u00a0newline=None,\u00a0line_buffering=None,\u00a0\u2502 \u2502write_through=None):\u2502 \u2502Reconfigure\u00a0the\u00a0text\u00a0stream\u00a0with\u00a0new\u00a0parameters.\u2502 \u2502\u2502 \u2502This\u00a0also\u00a0does\u00a0an\u00a0implicit\u00a0stream\u00a0flush.\u2502 \u2502seek\u00a0=def\u00a0seek(cookie,\u00a0whence=0,\u00a0/):\u2502 \u2502Change\u00a0stream\u00a0position.\u2502 \u2502\u2502 \u2502Change\u00a0the\u00a0stream\u00a0position\u00a0to\u00a0the\u00a0given\u00a0byte\u00a0offset.\u00a0The\u00a0offset\u00a0is\u2502 \u2502interpreted\u00a0relative\u00a0to\u00a0the\u00a0position\u00a0indicated\u00a0by\u00a0whence.\u00a0\u00a0Values\u2502 \u2502for\u00a0whence\u00a0are:\u2502 \u2502\u2502 \u2502*\u00a00\u00a0--\u00a0start\u00a0of\u00a0stream\u00a0(the\u00a0default);\u00a0offset\u00a0should\u00a0be\u00a0zero\u00a0or\u00a0positive\u2502 \u2502*\u00a01\u00a0--\u00a0current\u00a0stream\u00a0position;\u00a0offset\u00a0may\u00a0be\u00a0negative\u2502 \u2502*\u00a02\u00a0--\u00a0end\u00a0of\u00a0stream;\u00a0offset\u00a0is\u00a0usually\u00a0negative\u2502 \u2502\u2502 \u2502Return\u00a0the\u00a0new\u00a0absolute\u00a0position.\u2502 \u2502seekable\u00a0=def\u00a0seekable():\u2502 \u2502Return\u00a0whether\u00a0object\u00a0supports\u00a0random\u00a0access.\u2502 \u2502\u2502 \u2502If\u00a0False,\u00a0seek(),\u00a0tell()\u00a0and\u00a0truncate()\u00a0will\u00a0raise\u00a0OSError.\u2502 \u2502This\u00a0method\u00a0may\u00a0need\u00a0to\u00a0do\u00a0a\u00a0test\u00a0seek().\u2502 \u2502tell\u00a0=def\u00a0tell():Return\u00a0current\u00a0stream\u00a0position.\u2502 \u2502truncate\u00a0=def\u00a0truncate(pos=None,\u00a0/):\u2502 \u2502Truncate\u00a0file\u00a0to\u00a0size\u00a0bytes.\u2502 \u2502\u2502 \u2502File\u00a0pointer\u00a0is\u00a0left\u00a0unchanged.\u00a0\u00a0Size\u00a0defaults\u00a0to\u00a0the\u00a0current\u00a0IO\u2502 \u2502position\u00a0as\u00a0reported\u00a0by\u00a0tell().\u00a0\u00a0Returns\u00a0the\u00a0new\u00a0size.\u2502 \u2502writable\u00a0=def\u00a0writable():\u2502 \u2502Return\u00a0whether\u00a0object\u00a0was\u00a0opened\u00a0for\u00a0writing.\u2502 \u2502\u2502 \u2502If\u00a0False,\u00a0write()\u00a0will\u00a0raise\u00a0OSError.\u2502 \u2502write\u00a0=def\u00a0write(text,\u00a0/):\u2502 \u2502Write\u00a0string\u00a0to\u00a0stream.\u2502 \u2502Returns\u00a0the\u00a0number\u00a0of\u00a0characters\u00a0written\u00a0(which\u00a0is\u00a0always\u00a0equal\u00a0to\u2502 \u2502the\u00a0length\u00a0of\u00a0the\u00a0string).\u2502 \u2502writelines\u00a0=def\u00a0writelines(lines,\u00a0/):\u2502 \u2502Write\u00a0a\u00a0list\u00a0of\u00a0lines\u00a0to\u00a0stream.\u2502 \u2502\u2502 \u2502Line\u00a0separators\u00a0are\u00a0not\u00a0added,\u00a0so\u00a0it\u00a0is\u00a0usual\u00a0for\u00a0each\u00a0of\u00a0the\u2502 \u2502lines\u00a0provided\u00a0to\u00a0have\u00a0a\u00a0line\u00a0separator\u00a0at\u00a0the\u00a0end.\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

There are a few more arguments to refine the level of detail you need (private methods, dunder attributes etc). You can see the full range of options with this delightful little incantation:

>>> inspect(inspect)\n

If you are interested in Rich or Textual, join our Discord server!

"},{"location":"blog/2023/07/27/using-rich-inspect-to-interrogate-python-objects/#addendum","title":"Addendum","text":"

Here's how to have inspect always available without an explicit import:

Put this in your pythonrc file: pic.twitter.com/pXTi69ykZL

\u2014 Tushar Sadhwani (@sadhlife) July 27, 2023"},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/","title":"Spinners and progress bars in Textual","text":"

One of the things I love about mathematics is that you can solve a problem just by guessing the correct answer. That is a perfectly valid strategy for solving a problem. The only thing you need to do after guessing the answer is to prove that your guess is correct.

I used this strategy, to some success, to display spinners and indeterminate progress bars from Rich in Textual.

"},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/#display-an-indeterminate-progress-bar-in-textual","title":"Display an indeterminate progress bar in Textual","text":"

I have been playing around with Textual and recently I decided I needed an indeterminate progress bar to show that some data was loading. Textual is likely to get progress bars in the future, but I don't want to wait for the future! I want my progress bars now! Textual builds on top of Rich, so if Rich has progress bars, I reckoned I could use them in my Textual apps.

"},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/#progress-bars-in-rich","title":"Progress bars in Rich","text":"

Creating a progress bar in Rich is as easy as opening up the documentation for Progress and copying & pasting the code.

CodeOutput
import time\nfrom rich.progress import track\nfor _ in track(range(20), description=\"Processing...\"):\ntime.sleep(0.5)  # Simulate work being done\n

The function track provides a very convenient interface for creating progress bars that keep track of a well-specified number of steps. In the example above, we were keeping track of some task that was going to take 20 steps to complete. (For example, if we had to process a list with 20 elements.) However, I am looking for indeterminate progress bars.

Scrolling further down the documentation for rich.progress I found what I was looking for:

CodeOutput
import time\nfrom rich.progress import Progress\nwith Progress() as progress:\n_ = progress.add_task(\"Loading...\", total=None)  # (1)!\nwhile True:\ntime.sleep(0.01)\n
  1. Setting total=None is what makes it an indeterminate progress bar.

So, putting an indeterminate progress bar on the screen is easy. Now, I only needed to glue that together with the little I know about Textual to put an indeterminate progress bar in a Textual app.

"},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/#guessing-what-is-what-and-what-goes-where","title":"Guessing what is what and what goes where","text":"

What I want is to have an indeterminate progress bar inside my Textual app. Something that looks like this:

The GIF above shows just the progress bar. Obviously, the end goal is to have the progress bar be part of a Textual app that does something.

So, when I set out to do this, my first thought went to the stopwatch app in the Textual tutorial because it has a widget that updates automatically, the TimeDisplay. Below you can find the essential part of the code for the TimeDisplay widget and a small animation of it updating when the stopwatch is started.

TimeDisplay widgetOutput
from time import monotonic\nfrom textual.reactive import reactive\nfrom textual.widgets import Static\nclass TimeDisplay(Static):\n\"\"\"A widget to display elapsed time.\"\"\"\nstart_time = reactive(monotonic)\ntime = reactive(0.0)\ntotal = reactive(0.0)\ndef on_mount(self) -> None:\n\"\"\"Event handler called when widget is added to the app.\"\"\"\nself.update_timer = self.set_interval(1 / 60, self.update_time, pause=True)\ndef update_time(self) -> None:\n\"\"\"Method to update time to current.\"\"\"\nself.time = self.total + (monotonic() - self.start_time)\ndef watch_time(self, time: float) -> None:\n\"\"\"Called when the time attribute changes.\"\"\"\nminutes, seconds = divmod(time, 60)\nhours, minutes = divmod(minutes, 60)\nself.update(f\"{hours:02,.0f}:{minutes:02.0f}:{seconds:05.2f}\")\n

The reason the time display updates magically is due to the three methods that I highlighted in the code above:

  1. The method on_mount is called when the TimeDisplay widget is mounted on the app and, in it, we use the method set_interval to let Textual know that every 1 / 60 seconds we would like to call the method update_time. (In other words, we would like update_time to be called 60 times per second.)
  2. In turn, the method update_time (which is called automatically a bunch of times per second) will update the reactive attribute time. When this attribute update happens, the method watch_time kicks in.
  3. The method watch_time is a watcher method and gets called whenever the attribute self.time is assigned to. So, if the method update_time is called a bunch of times per second, the watcher method watch_time is also called a bunch of times per second. In it, we create a nice representation of the time that has elapsed and we use the method update to update the time that is being displayed.

I thought it would be reasonable if a similar mechanism needed to be in place for my progress bar, but then I realised that the progress bar seems to update itself... Looking at the indeterminate progress bar example from before, the only thing going on was that we used time.sleep to stop our program for a bit. We didn't do anything to update the progress bar... Look:

with Progress() as progress:\n_ = progress.add_task(\"Loading...\", total=None)  # (1)!\nwhile True:\ntime.sleep(0.01)\n

After pondering about this for a bit, I realised I would not need a watcher method for anything. The watcher method would only make sense if I needed to update an attribute related to some sort of artificial progress, but that clearly isn't needed to get the bar going...

At some point, I realised that the object progress is the object of interest. At first, I thought progress.add_task would return the progress bar, but it actually returns the integer ID of the task added, so the object of interest is progress. Because I am doing nothing to update the bar explicitly, the object progress must be updating itself.

The Textual documentation also says that we can build widgets from Rich renderables, so I concluded that if Progress were a renderable, then I could inherit from Static and use the method update to update the widget with my instance of Progress directly. I gave it a try and I put together this code:

from rich.progress import Progress, BarColumn\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass IndeterminateProgress(Static):\ndef __init__(self):\nsuper().__init__(\"\")\nself._bar = Progress(BarColumn())  # (1)!\nself._bar.add_task(\"\", total=None)  # (2)!\ndef on_mount(self) -> None:\n# When the widget is mounted start updating the display regularly.\nself.update_render = self.set_interval(\n1 / 60, self.update_progress_bar\n)  # (3)!\ndef update_progress_bar(self) -> None:\nself.update(self._bar)  # (4)!\nclass MyApp(App):\ndef compose(self) -> ComposeResult:\nyield IndeterminateProgress()\nif __name__ == \"__main__\":\napp = MyApp()\napp.run()\n
  1. Create an instance of Progress that just cares about the bar itself (Rich progress bars can have a label, an indicator for the time left, etc).
  2. We add the indeterminate task with total=None for the indeterminate progress bar.
  3. When the widget is mounted on the app, we want to start calling update_progress_bar 60 times per second.
  4. To update the widget of the progress bar we just call the method Static.update with the Progress object because self._bar is a Rich renderable.

And lo and behold, it worked:

"},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/#proving-it-works","title":"Proving it works","text":"

I finished writing this piece of code and I was ecstatic because it was working! After all, my Textual app starts and renders the progress bar. And so, I shared this simple app with someone who wanted to do a similar thing, but I was left with a bad taste in my mouth because I couldn't really connect all the dots and explain exactly why it worked.

Plot twist

By the end of the blog post, I will be much closer to a full explanation!

"},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/#display-a-rich-spinner-in-a-textual-app","title":"Display a Rich spinner in a Textual app","text":"

A day after creating my basic IndeterminateProgress widget, I found someone that was trying to display a Rich spinner in a Textual app. Actually, it was someone that had filed an issue against Rich. They didn't ask \u201chow can I display a Rich spinner in a Textual app?\u201d, but they filed an alleged bug that crept up on them when they tried displaying a spinner in a Textual app.

When reading the issue I realised that displaying a Rich spinner looked very similar to displaying a Rich progress bar, so I made a tiny change to my code and tried to run it:

CodeSpinner running
from rich.spinner import Spinner\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass SpinnerWidget(Static):\ndef __init__(self):\nsuper().__init__(\"\")\nself._spinner = Spinner(\"moon\")  # (1)!\ndef on_mount(self) -> None:\nself.update_render = self.set_interval(1 / 60, self.update_spinner)\ndef update_spinner(self) -> None:\nself.update(self._spinner)\nclass MyApp(App[None]):\ndef compose(self) -> ComposeResult:\nyield SpinnerWidget()\nMyApp().run()\n
  1. Instead of creating an instance of Progress, we create an instance of Spinner and save it so we can call self.update(self._spinner) later on.

"},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/#losing-the-battle-against-pausing-the-animations","title":"Losing the battle against pausing the animations","text":"

After creating the progress bar and spinner widgets I thought of creating the little display that was shown at the beginning of the blog post:

When writing the code for this app, I realised both widgets had a lot of shared code and logic and I tried abstracting away their common functionality. That led to the code shown below (more or less) where I implemented the updating functionality in IntervalUpdater and then let the IndeterminateProgressBar and SpinnerWidget instantiate the correct Rich renderable.

from rich.progress import Progress, BarColumn\nfrom rich.spinner import Spinner\nfrom textual.app import RenderableType\nfrom textual.widgets import Button, Static\nclass IntervalUpdater(Static):\n_renderable_object: RenderableType  # (1)!\ndef update_rendering(self) -> None:  # (2)!\nself.update(self._renderable_object)\ndef on_mount(self) -> None:  # (3)!\nself.interval_update = self.set_interval(1 / 60, self.update_rendering)\nclass IndeterminateProgressBar(IntervalUpdater):\n\"\"\"Basic indeterminate progress bar widget based on rich.progress.Progress.\"\"\"\ndef __init__(self) -> None:\nsuper().__init__(\"\")\nself._renderable_object = Progress(BarColumn())  # (4)!\nself._renderable_object.add_task(\"\", total=None)\nclass SpinnerWidget(IntervalUpdater):\n\"\"\"Basic spinner widget based on rich.spinner.Spinner.\"\"\"\ndef __init__(self, style: str) -> None:\nsuper().__init__(\"\")\nself._renderable_object = Spinner(style)  # (5)!\n
  1. Instances of IntervalUpdate should set the attribute _renderable_object to the instance of the Rich renderable that we want to animate.
  2. The methods update_rendering and on_mount are exactly the same as what we had before, both in the progress bar widget and in the spinner widget.
  3. The methods update_rendering and on_mount are exactly the same as what we had before, both in the progress bar widget and in the spinner widget.
  4. For an indeterminate progress bar we set the attribute _renderable_object to an instance of Progress.
  5. For a spinner we set the attribute _renderable_object to an instance of Spinner.

But I wanted something more! I wanted to make my app similar to the stopwatch app from the terminal and thus wanted to add a \u201cPause\u201d and a \u201cResume\u201d button. These buttons should, respectively, stop the progress bar and the spinner animations and resume them.

Below you can see the code I wrote and a short animation of the app working.

App codeCSSOutput
from rich.progress import Progress, BarColumn\nfrom rich.spinner import Spinner\nfrom textual.app import App, ComposeResult, RenderableType\nfrom textual.containers import Grid, Horizontal, Vertical\nfrom textual.widgets import Button, Static\nclass IntervalUpdater(Static):\n_renderable_object: RenderableType\ndef update_rendering(self) -> None:\nself.update(self._renderable_object)\ndef on_mount(self) -> None:\nself.interval_update = self.set_interval(1 / 60, self.update_rendering)\ndef pause(self) -> None:  # (1)!\nself.interval_update.pause()\ndef resume(self) -> None:  # (2)!\nself.interval_update.resume()\nclass IndeterminateProgressBar(IntervalUpdater):\n\"\"\"Basic indeterminate progress bar widget based on rich.progress.Progress.\"\"\"\ndef __init__(self) -> None:\nsuper().__init__(\"\")\nself._renderable_object = Progress(BarColumn())\nself._renderable_object.add_task(\"\", total=None)\nclass SpinnerWidget(IntervalUpdater):\n\"\"\"Basic spinner widget based on rich.spinner.Spinner.\"\"\"\ndef __init__(self, style: str) -> None:\nsuper().__init__(\"\")\nself._renderable_object = Spinner(style)\nclass LiveDisplayApp(App[None]):\n\"\"\"App showcasing some widgets that update regularly.\"\"\"\nCSS_PATH = \"myapp.css\"\ndef compose(self) -> ComposeResult:\nyield Vertical(\nGrid(\nSpinnerWidget(\"moon\"),\nIndeterminateProgressBar(),\nSpinnerWidget(\"aesthetic\"),\nSpinnerWidget(\"bouncingBar\"),\nSpinnerWidget(\"earth\"),\nSpinnerWidget(\"dots8Bit\"),\n),\nHorizontal(\nButton(\"Pause\", id=\"pause\"),  # (3)!\nButton(\"Resume\", id=\"resume\", disabled=True),\n),\n)\ndef on_button_pressed(self, event: Button.Pressed) -> None:  # (4)!\npressed_id = event.button.id\nassert pressed_id is not None\nfor widget in self.query(IntervalUpdater):\ngetattr(widget, pressed_id)()  # (5)!\nfor button in self.query(Button):  # (6)!\nif button.id == pressed_id:\nbutton.disabled = True\nelse:\nbutton.disabled = False\nLiveDisplayApp().run()\n
  1. The method pause looks at the attribute interval_update (returned by the method set_interval) and tells it to stop calling the method update_rendering 60 times per second.
  2. The method resume looks at the attribute interval_update (returned by the method set_interval) and tells it to resume calling the method update_rendering 60 times per second.
  3. We set two distinct IDs for the two buttons so we can easily tell which button was pressed and what the press of that button means.
  4. The event handler on_button_pressed will wait for button presses and will take care of pausing or resuming the animations.
  5. We look for all of the instances of IntervalUpdater in our app and use a little bit of introspection to call the correct method (pause or resume) in our widgets. Notice this was only possible because the buttons were assigned IDs that matched the names of the methods. (I love Python !)
  6. We go through our two buttons to disable the one that was just pressed and to enable the other one.
Screen {\nalign: center middle;\n}\nHorizontal {\nheight: 1fr;\nalign-horizontal: center;\n}\nButton {\nmargin: 0 3 0 3;\n}\nGrid {\nheight: 4fr;\nalign: center middle;\ngrid-size: 3 2;\ngrid-columns: 8;\ngrid-rows: 1;\ngrid-gutter: 1;\nborder: gray double;\n}\nIntervalUpdater {\ncontent-align: center middle;\n}\n

If you think this was a lot, take a couple of deep breaths before moving on.

The only issue with my app is that... it does not work! If you press the button to pause the animations, it looks like the widgets are paused. However, you can see that if I move my mouse over the paused widgets, they update:

Obviously, that caught me by surprise, in the sense that I expected it work. On the other hand, this isn't surprising. After all, I thought I had guessed how I could solve the problem of displaying these Rich renderables that update live and I thought I knew how to pause and resume their animations, but I hadn't convinced myself I knew exactly why it worked.

Warning

This goes to show that sometimes it is not the best idea to commit code that you wrote and that works if you don't know why it works. The code might seem to work and yet have deficiencies that will hurt you further down the road.

As it turns out, the reason why pausing is not working is that I did not grok why the rendering worked in the first place... So I had to go down that rabbit hole first.

"},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/#understanding-the-rich-rendering-magic","title":"Understanding the Rich rendering magic","text":""},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/#how-staticupdate-works","title":"How Static.update works","text":"

The most basic way of creating a Textual widget is to inherit from Widget and implement the method render that just returns the thing that must be printed on the screen. Then, the widget Static provides some functionality on top of that: the method update.

The method Static.update(renderable) is used to tell the widget in question that its method render (called when the widget needs to be drawn) should just return renderable. So, if the implementation of the method IntervalUpdater.update_rendering (the method that gets called 60 times per second) is this:

class IntervalUpdater(Static):\n# ...\ndef update_rendering(self) -> None:\nself.update(self._renderable_object)\n

Then, we are essentially saying \u201chey, the thing in self._renderable_object is what must be returned whenever Textual asks you to render yourself. So, this really proves that both Progress and Spinner from Rich are renderables. But what is more, this shows that my implementation of IntervalUpdater can be simplified greatly! In fact, we can boil it down to just this:

class IntervalUpdater(Static):\n_renderable_object: RenderableType\ndef __init__(self, renderable_object: RenderableType) -> None:  # (1)!\nsuper().__init__(renderable_object)  # (2)!\ndef on_mount(self) -> None:\nself.interval_update = self.set_interval(1 / 60, self.refresh)  # (3)!\n
  1. To create an instance of IntervalUpdater, now we give it the Rich renderable that we want displayed. If this Rich renderable is something that updates over time, then those changes will be reflected in the rendering.
  2. We initialise Static with the renderable object itself, instead of initialising with the empty string \"\" and then updating repeatedly.
  3. We call self.refresh 60 times per second. We don't need the auxiliary method update_rendering because this widget (an instance of Static) already knows what its renderable is.

Once you understand the code above you will realise that the previous implementation of update_rendering was actually doing superfluous work because the repeated calls to self.update always had the exact same object. Again, we see strong evidence that the Rich progress bars and the spinners have the inherent ability to display a different representation of themselves as time goes by.

"},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/#how-rich-spinners-get-updated","title":"How Rich spinners get updated","text":"

I kept seeing strong evidence that Rich spinners and Rich progress bars updated their own rendering but I still did not have actual proof. So, I went digging around to see how Spinner was implemented and I found this code (from the file spinner.py at the time of writing):

class Spinner:\n# ...\ndef __rich_console__(\nself, console: \"Console\", options: \"ConsoleOptions\"\n) -> \"RenderResult\":\nyield self.render(console.get_time())  # (1)!\n# ...\ndef render(self, time: float) -> \"RenderableType\":  # (2)!\n# ...\nframe_no = ((time - self.start_time) * self.speed) / (  # (3)!\nself.interval / 1000.0\n) + self.frame_no_offset\n# ...\n# ...\n
  1. The Rich spinner implements the function __rich_console__ that is supposed to return the result of rendering the spinner. Instead, it defers its work to the method render... However, to call the method render, we need to pass the argument console.get_time(), which the spinner uses to know in which state it is!
  2. The method render takes a time and returns a renderable!
  3. To determine the frame number (the current look of the spinner) we do some calculations with the \u201ccurrent time\u201d, given by the parameter time, and the time when the spinner started!

The snippet of code shown above, from the implementation of Spinner, explains why moving the mouse over a spinner (or a progress bar) that supposedly was paused makes it move. We no longer get repeated updates (60 times per second) because we told our app that we wanted to pause the result of set_interval, so we no longer get automatic updates. However, moving the mouse over the spinners and the progress bar makes Textual want to re-render them and, when it does, it figures out that time was not frozen (obviously!) and so the spinners and the progress bar have a different frame to show.

To get a better feeling for this, do the following experiment:

  1. Run the command textual console in a terminal to open the Textual devtools console.
  2. Add a print statement like print(\"Rendering from within spinner\") to the beginning of the method Spinner.render (from Rich).
  3. Add a print statement like print(\"Rendering static\") to the beginning of the method Static.render (from Textual).
  4. Put a blank terminal and the devtools console side by side.
  5. Run the app: notice that you get a lot of both print statements.
  6. Hit the Pause button: the print statements stop.
  7. Move your mouse over a widget or two: you get a couple of print statements, one from the Static.render and another from the Spinner.render.

The result of steps 6 and 7 are shown below. Notice that, in the beginning of the animation, the screen on the right shows some prints but is quiet because no more prints are coming in. When the mouse enters the screen and starts going over widgets, the screen on the right gets new prints in pairs, first from Static.render (which Textual calls to render the widget) and then from Spinner.render because ultimately we need to know how the Spinner looks.

Now, at this point, I made another educated guess and deduced that progress bars work in the same way! I still have to prove it, and I guess I will do so in another blog post, coming soon, where our spinner and progress bar widgets can be properly paused!

I will see you soon

"},{"location":"blog/2022/11/20/stealing-open-source-code-from-textual/","title":"Stealing Open Source code from Textual","text":"

I would like to talk about a serious issue in the Free and Open Source software world. Stealing code. You wouldn't steal a car would you?

But you should steal code from Open Source projects. Respect the license (you may need to give attribution) but stealing code is not like stealing a car. If I steal your car, I have deprived you of a car. If you steal my open source code, I haven't lost anything.

Warning

I'm not advocating for piracy. Open source code gives you explicit permission to use it.

From my point of view, I feel like code has greater value when it has been copied / modified in another project.

There are a number of files and modules in Textual that could either be lifted as is, or wouldn't require much work to extract. I'd like to cover a few here. You might find them useful in your next project.

"},{"location":"blog/2022/11/20/stealing-open-source-code-from-textual/#loop-first-last","title":"Loop first / last","text":"

How often do you find yourself looping over an iterable and needing to know if an element is the first and/or last in the sequence? It's a simple thing, but I find myself needing this a lot, so I wrote some helpers in _loop.py.

I'm sure there is an equivalent implementation on PyPI, but steal this if you need it.

Here's an example of use:

for last, (y, line) in loop_last(enumerate(self.lines, self.region.y)):\nyield move_to(x, y)\nyield from line\nif not last:\nyield new_line\n
"},{"location":"blog/2022/11/20/stealing-open-source-code-from-textual/#lru-cache","title":"LRU Cache","text":"

Python's lru_cache can be the one-liner that makes your code orders of magnitude faster. But it has a few gotchas.

The main issue is managing the lifetime of these caches. The decorator keeps a single global cache, which will keep a reference to every object in the function call. On an instance method that means you keep references to self for the lifetime of your app.

For a more flexibility you can use the LRUCache implementation from Textual. This uses essentially the same algorithm as the stdlib decorator, but it is implemented as a container.

Here's a quick example of its use. It works like a dictionary until you reach a maximum size. After that, new elements will kick out the element that was used least recently.

>>> from textual._cache import LRUCache\n>>> cache = LRUCache(maxsize=3)\n>>> cache[\"foo\"] = 1\n>>> cache[\"bar\"] = 2\n>>> cache[\"baz\"] = 3\n>>> dict(cache)\n{'foo': 1, 'bar': 2, 'baz': 3}\n>>> cache[\"egg\"] = 4\n>>> dict(cache)\n{'bar': 2, 'baz': 3, 'egg': 4}\n

In Textual, we use a LRUCache to store the results of rendering content to the terminal. For example, in a datatable it is too costly to render everything up front. So Textual renders only the lines that are currently visible on the \"screen\". The cache ensures that scrolling only needs to render the newly exposed lines, and lines that haven't been displayed in a while are discarded to save memory.

"},{"location":"blog/2022/11/20/stealing-open-source-code-from-textual/#color","title":"Color","text":"

Textual has a Color class which could be extracted in to a module of its own.

The Color class can parse colors encoded in a variety of HTML and CSS formats. Color object support a variety of methods and operators you can use to manipulate colors, in a fairly natural way.

Here's some examples in the REPL.

>>> from textual.color import Color\n>>> color = Color.parse(\"lime\")\n>>> color\nColor(0, 255, 0, a=1.0)\n>>> color.darken(0.8)\nColor(0, 45, 0, a=1.0)\n>>> color + Color.parse(\"red\").with_alpha(0.1)\nColor(25, 229, 0, a=1.0)\n>>> color = Color.parse(\"#12a30a\")\n>>> color\nColor(18, 163, 10, a=1.0)\n>>> color.css\n'rgb(18,163,10)'\n>>> color.hex\n'#12A30A'\n>>> color.monochrome\nColor(121, 121, 121, a=1.0)\n>>> color.monochrome.hex\n'#797979'\n>>> color.hsl\nHSL(h=0.3246187363834423, s=0.8843930635838151, l=0.33921568627450976)\n>>>\n

There are some very good color libraries in PyPI, which you should also consider using. But Textual's Color class is lean and performant, with no C dependencies.

"},{"location":"blog/2022/11/20/stealing-open-source-code-from-textual/#geometry","title":"Geometry","text":"

This may be my favorite module in Textual: geometry.py.

The geometry module contains a number of classes responsible for storing and manipulating 2D geometry. There is an Offset class which is a two dimensional point. A Region class which is a rectangular region defined by a coordinate and dimensions. There is a Spacing class which defines additional space around a region. And there is a Size class which defines the dimensions of an area by its width and height.

These objects are used by Textual's layout engine and compositor, which makes them the oldest and most thoroughly tested part of the project.

There's a lot going on in this module, but the docstrings are quite detailed and have unicode art like this to help explain things.

              cut_x \u2193\n          \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2510\n          \u2502        \u2502 \u2502   \u2502\n          \u2502    0   \u2502 \u2502 1 \u2502\n          \u2502        \u2502 \u2502   \u2502\n  cut_y \u2192 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2518\n          \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2510\n          \u2502    2   \u2502 \u2502 3 \u2502\n          \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2518\n
"},{"location":"blog/2022/11/20/stealing-open-source-code-from-textual/#you-should-steal-our-code","title":"You should steal our code","text":"

There is a lot going on in the Textual Repository. Including a CSS parser, renderer, layout and compositing engine. All written in pure Python. Steal it with my blessing.

"},{"location":"blog/2023/09/18/things-i-learned-while-building-textuals-textarea/","title":"Things I learned while building Textual's TextArea","text":"

TextArea is the latest widget to be added to Textual's growing collection. It provides a multi-line space to edit text, and features optional syntax highlighting for a selection of languages.

Adding a TextArea to your Textual app is as simple as adding this to your compose method:

yield TextArea()\n

Enabling syntax highlighting for a language is as simple as:

yield TextArea(language=\"python\")\n

Working on the TextArea widget for Textual taught me a lot about Python and my general approach to software engineering. It gave me an appreciation for the subtle functionality behind the editors we use on a daily basis \u2014 features we may not even notice, despite some engineer spending hours perfecting it to provide a small boost to our development experience.

This post is a tour of some of these learnings.

"},{"location":"blog/2023/09/18/things-i-learned-while-building-textuals-textarea/#vertical-cursor-movement-is-more-than-just-cursor_row","title":"Vertical cursor movement is more than just cursor_row++","text":"

When you move the cursor vertically, you can't simply keep the same column index and clamp it within the line. Editors should maintain the visual column offset where possible, meaning they must account for double-width emoji (sigh \ud83d\ude14) and East-Asian characters.

Notice that although the cursor is on column 11 while on line 1, it lands on column 6 when it arrives at line 3. This is because the 6th character of line 3 visually aligns with the 11th character of line 1.

"},{"location":"blog/2023/09/18/things-i-learned-while-building-textuals-textarea/#edits-from-other-sources-may-move-my-cursor","title":"Edits from other sources may move my cursor","text":"

There are two ways to interact with the TextArea:

  1. You can type into it.
  2. You can make API calls to edit the content in it.

In the example below, Hello, world!\\n is repeatedly inserted at the start of the document via the API. Notice that this updates the location of my cursor, ensuring that I don't lose my place.

This subtle feature should aid those implementing collaborative and multi-cursor editing.

This turned out to be one of the more complex features of the whole project, and went through several iterations before I was happy with the result.

Thankfully it resulted in some wonderful Tetris-esque whiteboards along the way!

A TetrisArea white-boarding session.

Sometimes stepping away from the screen and scribbling on a whiteboard with your colleagues (thanks Dave!) is what's needed to finally crack a tough problem.

Many thanks to David Brochart for sending me down this rabbit hole!

"},{"location":"blog/2023/09/18/things-i-learned-while-building-textuals-textarea/#spending-a-few-minutes-running-a-profiler-can-be-really-beneficial","title":"Spending a few minutes running a profiler can be really beneficial","text":"

While building the TextArea widget I avoided heavy optimisation work that may have affected readability or maintainability.

However, I did run a profiler in an attempt to detect flawed assumptions or mistakes which were affecting the performance of my code.

I spent around 30 minutes profiling TextArea using pyinstrument, and the result was a ~97% reduction in the time taken to handle a key press. What an amazing return on investment for such a minimal time commitment!

\"pyinstrument -r html\" produces this beautiful output.

pyinstrument unveiled two issues that were massively impacting performance.

"},{"location":"blog/2023/09/18/things-i-learned-while-building-textuals-textarea/#1-reparsing-highlighting-queries-on-each-key-press","title":"1. Reparsing highlighting queries on each key press","text":"

I was constructing a tree-sitter Query object on each key press, incorrectly assuming it was a low-overhead call. This query was completely static, so I moved it into the constructor ensuring the object was created only once. This reduced key processing time by around 94% - a substantial and very much noticeable improvement.

This seems obvious in hindsight, but the code in question was written earlier in the project and had been relegated in my mind to \"code that works correctly and will receive less attention from here on out\". pyinstrument quickly brought this code back to my attention and highlighted it as a glaring performance bug.

"},{"location":"blog/2023/09/18/things-i-learned-while-building-textuals-textarea/#2-namedtuples-are-slower-than-i-expected","title":"2. NamedTuples are slower than I expected","text":"

In Python, NamedTuples are slow to create relative to tuples, and this cost was adding up inside an extremely hot loop which was instantiating a large number of them. pyinstrument revealed that a large portion of the time during syntax highlighting was spent inside NamedTuple.__new__.

Here's a quick benchmark which constructs 10,000 NamedTuples:

\u276f hyperfine -w 2 'python sandbox/darren/make_namedtuples.py'\nBenchmark 1: python sandbox/darren/make_namedtuples.py\nTime (mean \u00b1 \u03c3):      15.9 ms \u00b1   0.5 ms    [User: 12.8 ms, System: 2.5 ms]\nRange (min \u2026 max):    15.2 ms \u2026  18.4 ms    165 runs\n

Here's the same benchmark using tuple instead:

\u276f hyperfine -w 2 'python sandbox/darren/make_tuples.py'\nBenchmark 1: python sandbox/darren/make_tuples.py\nTime (mean \u00b1 \u03c3):       9.3 ms \u00b1   0.5 ms    [User: 6.8 ms, System: 2.0 ms]\nRange (min \u2026 max):     8.7 ms \u2026  12.3 ms    256 runs\n

Switching to tuple resulted in another noticeable increase in responsiveness. Key-press handling time dropped by almost 50%! Unfortunately, this change does impact readability. However, the scope in which these tuples were used was very small, and so I felt it was a worthy trade-off.

"},{"location":"blog/2023/09/18/things-i-learned-while-building-textuals-textarea/#syntax-highlighting-is-very-different-from-what-i-expected","title":"Syntax highlighting is very different from what I expected","text":"

In order to support syntax highlighting, we make use of the tree-sitter library, which maintains a syntax tree representing the structure of our document.

To perform highlighting, we follow these steps:

  1. The user edits the document.
  2. We inform tree-sitter of the location of this edit.
  3. tree-sitter intelligently parses only the subset of the document impacted by the change, updating the tree.
  4. We run a query against the tree to retrieve ranges of text we wish to highlight.
  5. These ranges are mapped to styles (defined by the chosen \"theme\").
  6. These styles to the appropriate text ranges when rendering the widget.

Cycling through a few of the builtin themes.

Another benefit that I didn't consider before working on this project is that tree-sitter parsers can also be used to highlight syntax errors in a document. This can be useful in some situations - for example, highlighting mismatched HTML closing tags:

Highlighting mismatched closing HTML tags in red.

Before building this widget, I was oblivious as to how we might approach syntax highlighting. Without tree-sitter's incremental parsing approach, I'm not sure reasonable performance would have been feasible.

"},{"location":"blog/2023/09/18/things-i-learned-while-building-textuals-textarea/#edits-are-replacements","title":"Edits are replacements","text":"

All single-cursor edits can be distilled into a single behaviour: replace_range. This replaces a range of characters with some text. We can use this one method to easily implement deletion, insertion, and replacement of text.

  • Inserting text is replacing a zero-width range with the text to insert.
  • Pressing backspace (delete left) is just replacing the character behind the cursor with an empty string.
  • Selecting text and pressing delete is just replacing the selected text with an empty string.
  • Selecting text and pasting is replacing the selected text with some other text.

This greatly simplified my initial approach, which involved unique implementations for inserting and deleting.

"},{"location":"blog/2023/09/18/things-i-learned-while-building-textuals-textarea/#the-line-between-text-area-and-vscode-in-the-terminal","title":"The line between \"text area\" and \"VSCode in the terminal\"","text":"

A project like this has no clear finish line. There are always new features, optimisations, and refactors waiting to be made.

So where do we draw the line?

We want to provide a widget which can act as both a basic multiline text area that anyone can drop into their app, yet powerful and extensible enough to act as the foundation for a Textual-powered text editor.

Yet, the more features we add, the more opinionated the widget becomes, and the less that users will feel like they can build it into their own thing. Finding the sweet spot between feature-rich and flexible is no easy task.

I don't think the answer is clear, and I don't believe it's possible to please everyone.

Regardless, I'm happy with where we've landed, and I'm really excited to see what people build using TextArea in the future!

"},{"location":"blog/2023/09/06/what-is-textual-web/","title":"What is Textual Web?","text":"

If you know us, you will know that we are the team behind Rich and Textual \u2014 two popular Python libraries that work magic in the terminal.

Note

Not to mention Rich-CLI, Trogon, and Frogmouth

Today we are adding one project more to that lineup: textual-web.

Textual Web takes a Textual-powered TUI and turns it in to a web application. Here's a video of that in action:

With the textual-web command you can publish any Textual app on the web, making it available to anyone you send the URL to. This works without creating a socket server on your machine, so you won't have to configure firewalls and ports to share your applications.

We're excited about the possibilities here. Textual web apps are fast to spin up and tear down, and they can run just about anywhere that has an outgoing internet connection. They can be built by a single developer without any experience with a traditional web stack. All you need is proficiency in Python and a little time to read our lovely docs.

Future releases will expose more of the Web platform APIs to Textual apps, such as notifications and file system access. We plan to do this in a way that allows the same (Python) code to drive those features. For instance, a Textual app might save a file to disk in a terminal, but offer to download it in the browser.

Also in the pipeline is PWA support, so you can build terminal apps, web apps, and desktop apps with a single codebase.

Textual Web is currently in a public beta. Join our Discord server if you would like to help us test, or if you have any questions.

"},{"location":"blog/2023/06/06/to-tui-or-not-to-tui/","title":"To TUI or not to TUI","text":"

Tech moves pretty fast. If you don\u2019t stop and look around once in a while, you could miss it. And yet some technology feels like it has been around forever.

Terminals are one of those forever-technologies.

My interest is in Text User Interfaces: interactive apps that run within a terminal. I spend lot of time thinking about where TUIs might fit within the tech ecosystem, and how much more they could be doing for developers. Hardly surprising, since that is what we do at Textualize.

Recently I had the opportunity to test how new TUI projects would be received. You can consider these to be \"testing the water\", and hopefully representative of TUI apps in general.

"},{"location":"blog/2023/06/06/to-tui-or-not-to-tui/#the-projects","title":"The projects","text":"

In April we took a break from building Textual, to building apps with Textual. We had three ideas to work on, and three devs to do the work. One idea we parked for later. The other two were so promising we devoted more time to them. Both projects took around three developer-weeks to build, which also included work on Textual itself and standard duties for responding to issues / community requests. We released them in May.

The first project was Frogmouth, a Markdown browser. I think this TUI does better than the equivalent web experience in many ways. The only notable missing feature is images, and that will happen before too long.

Here's a screenshot:

Frogmouth /Users/willmcgugan/projects/textual/FAQ.md ContentsLocalBookmarksHistory\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u258e\u258a \u258eHow\u00a0do\u00a0I\u00a0pass\u00a0arguments\u00a0to\u00a0an\u00a0app?\u258a \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u258e\u258a \u2503\u25bc\u00a0\u2160\u00a0Frequently\u00a0Asked\u00a0Questions\u2503\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0Does\u00a0Textual\u00a0support\u00a0images?\u2503When\u00a0creating\u00a0your\u00a0App\u00a0class,\u00a0override\u00a0__init__\u00a0as\u00a0you\u00a0would\u00a0wheninheriting\u00a0normally.\u00a0For\u00a0example: \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0can\u00a0I\u00a0fix\u00a0ImportError\u00a0cannot\u00a0i\u2503 \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0can\u00a0I\u00a0select\u00a0and\u00a0copy\u00a0text\u00a0in\u00a0\u2503 \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0can\u00a0I\u00a0set\u00a0a\u00a0translucent\u00a0app\u00a0ba\u2503fromtextual.appimportApp,ComposeResult \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0do\u00a0I\u00a0center\u00a0a\u00a0widget\u00a0in\u00a0a\u00a0scre\u2503fromtextual.widgetsimportStatic \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0do\u00a0I\u00a0pass\u00a0arguments\u00a0to\u00a0an\u00a0app?\u2503 \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0Why\u00a0do\u00a0some\u00a0key\u00a0combinations\u00a0never\u2503classGreetings(App[None]): \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0Why\u00a0doesn't\u00a0Textual\u00a0look\u00a0good\u00a0on\u00a0m\u2503\u2502\u00a0\u00a0\u00a0 \u2503\u2514\u2500\u2500\u00a0\u2161\u00a0Why\u00a0doesn't\u00a0Textual\u00a0support\u00a0ANSI\u00a0t\u2503\u2502\u00a0\u00a0\u00a0def__init__(self,greeting:str=\"Hello\",to_greet:str=\"World\")->None: \u2503\u2503\u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0self.greeting=greeting \u2503\u2503\u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0self.to_greet=to_greet \u2503\u2503\u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0super().__init__() \u2503\u2503\u2502\u00a0\u00a0\u00a0 \u2503\u2503\u2502\u00a0\u00a0\u00a0defcompose(self)->ComposeResult: \u2503\u2503\u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0yieldStatic(f\"{self.greeting},\u00a0{self.to_greet}\") \u2503\u2503 \u2503\u2503 \u2503\u2503Then\u00a0the\u00a0app\u00a0can\u00a0be\u00a0run,\u00a0passing\u00a0in\u00a0various\u00a0arguments;\u00a0for\u00a0example: \u2503\u2503\u2585\u2585 \u2503\u2503 \u2503\u2503#\u00a0Running\u00a0with\u00a0default\u00a0arguments. \u2503\u2503Greetings().run() \u2503\u2503 \u2503\u2503#\u00a0Running\u00a0with\u00a0a\u00a0keyword\u00a0arguyment. \u2503\u2503Greetings(to_greet=\"davep\").run()\u2585\u2585 \u2503\u2503 \u2503\u2503#\u00a0Running\u00a0with\u00a0both\u00a0positional\u00a0arguments. \u2503\u2503Greetings(\"Well\u00a0hello\",\"there\").run() \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2503\u2589\u2503\u258e\u258a \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u00a0F1\u00a0\u00a0Help\u00a0\u00a0F2\u00a0\u00a0About\u00a0\u00a0CTRL+N\u00a0\u00a0Navigation\u00a0\u00a0CTRL+Q\u00a0\u00a0Quit\u00a0

Info

Quick aside about these \"screenshots\", because its a common ask. They aren't true screenshots, but rather SVGs exported by Textual.

We posted Frogmouth on Hacker News and Reddit on a Sunday morning (US time). A day later, it had 1,000 stars and lots of positive feedback.

The second project was Trogon, a library this time. Trogon automatically creates a TUI for command line apps. Same deal: we released it on a Sunday morning, and it reached 1K stars even quicker than Frogmouth.

Trogon sqlite-utilstransform v3.31Transform\u00a0a\u00a0table\u00a0beyond\u00a0the\u00a0capabilities\u00a0of\u00a0ALTER\u00a0TABLE \u258a\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258e disable-wal\u258a\u258a\u258e\u258e drop-table\u258a\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258e drop-view\u258a\u258e\u2587\u2587 dump\u258aOptions\u258e duplicate\u258a\u258e enable-counts\u258a--type\u00a0multiple\u00a0<text\u00a0choice>\u258e enable-fts\u258a\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u258e enable-wal\u258a\u2502\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2502\u258e extract\u258a\u2502\u258a\u258e\u2502\u258e\u2585\u2585 index-foreign-keys\u258a\u2502\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u2502\u258e indexes\u258a\u2502\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2502\u258e insert\u258a\u2502\u258aSelect\u25b2\u258e\u2502\u258e insert-files\u258a\u2502\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u2502\u258e install\u258a\u2514\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2518\u258e memory\u258a\u258aSelect\u258e\u258e optimize\u258a\u258aINTEGER\u258e\u258e populate-fts\u258a\u258aTEXT\u258e\u258e query\u258a\u258aFLOAT\u258e\u258e rebuild-fts\u258a\u258a\u258aBLOB\u258e\u258e\u258e reset-counts\u258a\u258a\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258e\u258e rows\u258a\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258e schema\u258a+\u00a0value\u258e search\u258aDrop\u00a0this\u00a0column\u258e tables\u258a\u258e transform\u258a--rename\u00a0multiple\u00a0<text\u00a0text>\u258e triggers\u258a\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u258e uninstall\u258a\u2502\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2502\u258e upsert\u258a\u2502\u258a\u258e\u2502\u258e vacuum views$\u00a0sqlite-utils\u00a0transform \u00a0CTRL+R\u00a0\u00a0Close\u00a0&\u00a0Run\u00a0\u00a0CTRL+T\u00a0Focus\u00a0Command\u00a0Tree\u00a0\u00a0CTRL+O\u00a0\u00a0Command\u00a0Info\u00a0\u00a0CTRL+S\u00a0\u00a0Search\u00a0\u00a0F1\u00a0\u00a0About\u00a0

Both of these projects are very young, but off to a great start. I'm looking forward to seeing how far we can taken them.

"},{"location":"blog/2023/06/06/to-tui-or-not-to-tui/#wrapping-up","title":"Wrapping up","text":"

With previous generations of software, TUIs have required a high degree of motivation to build. That has changed with the work that we (and others) have been doing. A TUI can be a powerful and maintainable piece of software which works as a standalone project, or as a value-add to an existing project.

As a forever-technology, a TUI is a safe bet.

"},{"location":"blog/2023/06/06/to-tui-or-not-to-tui/#discord","title":"Discord","text":"

Want to discuss this post with myself or other Textualize devs? Join our Discord server...

"},{"location":"css_types/","title":"CSS Types","text":"

CSS types define the values that Textual CSS styles accept.

CSS types will be linked from within the styles reference in the \"Formal Syntax\" section of each style. The CSS types will be denoted by a keyword enclosed by angle brackets < and >.

For example, the style align-horizontal references the CSS type <horizontal>:

\nalign-horizontal: <horizontal>;\n
"},{"location":"css_types/border/","title":"<border>","text":"

The <border> CSS type represents a border style.

"},{"location":"css_types/border/#syntax","title":"Syntax","text":"

The <border> type can take any of the following values:

Border type Description ascii A border with plus, hyphen, and vertical bar characters. blank A blank border (reserves space for a border). dashed Dashed line border. double Double lined border. heavy Heavy border. hidden Alias for \"none\". hkey Horizontal key-line border. inner Thick solid border. none Disabled border. outer Solid border with additional space around content. round Rounded corners. solid Solid border. tall Solid border with additional space top and bottom. thick Border style that is consistently thick across edges. vkey Vertical key-line border. wide Solid border with additional space left and right."},{"location":"css_types/border/#border-command","title":"Border command","text":"

The textual CLI has a subcommand which will let you explore the various border types interactively, when applied to the CSS rule border:

textual borders\n
"},{"location":"css_types/border/#examples","title":"Examples","text":""},{"location":"css_types/border/#css","title":"CSS","text":"
#container {\nborder: heavy red;\n}\n#heading {\nborder-bottom: solid blue;\n}\n
"},{"location":"css_types/border/#python","title":"Python","text":"
widget.styles.border = (\"heavy\", \"red\")\nwidget.styles.border_bottom = (\"solid\", \"blue\")\n
"},{"location":"css_types/color/","title":"<color>","text":"

The <color> CSS type represents a color.

Warning

Not to be confused with the color CSS rule to set text color.

"},{"location":"css_types/color/#syntax","title":"Syntax","text":"

A <color> should be in one of the formats explained in this section. A bullet point summary of the formats available follows:

  • a recognised named color (e.g., red);
  • a 3 or 6 hexadecimal digit number representing the RGB values of the color (e.g., #F35573);
  • a 4 or 8 hexadecimal digit number representing the RGBA values of the color (e.g., #F35573A0);
  • a color description in the RGB system, with or without opacity (e.g., rgb(23, 78, 200));
  • a color description in the HSL system, with or without opacity (e.g., hsl(290, 70%, 80%));

Textual's default themes also provide many CSS variables with colors that can be used out of the box.

"},{"location":"css_types/color/#named-colors","title":"Named colors","text":"

A named color is a <name> that Textual recognises. Below, you can find a (collapsed) list of all of the named colors that Textual recognises, along with their hexadecimal values, their RGB values, and a visual sample.

All named colors available. colors \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Name\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503hex\u00a0\u00a0\u00a0\u00a0\u2503RGB\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Color\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503 \u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529 \u2502\"aliceblue\"\u2502#F0F8FF\u2502rgb(240,\u00a0248,\u00a0255)\u2502\u2502 \u2502\"ansi_black\"\u2502#000000\u2502rgb(0,\u00a00,\u00a00)\u2502\u2502 \u2502\"ansi_blue\"\u2502#000080\u2502rgb(0,\u00a00,\u00a0128)\u2502\u2502 \u2502\"ansi_bright_black\"\u2502#808080\u2502rgb(128,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"ansi_bright_blue\"\u2502#0000FF\u2502rgb(0,\u00a00,\u00a0255)\u2502\u2502 \u2502\"ansi_bright_cyan\"\u2502#00FFFF\u2502rgb(0,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"ansi_bright_green\"\u2502#00FF00\u2502rgb(0,\u00a0255,\u00a00)\u2502\u2502 \u2502\"ansi_bright_magenta\"\u2502#FF00FF\u2502rgb(255,\u00a00,\u00a0255)\u2502\u2502 \u2502\"ansi_bright_red\"\u2502#FF0000\u2502rgb(255,\u00a00,\u00a00)\u2502\u2502 \u2502\"ansi_bright_white\"\u2502#FFFFFF\u2502rgb(255,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"ansi_bright_yellow\"\u2502#FFFF00\u2502rgb(255,\u00a0255,\u00a00)\u2502\u2502 \u2502\"ansi_cyan\"\u2502#008080\u2502rgb(0,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"ansi_green\"\u2502#008000\u2502rgb(0,\u00a0128,\u00a00)\u2502\u2502 \u2502\"ansi_magenta\"\u2502#800080\u2502rgb(128,\u00a00,\u00a0128)\u2502\u2502 \u2502\"ansi_red\"\u2502#800000\u2502rgb(128,\u00a00,\u00a00)\u2502\u2502 \u2502\"ansi_white\"\u2502#C0C0C0\u2502rgb(192,\u00a0192,\u00a0192)\u2502\u2502 \u2502\"ansi_yellow\"\u2502#808000\u2502rgb(128,\u00a0128,\u00a00)\u2502\u2502 \u2502\"antiquewhite\"\u2502#FAEBD7\u2502rgb(250,\u00a0235,\u00a0215)\u2502\u2502 \u2502\"aqua\"\u2502#00FFFF\u2502rgb(0,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"aquamarine\"\u2502#7FFFD4\u2502rgb(127,\u00a0255,\u00a0212)\u2502\u2502 \u2502\"azure\"\u2502#F0FFFF\u2502rgb(240,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"beige\"\u2502#F5F5DC\u2502rgb(245,\u00a0245,\u00a0220)\u2502\u2502 \u2502\"bisque\"\u2502#FFE4C4\u2502rgb(255,\u00a0228,\u00a0196)\u2502\u2502 \u2502\"black\"\u2502#000000\u2502rgb(0,\u00a00,\u00a00)\u2502\u2502 \u2502\"blanchedalmond\"\u2502#FFEBCD\u2502rgb(255,\u00a0235,\u00a0205)\u2502\u2502 \u2502\"blue\"\u2502#0000FF\u2502rgb(0,\u00a00,\u00a0255)\u2502\u2502 \u2502\"blueviolet\"\u2502#8A2BE2\u2502rgb(138,\u00a043,\u00a0226)\u2502\u2502 \u2502\"brown\"\u2502#A52A2A\u2502rgb(165,\u00a042,\u00a042)\u2502\u2502 \u2502\"burlywood\"\u2502#DEB887\u2502rgb(222,\u00a0184,\u00a0135)\u2502\u2502 \u2502\"cadetblue\"\u2502#5F9EA0\u2502rgb(95,\u00a0158,\u00a0160)\u2502\u2502 \u2502\"chartreuse\"\u2502#7FFF00\u2502rgb(127,\u00a0255,\u00a00)\u2502\u2502 \u2502\"chocolate\"\u2502#D2691E\u2502rgb(210,\u00a0105,\u00a030)\u2502\u2502 \u2502\"coral\"\u2502#FF7F50\u2502rgb(255,\u00a0127,\u00a080)\u2502\u2502 \u2502\"cornflowerblue\"\u2502#6495ED\u2502rgb(100,\u00a0149,\u00a0237)\u2502\u2502 \u2502\"cornsilk\"\u2502#FFF8DC\u2502rgb(255,\u00a0248,\u00a0220)\u2502\u2502 \u2502\"crimson\"\u2502#DC143C\u2502rgb(220,\u00a020,\u00a060)\u2502\u2502 \u2502\"cyan\"\u2502#00FFFF\u2502rgb(0,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"darkblue\"\u2502#00008B\u2502rgb(0,\u00a00,\u00a0139)\u2502\u2502 \u2502\"darkcyan\"\u2502#008B8B\u2502rgb(0,\u00a0139,\u00a0139)\u2502\u2502 \u2502\"darkgoldenrod\"\u2502#B8860B\u2502rgb(184,\u00a0134,\u00a011)\u2502\u2502 \u2502\"darkgray\"\u2502#A9A9A9\u2502rgb(169,\u00a0169,\u00a0169)\u2502\u2502 \u2502\"darkgreen\"\u2502#006400\u2502rgb(0,\u00a0100,\u00a00)\u2502\u2502 \u2502\"darkgrey\"\u2502#A9A9A9\u2502rgb(169,\u00a0169,\u00a0169)\u2502\u2502 \u2502\"darkkhaki\"\u2502#BDB76B\u2502rgb(189,\u00a0183,\u00a0107)\u2502\u2502 \u2502\"darkmagenta\"\u2502#8B008B\u2502rgb(139,\u00a00,\u00a0139)\u2502\u2502 \u2502\"darkolivegreen\"\u2502#556B2F\u2502rgb(85,\u00a0107,\u00a047)\u2502\u2502 \u2502\"darkorange\"\u2502#FF8C00\u2502rgb(255,\u00a0140,\u00a00)\u2502\u2502 \u2502\"darkorchid\"\u2502#9932CC\u2502rgb(153,\u00a050,\u00a0204)\u2502\u2502 \u2502\"darkred\"\u2502#8B0000\u2502rgb(139,\u00a00,\u00a00)\u2502\u2502 \u2502\"darksalmon\"\u2502#E9967A\u2502rgb(233,\u00a0150,\u00a0122)\u2502\u2502 \u2502\"darkseagreen\"\u2502#8FBC8F\u2502rgb(143,\u00a0188,\u00a0143)\u2502\u2502 \u2502\"darkslateblue\"\u2502#483D8B\u2502rgb(72,\u00a061,\u00a0139)\u2502\u2502 \u2502\"darkslategray\"\u2502#2F4F4F\u2502rgb(47,\u00a079,\u00a079)\u2502\u2502 \u2502\"darkslategrey\"\u2502#2F4F4F\u2502rgb(47,\u00a079,\u00a079)\u2502\u2502 \u2502\"darkturquoise\"\u2502#00CED1\u2502rgb(0,\u00a0206,\u00a0209)\u2502\u2502 \u2502\"darkviolet\"\u2502#9400D3\u2502rgb(148,\u00a00,\u00a0211)\u2502\u2502 \u2502\"deeppink\"\u2502#FF1493\u2502rgb(255,\u00a020,\u00a0147)\u2502\u2502 \u2502\"deepskyblue\"\u2502#00BFFF\u2502rgb(0,\u00a0191,\u00a0255)\u2502\u2502 \u2502\"dimgray\"\u2502#696969\u2502rgb(105,\u00a0105,\u00a0105)\u2502\u2502 \u2502\"dimgrey\"\u2502#696969\u2502rgb(105,\u00a0105,\u00a0105)\u2502\u2502 \u2502\"dodgerblue\"\u2502#1E90FF\u2502rgb(30,\u00a0144,\u00a0255)\u2502\u2502 \u2502\"firebrick\"\u2502#B22222\u2502rgb(178,\u00a034,\u00a034)\u2502\u2502 \u2502\"floralwhite\"\u2502#FFFAF0\u2502rgb(255,\u00a0250,\u00a0240)\u2502\u2502 \u2502\"forestgreen\"\u2502#228B22\u2502rgb(34,\u00a0139,\u00a034)\u2502\u2502 \u2502\"fuchsia\"\u2502#FF00FF\u2502rgb(255,\u00a00,\u00a0255)\u2502\u2502 \u2502\"gainsboro\"\u2502#DCDCDC\u2502rgb(220,\u00a0220,\u00a0220)\u2502\u2502 \u2502\"ghostwhite\"\u2502#F8F8FF\u2502rgb(248,\u00a0248,\u00a0255)\u2502\u2502 \u2502\"gold\"\u2502#FFD700\u2502rgb(255,\u00a0215,\u00a00)\u2502\u2502 \u2502\"goldenrod\"\u2502#DAA520\u2502rgb(218,\u00a0165,\u00a032)\u2502\u2502 \u2502\"gray\"\u2502#808080\u2502rgb(128,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"green\"\u2502#008000\u2502rgb(0,\u00a0128,\u00a00)\u2502\u2502 \u2502\"greenyellow\"\u2502#ADFF2F\u2502rgb(173,\u00a0255,\u00a047)\u2502\u2502 \u2502\"grey\"\u2502#808080\u2502rgb(128,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"honeydew\"\u2502#F0FFF0\u2502rgb(240,\u00a0255,\u00a0240)\u2502\u2502 \u2502\"hotpink\"\u2502#FF69B4\u2502rgb(255,\u00a0105,\u00a0180)\u2502\u2502 \u2502\"indianred\"\u2502#CD5C5C\u2502rgb(205,\u00a092,\u00a092)\u2502\u2502 \u2502\"indigo\"\u2502#4B0082\u2502rgb(75,\u00a00,\u00a0130)\u2502\u2502 \u2502\"ivory\"\u2502#FFFFF0\u2502rgb(255,\u00a0255,\u00a0240)\u2502\u2502 \u2502\"khaki\"\u2502#F0E68C\u2502rgb(240,\u00a0230,\u00a0140)\u2502\u2502 \u2502\"lavender\"\u2502#E6E6FA\u2502rgb(230,\u00a0230,\u00a0250)\u2502\u2502 \u2502\"lavenderblush\"\u2502#FFF0F5\u2502rgb(255,\u00a0240,\u00a0245)\u2502\u2502 \u2502\"lawngreen\"\u2502#7CFC00\u2502rgb(124,\u00a0252,\u00a00)\u2502\u2502 \u2502\"lemonchiffon\"\u2502#FFFACD\u2502rgb(255,\u00a0250,\u00a0205)\u2502\u2502 \u2502\"lightblue\"\u2502#ADD8E6\u2502rgb(173,\u00a0216,\u00a0230)\u2502\u2502 \u2502\"lightcoral\"\u2502#F08080\u2502rgb(240,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"lightcyan\"\u2502#E0FFFF\u2502rgb(224,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"lightgoldenrodyellow\"\u2502#FAFAD2\u2502rgb(250,\u00a0250,\u00a0210)\u2502\u2502 \u2502\"lightgray\"\u2502#D3D3D3\u2502rgb(211,\u00a0211,\u00a0211)\u2502\u2502 \u2502\"lightgreen\"\u2502#90EE90\u2502rgb(144,\u00a0238,\u00a0144)\u2502\u2502 \u2502\"lightgrey\"\u2502#D3D3D3\u2502rgb(211,\u00a0211,\u00a0211)\u2502\u2502 \u2502\"lightpink\"\u2502#FFB6C1\u2502rgb(255,\u00a0182,\u00a0193)\u2502\u2502 \u2502\"lightsalmon\"\u2502#FFA07A\u2502rgb(255,\u00a0160,\u00a0122)\u2502\u2502 \u2502\"lightseagreen\"\u2502#20B2AA\u2502rgb(32,\u00a0178,\u00a0170)\u2502\u2502 \u2502\"lightskyblue\"\u2502#87CEFA\u2502rgb(135,\u00a0206,\u00a0250)\u2502\u2502 \u2502\"lightslategray\"\u2502#778899\u2502rgb(119,\u00a0136,\u00a0153)\u2502\u2502 \u2502\"lightslategrey\"\u2502#778899\u2502rgb(119,\u00a0136,\u00a0153)\u2502\u2502 \u2502\"lightsteelblue\"\u2502#B0C4DE\u2502rgb(176,\u00a0196,\u00a0222)\u2502\u2502 \u2502\"lightyellow\"\u2502#FFFFE0\u2502rgb(255,\u00a0255,\u00a0224)\u2502\u2502 \u2502\"lime\"\u2502#00FF00\u2502rgb(0,\u00a0255,\u00a00)\u2502\u2502 \u2502\"limegreen\"\u2502#32CD32\u2502rgb(50,\u00a0205,\u00a050)\u2502\u2502 \u2502\"linen\"\u2502#FAF0E6\u2502rgb(250,\u00a0240,\u00a0230)\u2502\u2502 \u2502\"magenta\"\u2502#FF00FF\u2502rgb(255,\u00a00,\u00a0255)\u2502\u2502 \u2502\"maroon\"\u2502#800000\u2502rgb(128,\u00a00,\u00a00)\u2502\u2502 \u2502\"mediumaquamarine\"\u2502#66CDAA\u2502rgb(102,\u00a0205,\u00a0170)\u2502\u2502 \u2502\"mediumblue\"\u2502#0000CD\u2502rgb(0,\u00a00,\u00a0205)\u2502\u2502 \u2502\"mediumorchid\"\u2502#BA55D3\u2502rgb(186,\u00a085,\u00a0211)\u2502\u2502 \u2502\"mediumpurple\"\u2502#9370DB\u2502rgb(147,\u00a0112,\u00a0219)\u2502\u2502 \u2502\"mediumseagreen\"\u2502#3CB371\u2502rgb(60,\u00a0179,\u00a0113)\u2502\u2502 \u2502\"mediumslateblue\"\u2502#7B68EE\u2502rgb(123,\u00a0104,\u00a0238)\u2502\u2502 \u2502\"mediumspringgreen\"\u2502#00FA9A\u2502rgb(0,\u00a0250,\u00a0154)\u2502\u2502 \u2502\"mediumturquoise\"\u2502#48D1CC\u2502rgb(72,\u00a0209,\u00a0204)\u2502\u2502 \u2502\"mediumvioletred\"\u2502#C71585\u2502rgb(199,\u00a021,\u00a0133)\u2502\u2502 \u2502\"midnightblue\"\u2502#191970\u2502rgb(25,\u00a025,\u00a0112)\u2502\u2502 \u2502\"mintcream\"\u2502#F5FFFA\u2502rgb(245,\u00a0255,\u00a0250)\u2502\u2502 \u2502\"mistyrose\"\u2502#FFE4E1\u2502rgb(255,\u00a0228,\u00a0225)\u2502\u2502 \u2502\"moccasin\"\u2502#FFE4B5\u2502rgb(255,\u00a0228,\u00a0181)\u2502\u2502 \u2502\"navajowhite\"\u2502#FFDEAD\u2502rgb(255,\u00a0222,\u00a0173)\u2502\u2502 \u2502\"navy\"\u2502#000080\u2502rgb(0,\u00a00,\u00a0128)\u2502\u2502 \u2502\"oldlace\"\u2502#FDF5E6\u2502rgb(253,\u00a0245,\u00a0230)\u2502\u2502 \u2502\"olive\"\u2502#808000\u2502rgb(128,\u00a0128,\u00a00)\u2502\u2502 \u2502\"olivedrab\"\u2502#6B8E23\u2502rgb(107,\u00a0142,\u00a035)\u2502\u2502 \u2502\"orange\"\u2502#FFA500\u2502rgb(255,\u00a0165,\u00a00)\u2502\u2502 \u2502\"orangered\"\u2502#FF4500\u2502rgb(255,\u00a069,\u00a00)\u2502\u2502 \u2502\"orchid\"\u2502#DA70D6\u2502rgb(218,\u00a0112,\u00a0214)\u2502\u2502 \u2502\"palegoldenrod\"\u2502#EEE8AA\u2502rgb(238,\u00a0232,\u00a0170)\u2502\u2502 \u2502\"palegreen\"\u2502#98FB98\u2502rgb(152,\u00a0251,\u00a0152)\u2502\u2502 \u2502\"paleturquoise\"\u2502#AFEEEE\u2502rgb(175,\u00a0238,\u00a0238)\u2502\u2502 \u2502\"palevioletred\"\u2502#DB7093\u2502rgb(219,\u00a0112,\u00a0147)\u2502\u2502 \u2502\"papayawhip\"\u2502#FFEFD5\u2502rgb(255,\u00a0239,\u00a0213)\u2502\u2502 \u2502\"peachpuff\"\u2502#FFDAB9\u2502rgb(255,\u00a0218,\u00a0185)\u2502\u2502 \u2502\"peru\"\u2502#CD853F\u2502rgb(205,\u00a0133,\u00a063)\u2502\u2502 \u2502\"pink\"\u2502#FFC0CB\u2502rgb(255,\u00a0192,\u00a0203)\u2502\u2502 \u2502\"plum\"\u2502#DDA0DD\u2502rgb(221,\u00a0160,\u00a0221)\u2502\u2502 \u2502\"powderblue\"\u2502#B0E0E6\u2502rgb(176,\u00a0224,\u00a0230)\u2502\u2502 \u2502\"purple\"\u2502#800080\u2502rgb(128,\u00a00,\u00a0128)\u2502\u2502 \u2502\"rebeccapurple\"\u2502#663399\u2502rgb(102,\u00a051,\u00a0153)\u2502\u2502 \u2502\"red\"\u2502#FF0000\u2502rgb(255,\u00a00,\u00a00)\u2502\u2502 \u2502\"rosybrown\"\u2502#BC8F8F\u2502rgb(188,\u00a0143,\u00a0143)\u2502\u2502 \u2502\"royalblue\"\u2502#4169E1\u2502rgb(65,\u00a0105,\u00a0225)\u2502\u2502 \u2502\"saddlebrown\"\u2502#8B4513\u2502rgb(139,\u00a069,\u00a019)\u2502\u2502 \u2502\"salmon\"\u2502#FA8072\u2502rgb(250,\u00a0128,\u00a0114)\u2502\u2502 \u2502\"sandybrown\"\u2502#F4A460\u2502rgb(244,\u00a0164,\u00a096)\u2502\u2502 \u2502\"seagreen\"\u2502#2E8B57\u2502rgb(46,\u00a0139,\u00a087)\u2502\u2502 \u2502\"seashell\"\u2502#FFF5EE\u2502rgb(255,\u00a0245,\u00a0238)\u2502\u2502 \u2502\"sienna\"\u2502#A0522D\u2502rgb(160,\u00a082,\u00a045)\u2502\u2502 \u2502\"silver\"\u2502#C0C0C0\u2502rgb(192,\u00a0192,\u00a0192)\u2502\u2502 \u2502\"skyblue\"\u2502#87CEEB\u2502rgb(135,\u00a0206,\u00a0235)\u2502\u2502 \u2502\"slateblue\"\u2502#6A5ACD\u2502rgb(106,\u00a090,\u00a0205)\u2502\u2502 \u2502\"slategray\"\u2502#708090\u2502rgb(112,\u00a0128,\u00a0144)\u2502\u2502 \u2502\"slategrey\"\u2502#708090\u2502rgb(112,\u00a0128,\u00a0144)\u2502\u2502 \u2502\"snow\"\u2502#FFFAFA\u2502rgb(255,\u00a0250,\u00a0250)\u2502\u2502 \u2502\"springgreen\"\u2502#00FF7F\u2502rgb(0,\u00a0255,\u00a0127)\u2502\u2502 \u2502\"steelblue\"\u2502#4682B4\u2502rgb(70,\u00a0130,\u00a0180)\u2502\u2502 \u2502\"tan\"\u2502#D2B48C\u2502rgb(210,\u00a0180,\u00a0140)\u2502\u2502 \u2502\"teal\"\u2502#008080\u2502rgb(0,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"thistle\"\u2502#D8BFD8\u2502rgb(216,\u00a0191,\u00a0216)\u2502\u2502 \u2502\"tomato\"\u2502#FF6347\u2502rgb(255,\u00a099,\u00a071)\u2502\u2502 \u2502\"turquoise\"\u2502#40E0D0\u2502rgb(64,\u00a0224,\u00a0208)\u2502\u2502 \u2502\"violet\"\u2502#EE82EE\u2502rgb(238,\u00a0130,\u00a0238)\u2502\u2502 \u2502\"wheat\"\u2502#F5DEB3\u2502rgb(245,\u00a0222,\u00a0179)\u2502\u2502 \u2502\"white\"\u2502#FFFFFF\u2502rgb(255,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"whitesmoke\"\u2502#F5F5F5\u2502rgb(245,\u00a0245,\u00a0245)\u2502\u2502 \u2502\"yellow\"\u2502#FFFF00\u2502rgb(255,\u00a0255,\u00a00)\u2502\u2502 \u2502\"yellowgreen\"\u2502#9ACD32\u2502rgb(154,\u00a0205,\u00a050)\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"},{"location":"css_types/color/#hex-rgb-value","title":"Hex RGB value","text":"

The hexadecimal RGB format starts with an octothorpe # and is then followed by 3 or 6 hexadecimal digits: 0123456789ABCDEF. Casing is ignored.

  • If 6 digits are used, the format is #RRGGBB:
  • RR represents the red channel;
  • GG represents the green channel; and
  • BB represents the blue channel.
  • If 3 digits are used, the format is #RGB.

In a 3 digit color, each channel is represented by a single digit which is duplicated when converting to the 6 digit format. For example, the color #A2F is the same as #AA22FF.

"},{"location":"css_types/color/#hex-rgba-value","title":"Hex RGBA value","text":"

This is the same as the hex RGB value, but with an extra channel for the alpha component (that sets opacity).

  • If 8 digits are used, the format is #RRGGBBAA, equivalent to the format #RRGGBB with two extra digits for opacity.
  • If 4 digits are used, the format is #RGBA, equivalent to the format #RGB with an extra digit for opacity.
"},{"location":"css_types/color/#rgb-description","title":"rgb description","text":"

The rgb format description is a functional description of a color in the RGB color space. This description follows the format rgb(red, green, blue), where red, green, and blue are decimal integers between 0 and 255. They represent the value of the channel with the same name.

For example, rgb(0, 255, 32) is equivalent to #00FF20.

"},{"location":"css_types/color/#rgba-description","title":"rgba description","text":"

The rgba format description is the same as the rgb with an extra parameter for opacity, which should be a value between 0 and 1.

For example, rgba(0, 255, 32, 0.5) is the color rgb(0, 255, 32) with 50% opacity.

"},{"location":"css_types/color/#hsl-description","title":"hsl description","text":"

The hsl format description is a functional description of a color in the HSL color space. This description follows the format hsl(hue, saturation, lightness), where

  • hue is a float between 0 and 360;
  • saturation is a percentage between 0% and 100%; and
  • lightness is a percentage between 0% and 100%.

For example, the color #00FF20 would be represented as hsl(128, 100%, 50%) in the HSL color space.

"},{"location":"css_types/color/#hsla-description","title":"hsla description","text":"

The hsla format description is the same as the hsl with an extra parameter for opacity, which should be a value between 0 and 1.

For example, hsla(128, 100%, 50%, 0.5) is the color hsl(128, 100%, 50%) with 50% opacity.

"},{"location":"css_types/color/#examples","title":"Examples","text":""},{"location":"css_types/color/#css","title":"CSS","text":"
Header {\nbackground: red;           /* Color name */\n}\n.accent {\ncolor: $accent;            /* Textual variable */\n}\n#footer {\ntint: hsl(300, 20%, 70%);  /* HSL description */\n}\n
"},{"location":"css_types/color/#python","title":"Python","text":"

In Python, rules that expect a <color> can also accept an instance of the type Color.

# Mimicking the CSS syntax\nwidget.styles.background = \"red\"           # Color name\nwidget.styles.color = \"$accent\"            # Textual variable\nwidget.styles.tint = \"hsl(300, 20%, 70%)\"  # HSL description\nfrom textual.color import Color\n# Using a Color object directly...\ncolor = Color(16, 200, 45)\n# ... which can also parse the CSS syntax\ncolor = Color.parse(\"#A8F\")\n
"},{"location":"css_types/horizontal/","title":"<horizontal>","text":"

The <horizontal> CSS type represents a position along the horizontal axis.

"},{"location":"css_types/horizontal/#syntax","title":"Syntax","text":"

The <horizontal> type can take any of the following values:

Value Description center Aligns in the center of the horizontal axis. left (default) Aligns on the left of the horizontal axis. right Aligns on the right of the horizontal axis."},{"location":"css_types/horizontal/#examples","title":"Examples","text":""},{"location":"css_types/horizontal/#css","title":"CSS","text":"
.container {\nalign-horizontal: right;\n}\n
"},{"location":"css_types/horizontal/#python","title":"Python","text":"
widget.styles.align_horizontal = \"right\"\n
"},{"location":"css_types/integer/","title":"<integer>","text":"

The <integer> CSS type represents an integer number.

"},{"location":"css_types/integer/#syntax","title":"Syntax","text":"

An <integer> is any valid integer number like -10 or 42.

Note

Some CSS rules may expect an <integer> within certain bounds. If that is the case, it will be noted in that rule.

"},{"location":"css_types/integer/#examples","title":"Examples","text":""},{"location":"css_types/integer/#css","title":"CSS","text":"
.classname {\noffset: 10 -20\n}\n
"},{"location":"css_types/integer/#python","title":"Python","text":"

In Python, a rule that expects a CSS type <integer> will expect a value of the type int:

widget.styles.offset = (10, -20)\n
"},{"location":"css_types/name/","title":"<name>","text":"

The <name> type represents a sequence of characters that identifies something.

"},{"location":"css_types/name/#syntax","title":"Syntax","text":"

A <name> is any non-empty sequence of characters:

  • starting with a letter a-z, A-Z, or underscore _; and
  • followed by zero or more letters a-zA-Z, digits 0-9, underscores _, and hiphens -.
"},{"location":"css_types/name/#examples","title":"Examples","text":""},{"location":"css_types/name/#css","title":"CSS","text":"
Screen {\nlayers: onlyLetters Letters-and-hiphens _lead-under letters-1-digit;\n}\n
"},{"location":"css_types/name/#python","title":"Python","text":"
widget.styles.layers = \"onlyLetters Letters-and-hiphens _lead-under letters-1-digit\"\n
"},{"location":"css_types/number/","title":"<number>","text":"

The <number> CSS type represents a real number, which can be an integer or a number with a decimal part (akin to a float in Python).

"},{"location":"css_types/number/#syntax","title":"Syntax","text":"

A <number> is an <integer>, optionally followed by the decimal point . and a decimal part composed of one or more digits.

"},{"location":"css_types/number/#examples","title":"Examples","text":""},{"location":"css_types/number/#css","title":"CSS","text":"
Grid {\ngrid-size: 3 6  /* Integers are numbers */\n}\n.translucid {\nopacity: 0.5    /* Numbers can have a decimal part */\n}\n
"},{"location":"css_types/number/#python","title":"Python","text":"

In Python, a rule that expects a CSS type <number> will accept an int or a float:

widget.styles.grid_size = (3, 6)  # Integers are numbers\nwidget.styles.opacity = 0.5       # Numbers can have a decimal part\n
"},{"location":"css_types/overflow/","title":"<overflow>","text":"

The <overflow> CSS type represents overflow modes.

"},{"location":"css_types/overflow/#syntax","title":"Syntax","text":"

The <overflow> type can take any of the following values:

Value Description auto Determine overflow mode automatically. hidden Don't overflow. scroll Allow overflowing."},{"location":"css_types/overflow/#examples","title":"Examples","text":""},{"location":"css_types/overflow/#css","title":"CSS","text":"
#container {\noverflow-y: hidden;  /* Don't overflow */\n}\n
"},{"location":"css_types/overflow/#python","title":"Python","text":"
widget.styles.overflow_y = \"hidden\"  # Don't overflow\n
"},{"location":"css_types/percentage/","title":"<percentage>","text":"

The <percentage> CSS type represents a percentage value. It is often used to represent values that are relative to the parent's values.

Warning

Not to be confused with the <scalar> type.

"},{"location":"css_types/percentage/#syntax","title":"Syntax","text":"

A <percentage> is a <number> followed by the percent sign % (without spaces). Some rules may clamp the values between 0% and 100%.

"},{"location":"css_types/percentage/#examples","title":"Examples","text":""},{"location":"css_types/percentage/#css","title":"CSS","text":"
#footer {\n/* Integer followed by % */\ncolor: red 70%;\n/* The number can be negative/decimal, although that may not make sense */\noffset: -30% 12.5%;\n}\n
"},{"location":"css_types/percentage/#python","title":"Python","text":"
# Integer followed by %\nwidget.styles.color = \"red 70%\"\n# The number can be negative/decimal, although that may not make sense\nwidget.styles.offset = (\"-30%\", \"12.5%\")\n
"},{"location":"css_types/scalar/","title":"<scalar>","text":"

The <scalar> CSS type represents a length. It can be a <number> and a unit, or the special value auto. It is used to represent lengths, for example in the width and height rules.

Warning

Not to be confused with the <number> or <percentage> types.

"},{"location":"css_types/scalar/#syntax","title":"Syntax","text":"

A <scalar> can be any of the following:

  • a fixed number of cells (e.g., 10);
  • a fractional proportion relative to the sizes of the other widgets (e.g., 1fr);
  • a percentage relative to the container widget (e.g., 50%);
  • a percentage relative to the container width/height (e.g., 25w/75h);
  • a percentage relative to the viewport width/height (e.g., 25vw/75vh); or
  • the special value auto to compute the optimal size to fit without scrolling.

A complete reference table and detailed explanations follow. You can skip to the examples.

Unit symbol Unit Example Description \"\" Cell 10 Number of cells (rows or columns). \"fr\" Fraction 1fr Specifies the proportion of space the widget should occupy. \"%\" Percent 75% Length relative to the container widget. \"w\" Width 25w Percentage relative to the width of the container widget. \"h\" Height 75h Percentage relative to the height of the container widget. \"vw\" Viewport width 25vw Percentage relative to the viewport width. \"vh\" Viewport height 75vh Percentage relative to the viewport height. - Auto auto Tries to compute the optimal size to fit without scrolling."},{"location":"css_types/scalar/#cell","title":"Cell","text":"

The number of cells is the only unit for a scalar that is absolute. This can be an integer or a float but floats are truncated to integers.

If used to specify a horizontal length, it corresponds to the number of columns. For example, in width: 15, this sets the width of a widget to be equal to 15 cells, which translates to 15 columns.

If used to specify a vertical length, it corresponds to the number of lines. For example, in height: 10, this sets the height of a widget to be equal to 10 cells, which translates to 10 lines.

"},{"location":"css_types/scalar/#fraction","title":"Fraction","text":"

The unit fraction is used to represent proportional sizes.

For example, if two widgets are side by side and one has width: 1fr and the other has width: 3fr, the second one will be three times as wide as the first one.

"},{"location":"css_types/scalar/#percent","title":"Percent","text":"

The percent unit matches a <percentage> and is used to specify a total length relative to the space made available by the container widget.

If used to specify a horizontal length, it will be relative to the width of the container. For example, width: 50% sets the width of a widget to 50% of the width of its container.

If used to specify a vertical length, it will be relative to the height of the container. For example, height: 50% sets the height of a widget to 50% of the height of its container.

"},{"location":"css_types/scalar/#width","title":"Width","text":"

The width unit is similar to the percent unit, except it sets the percentage to be relative to the width of the container.

For example, width: 25w sets the width of a widget to 25% of the width of its container and height: 25w sets the height of a widget to 25% of the width of its container. So, if the container has a width of 100 cells, the width and the height of the child widget will be of 25 cells.

"},{"location":"css_types/scalar/#height","title":"Height","text":"

The height unit is similar to the percent unit, except it sets the percentage to be relative to the height of the container.

For example, height: 75h sets the height of a widget to 75% of the height of its container and width: 75h sets the width of a widget to 75% of the height of its container. So, if the container has a height of 100 cells, the width and the height of the child widget will be of 75 cells.

"},{"location":"css_types/scalar/#viewport-width","title":"Viewport width","text":"

This is the same as the width unit, except that it is relative to the width of the viewport instead of the width of the immediate container. The width of the viewport is the width of the terminal minus the widths of widgets that are docked left or right.

For example, width: 25vw will try to set the width of a widget to be 25% of the viewport width, regardless of the widths of its containers.

"},{"location":"css_types/scalar/#viewport-height","title":"Viewport height","text":"

This is the same as the height unit, except that it is relative to the height of the viewport instead of the height of the immediate container. The height of the viewport is the height of the terminal minus the heights of widgets that are docked top or bottom.

For example, height: 75vh will try to set the height of a widget to be 75% of the viewport height, regardless of the height of its containers.

"},{"location":"css_types/scalar/#auto","title":"Auto","text":"

This special value will try to calculate the optimal size to fit the contents of the widget without scrolling.

For example, if its container is big enough, a label with width: auto will be just as wide as its text.

"},{"location":"css_types/scalar/#examples","title":"Examples","text":""},{"location":"css_types/scalar/#css","title":"CSS","text":"
Horizontal {\nwidth: 60;     /* 60 cells */\nheight: 1fr;   /* proportional size of 1 */\n}\n
"},{"location":"css_types/scalar/#python","title":"Python","text":"
widget.styles.width = 16       # Cell unit can be specified with an int/float\nwidget.styles.height = \"1fr\"   # proportional size of 1\n
"},{"location":"css_types/text_align/","title":"<text-align>","text":"

The <text-align> CSS type represents alignments that can be applied to text.

Warning

Not to be confused with the text-align CSS rule that sets the alignment of text in a widget.

"},{"location":"css_types/text_align/#syntax","title":"Syntax","text":"

A <text-align> can be any of the following values:

Value Alignment type center Center alignment. end Alias for right. justify Text is justified inside the widget. left Left alignment. right Right alignment. start Alias for left.

Tip

The meanings of start and end will likely change when RTL languages become supported by Textual.

"},{"location":"css_types/text_align/#examples","title":"Examples","text":""},{"location":"css_types/text_align/#css","title":"CSS","text":"
Label {\nrule: justify;\n}\n
"},{"location":"css_types/text_align/#python","title":"Python","text":"
widget.styles.text_align = \"justify\"\n
"},{"location":"css_types/text_style/","title":"<text-style>","text":"

The <text-style> CSS type represents styles that can be applied to text.

Warning

Not to be confused with the text-style CSS rule that sets the style of text in a widget.

"},{"location":"css_types/text_style/#syntax","title":"Syntax","text":"

A <text-style> can be the value none for plain text with no styling, or any space-separated combination of the following values:

Value Description bold Bold text. italic Italic text. reverse Reverse video text (foreground and background colors reversed). strike Strikethrough text. underline Underline text."},{"location":"css_types/text_style/#examples","title":"Examples","text":""},{"location":"css_types/text_style/#css","title":"CSS","text":"
#label1 {\n/* You can specify any value by itself. */\nrule: strike;\n}\n#label2 {\n/* You can also combine multiple values. */\nrule: strike bold italic reverse;\n}\n
"},{"location":"css_types/text_style/#python","title":"Python","text":"
# You can specify any value by itself\nwidget.styles.text_style = \"strike\"\n# You can also combine multiple values\nwidget.styles.text_style = \"strike bold italic reverse\n
"},{"location":"css_types/vertical/","title":"<vertical>","text":"

The <vertical> CSS type represents a position along the vertical axis.

"},{"location":"css_types/vertical/#syntax","title":"Syntax","text":"

The <vertical> type can take any of the following values:

Value Description bottom Aligns at the bottom of the vertical axis. middle Aligns in the middle of the vertical axis. top (default) Aligns at the top of the vertical axis."},{"location":"css_types/vertical/#examples","title":"Examples","text":""},{"location":"css_types/vertical/#css","title":"CSS","text":"
.container {\nalign-vertical: top;\n}\n
"},{"location":"css_types/vertical/#python","title":"Python","text":"
widget.styles.align_vertical = \"top\"\n
"},{"location":"events/","title":"Events","text":"

A reference to Textual events.

See the links to the left of the page, or in the hamburger menu (three horizontal bars, top left).

"},{"location":"events/blur/","title":"Blur","text":"

The Blur event is sent to a widget when it loses focus.

  • Bubbles
  • Verbose
"},{"location":"events/blur/#attributes","title":"Attributes","text":"

No other attributes

"},{"location":"events/blur/#code","title":"Code","text":""},{"location":"events/blur/#textual.events.Blur","title":"textual.events.Blur class","text":"

Bases: Event

Sent when a widget is blurred (un-focussed).

  • Bubbles
  • Verbose
"},{"location":"events/blur/#see-also","title":"See also","text":"
  • DescendantBlur
  • DescendantFocus
  • Focus
"},{"location":"events/click/","title":"Click","text":"

The Click event is sent to a widget when the user clicks a mouse button.

  • Bubbles
  • Verbose
"},{"location":"events/click/#attributes","title":"Attributes","text":"attribute type purpose x int Mouse x coordinate, relative to Widget y int Mouse y coordinate, relative to Widget delta_x int Change in x since last mouse event delta_y int Change in y since last mouse event button int Index of mouse button shift bool Shift key pressed if True meta bool Meta key pressed if True ctrl bool Ctrl key pressed if True screen_x int Mouse x coordinate relative to the screen screen_y int Mouse y coordinate relative to the screen"},{"location":"events/click/#code","title":"Code","text":""},{"location":"events/click/#textual.events.Click","title":"textual.events.Click class","text":"

Bases: MouseEvent

Sent when a widget is clicked.

  • Bubbles
  • Verbose
"},{"location":"events/descendant_blur/","title":"DescendantBlur","text":"

The DescendantBlur event is sent to a widget when one of its children loses focus.

  • Bubbles
  • Verbose
"},{"location":"events/descendant_blur/#attributes","title":"Attributes","text":"

No other attributes

"},{"location":"events/descendant_blur/#code","title":"Code","text":""},{"location":"events/descendant_blur/#textual.events.DescendantBlur","title":"textual.events.DescendantBlur class","text":"

Bases: Event

Sent when a child widget is blurred.

  • Bubbles
  • Verbose
"},{"location":"events/descendant_blur/#textual.events.DescendantBlur.control","title":"control property","text":"
control: Widget\n

The widget that was blurred (alias of widget).

"},{"location":"events/descendant_blur/#textual.events.DescendantBlur.widget","title":"widget instance-attribute","text":"
widget: Widget\n

The widget that was blurred.

"},{"location":"events/descendant_blur/#see-also","title":"See also","text":"
  • Blur
  • DescendantFocus
  • Focus
"},{"location":"events/descendant_focus/","title":"DescendantFocus","text":"

The DescendantFocus event is sent to a widget when one of its descendants receives focus.

  • Bubbles
  • Verbose
"},{"location":"events/descendant_focus/#attributes","title":"Attributes","text":"

No other attributes

"},{"location":"events/descendant_focus/#code","title":"Code","text":""},{"location":"events/descendant_focus/#textual.events.DescendantFocus","title":"textual.events.DescendantFocus class","text":"

Bases: Event

Sent when a child widget is focussed.

  • Bubbles
  • Verbose
"},{"location":"events/descendant_focus/#textual.events.DescendantFocus.control","title":"control property","text":"
control: Widget\n

The widget that was focused (alias of widget).

"},{"location":"events/descendant_focus/#textual.events.DescendantFocus.widget","title":"widget instance-attribute","text":"
widget: Widget\n

The widget that was focused.

"},{"location":"events/descendant_focus/#see-also","title":"See also","text":"
  • Blur
  • DescendantBlur
  • Focus
"},{"location":"events/enter/","title":"Enter","text":"

The Enter event is sent to a widget when the mouse pointer first moves over a widget.

  • Bubbles
  • Verbose
"},{"location":"events/enter/#attributes","title":"Attributes","text":"

No other attributes

"},{"location":"events/enter/#code","title":"Code","text":""},{"location":"events/enter/#textual.events.Enter","title":"textual.events.Enter class","text":"

Bases: Event

Sent when the mouse is moved over a widget.

  • Bubbles
  • Verbose
"},{"location":"events/focus/","title":"Focus","text":"

The Focus event is sent to a widget when it receives input focus.

  • Bubbles
  • Verbose
"},{"location":"events/focus/#attributes","title":"Attributes","text":"

No other attributes

"},{"location":"events/focus/#code","title":"Code","text":""},{"location":"events/focus/#textual.events.Focus","title":"textual.events.Focus class","text":"

Bases: Event

Sent when a widget is focussed.

  • Bubbles
  • Verbose
"},{"location":"events/focus/#see-also","title":"See also","text":"
  • Blur
  • DescendantBlur
  • DescendantFocus
"},{"location":"events/hide/","title":"Hide","text":"

The Hide event is sent to a widget when it is hidden from view.

  • Bubbles
  • Verbose
"},{"location":"events/hide/#attributes","title":"Attributes","text":"

No additional attributes

"},{"location":"events/hide/#code","title":"Code","text":""},{"location":"events/hide/#textual.events.Hide","title":"textual.events.Hide class","text":"

Bases: Event

Sent when a widget has been hidden.

  • Bubbles
  • Verbose

A widget may be hidden by setting its visible flag to False, if it is no longer in a layout, or if it has been offset beyond the edges of the terminal.

"},{"location":"events/key/","title":"Key","text":"

The Key event is sent to a widget when the user presses a key on the keyboard.

  • Bubbles
  • Verbose
"},{"location":"events/key/#attributes","title":"Attributes","text":"attribute type purpose key str Name of the key that was pressed. char str or None The character that was pressed, or None it isn't printable."},{"location":"events/key/#code","title":"Code","text":""},{"location":"events/key/#textual.events.Key","title":"textual.events.Key class","text":"
def __init__(self, key, character):\n

Bases: InputEvent

Sent when the user hits a key on the keyboard.

  • Bubbles
  • Verbose
Parameters Name Type Description Default key str

The key that was pressed.

required character str | None

A printable character or None if it is not printable.

required Attributes Name Type Description aliases list[str]

The aliases for the key, including the key itself.

"},{"location":"events/key/#textual.events.Key.is_printable","title":"is_printable property","text":"
is_printable: bool\n

Check if the key is printable (produces a unicode character).

Returns Type Description bool

True if the key is printable.

"},{"location":"events/key/#textual.events.Key.name","title":"name property","text":"
name: str\n

Name of a key suitable for use as a Python identifier.

"},{"location":"events/key/#textual.events.Key.name_aliases","title":"name_aliases property","text":"
name_aliases: list[str]\n

The corresponding name for every alias in aliases list.

"},{"location":"events/leave/","title":"Leave","text":"

The Leave event is sent to a widget when the mouse pointer moves off a widget.

  • Bubbles
  • Verbose
"},{"location":"events/leave/#attributes","title":"Attributes","text":"

No other attributes

"},{"location":"events/leave/#code","title":"Code","text":""},{"location":"events/leave/#textual.events.Leave","title":"textual.events.Leave class","text":"

Bases: Event

Sent when the mouse is moved away from a widget.

  • Bubbles
  • Verbose
"},{"location":"events/load/","title":"Load","text":"

The Load event is sent to the app prior to switching the terminal to application mode.

The load event is typically used to do any setup actions required by the app that don't change the display.

  • Bubbles
  • Verbose
"},{"location":"events/load/#attributes","title":"Attributes","text":"

No additional attributes

"},{"location":"events/load/#code","title":"Code","text":""},{"location":"events/load/#textual.events.Load","title":"textual.events.Load class","text":"

Bases: Event

Sent when the App is running but before the terminal is in application mode.

Use this event to run any set up that doesn't require any visuals such as loading configuration and binding keys.

  • Bubbles
  • Verbose
"},{"location":"events/mount/","title":"Mount","text":"

The Mount event is sent to a widget and Application when it is first mounted.

The mount event is typically used to set the initial state of a widget or to add new children widgets.

  • Bubbles
  • Verbose
"},{"location":"events/mount/#attributes","title":"Attributes","text":"

No additional attributes

"},{"location":"events/mount/#code","title":"Code","text":""},{"location":"events/mount/#textual.events.Mount","title":"textual.events.Mount class","text":"

Bases: Event

Sent when a widget is mounted and may receive messages.

  • Bubbles
  • Verbose
"},{"location":"events/mouse_capture/","title":"MouseCapture","text":"

The MouseCapture event is sent to a widget when it is capturing mouse events from outside of its borders on the screen.

  • Bubbles
  • Verbose
"},{"location":"events/mouse_capture/#attributes","title":"Attributes","text":"attribute type purpose mouse_position Offset Mouse coordinates when the mouse was captured"},{"location":"events/mouse_capture/#code","title":"Code","text":""},{"location":"events/mouse_capture/#textual.events.MouseCapture","title":"textual.events.MouseCapture class","text":"
def __init__(self, mouse_position):\n

Bases: Event

Sent when the mouse has been captured.

  • Bubbles
  • Verbose

When a mouse has been captured, all further mouse events will be sent to the capturing widget.

Parameters Name Type Description Default mouse_position Offset

The position of the mouse when captured.

required"},{"location":"events/mouse_down/","title":"MouseDown","text":"

The MouseDown event is sent to a widget when a mouse button is pressed.

  • Bubbles
  • Verbose
"},{"location":"events/mouse_down/#attributes","title":"Attributes","text":"attribute type purpose x int Mouse x coordinate, relative to Widget y int Mouse y coordinate, relative to Widget delta_x int Change in x since last mouse event delta_y int Change in y since last mouse event button int Index of mouse button shift bool Shift key pressed if True meta bool Meta key pressed if True ctrl bool Ctrl key pressed if True screen_x int Mouse x coordinate relative to the screen screen_y int Mouse y coordinate relative to the screen"},{"location":"events/mouse_down/#code","title":"Code","text":""},{"location":"events/mouse_down/#textual.events.MouseDown","title":"textual.events.MouseDown class","text":"

Bases: MouseEvent

Sent when a mouse button is pressed.

  • Bubbles
  • Verbose
"},{"location":"events/mouse_move/","title":"MouseMove","text":"

The MouseMove event is sent to a widget when the mouse pointer is moved over a widget.

  • Bubbles
  • Verbose
"},{"location":"events/mouse_move/#attributes","title":"Attributes","text":"attribute type purpose x int Mouse x coordinate, relative to Widget y int Mouse y coordinate, relative to Widget delta_x int Change in x since last mouse event delta_y int Change in y since last mouse event button int Index of mouse button shift bool Shift key pressed if True meta bool Meta key pressed if True ctrl bool Ctrl key pressed if True screen_x int Mouse x coordinate relative to the screen screen_y int Mouse y coordinate relative to the screen"},{"location":"events/mouse_move/#code","title":"Code","text":""},{"location":"events/mouse_move/#textual.events.MouseMove","title":"textual.events.MouseMove class","text":"

Bases: MouseEvent

Sent when the mouse cursor moves.

  • Bubbles
  • Verbose
"},{"location":"events/mouse_release/","title":"MouseRelease","text":"

The MouseRelease event is sent to a widget when it is no longer receiving mouse events outside of its borders.

  • Bubbles
  • Verbose
"},{"location":"events/mouse_release/#attributes","title":"Attributes","text":"attribute type purpose mouse_position Offset Mouse coordinates when the mouse was released"},{"location":"events/mouse_release/#code","title":"Code","text":""},{"location":"events/mouse_release/#textual.events.MouseRelease","title":"textual.events.MouseRelease class","text":"
def __init__(self, mouse_position):\n

Bases: Event

Mouse has been released.

  • Bubbles
  • Verbose
Parameters Name Type Description Default mouse_position Offset

The position of the mouse when released.

required"},{"location":"events/mouse_scroll_down/","title":"MouseScrollDown","text":"

The MouseScrollDown event is sent to a widget when the scroll wheel (or trackpad equivalent) is moved down.

  • Bubbles
  • Verbose
"},{"location":"events/mouse_scroll_down/#attributes","title":"Attributes","text":"attribute type purpose x int Mouse x coordinate, relative to Widget y int Mouse y coordinate, relative to Widget"},{"location":"events/mouse_scroll_down/#code","title":"Code","text":""},{"location":"events/mouse_scroll_down/#textual.events.MouseScrollDown","title":"textual.events.MouseScrollDown class","text":"

Bases: MouseEvent

Sent when the mouse wheel is scrolled down.

  • Bubbles
  • Verbose
"},{"location":"events/mouse_scroll_up/","title":"MouseScrollUp","text":"

The MouseScrollUp event is sent to a widget when the scroll wheel (or trackpad equivalent) is moved up.

  • Bubbles
  • Verbose
"},{"location":"events/mouse_scroll_up/#attributes","title":"Attributes","text":"attribute type purpose x int Mouse x coordinate, relative to Widget y int Mouse y coordinate, relative to Widget"},{"location":"events/mouse_scroll_up/#code","title":"Code","text":""},{"location":"events/mouse_scroll_up/#textual.events.MouseScrollUp","title":"textual.events.MouseScrollUp class","text":"

Bases: MouseEvent

Sent when the mouse wheel is scrolled up.

  • Bubbles
  • Verbose
"},{"location":"events/mouse_up/","title":"MouseUp","text":"

The MouseUp event is sent to a widget when the user releases a mouse button.

  • Bubbles
  • Verbose
"},{"location":"events/mouse_up/#attributes","title":"Attributes","text":"attribute type purpose x int Mouse x coordinate, relative to Widget y int Mouse y coordinate, relative to Widget delta_x int Change in x since last mouse event delta_y int Change in y since last mouse event button int Index of mouse button shift bool Shift key pressed if True meta bool Meta key pressed if True ctrl bool Ctrl key pressed if True screen_x int Mouse x coordinate relative to the screen screen_y int Mouse y coordinate relative to the screen"},{"location":"events/mouse_up/#code","title":"Code","text":""},{"location":"events/mouse_up/#textual.events.MouseUp","title":"textual.events.MouseUp class","text":"

Bases: MouseEvent

Sent when a mouse button is released.

  • Bubbles
  • Verbose
"},{"location":"events/paste/","title":"Paste","text":"

The Paste event is sent to a widget when the user pastes text.

  • Bubbles
  • Verbose
"},{"location":"events/paste/#attributes","title":"Attributes","text":"attribute type purpose text str The text that was pasted"},{"location":"events/paste/#code","title":"Code","text":""},{"location":"events/paste/#textual.events.Paste","title":"textual.events.Paste class","text":"
def __init__(self, text):\n

Bases: Event

Event containing text that was pasted into the Textual application. This event will only appear when running in a terminal emulator that supports bracketed paste mode. Textual will enable bracketed pastes when an app starts, and disable it when the app shuts down.

  • Bubbles
  • Verbose
Parameters Name Type Description Default text str

The text that has been pasted.

required"},{"location":"events/resize/","title":"Resize","text":"

The Resize event is sent to a widget when its size changes and when it is first made visible.

  • Bubbles
  • Verbose
"},{"location":"events/resize/#attributes","title":"Attributes","text":"attribute type purpose size Size The new size of the Widget virtual_size Size The virtual size (scrollable area) of the Widget container_size Size The size of the container (parent widget)"},{"location":"events/resize/#code","title":"Code","text":""},{"location":"events/resize/#textual.events.Resize","title":"textual.events.Resize class","text":"
def __init__(self, size, virtual_size, container_size=None):\n

Bases: Event

Sent when the app or widget has been resized.

  • Bubbles
  • Verbose
Parameters Name Type Description Default size Size

The new size of the Widget.

required virtual_size Size

The virtual size (scrollable size) of the Widget.

required container_size Size | None

The size of the Widget's container widget.

None"},{"location":"events/screen_resume/","title":"ScreenResume","text":"

The ScreenResume event is sent to a Screen when it becomes current.

  • Bubbles
  • Verbose
"},{"location":"events/screen_resume/#attributes","title":"Attributes","text":"

No other attributes

"},{"location":"events/screen_resume/#code","title":"Code","text":""},{"location":"events/screen_resume/#textual.events.ScreenResume","title":"textual.events.ScreenResume class","text":"

Bases: Event

Sent to screen that has been made active.

  • Bubbles
  • Verbose
"},{"location":"events/screen_suspend/","title":"ScreenSuspend","text":"

The ScreenSuspend event is sent to a Screen when it is replaced by another screen.

  • Bubbles
  • Verbose
"},{"location":"events/screen_suspend/#attributes","title":"Attributes","text":"

No other attributes

"},{"location":"events/screen_suspend/#code","title":"Code","text":""},{"location":"events/screen_suspend/#textual.events.ScreenSuspend","title":"textual.events.ScreenSuspend class","text":"

Bases: Event

Sent to screen when it is no longer active.

  • Bubbles
  • Verbose
"},{"location":"events/show/","title":"Show","text":"

The Show event is sent to a widget when it becomes visible.

  • Bubbles
  • Verbose
"},{"location":"events/show/#attributes","title":"Attributes","text":"

No additional attributes

"},{"location":"events/show/#code","title":"Code","text":""},{"location":"events/show/#textual.events.Show","title":"textual.events.Show class","text":"

Bases: Event

Sent when a widget has become visible.

  • Bubbles
  • Verbose
"},{"location":"examples/styles/","title":"Index","text":"

These are the examples from the documentation, used to generate screenshots.

You can run them with the textual CLI.

For example:

textual run text_style.py\n
"},{"location":"guide/","title":"Guide","text":"

Welcome to the Textual Guide! An in-depth reference on how to build apps with Textual.

"},{"location":"guide/#example-code","title":"Example code","text":"

Most of the code in this guide is fully working\u2014you could cut and paste it if you wanted to.

Although it is probably easier to check out the Textual repository and navigate to the docs/examples/guide directory and run the examples from there.

"},{"location":"guide/CSS/","title":"Textual CSS","text":"

Textual uses CSS to apply style to widgets. If you have any exposure to web development you will have encountered CSS, but don't worry if you haven't: this chapter will get you up to speed.

VSCode User?

The official Textual CSS extension adds syntax highlighting for both external files and inline CSS.

"},{"location":"guide/CSS/#stylesheets","title":"Stylesheets","text":"

CSS stands for Cascading Stylesheet. A stylesheet is a list of styles and rules about how those styles should be applied to a web page. In the case of Textual, the stylesheet applies styles to widgets, but otherwise it is the same idea.

Let's look at some Textual CSS.

Header {\ndock: top;\nheight: 3;\ncontent-align: center middle;\nbackground: blue;\ncolor: white;\n}\n

This is an example of a CSS rule set. There may be many such sections in any given CSS file.

Let's break this CSS code down a bit.

Header {\ndock: top;\nheight: 3;\ncontent-align: center middle;\nbackground: blue;\ncolor: white;\n}\n

The first line is a selector which tells Textual which widget(s) to modify. In the above example, the styles will be applied to a widget defined by the Python class Header.

Header {\ndock: top;\nheight: 3;\ncontent-align: center middle;\nbackground: blue;\ncolor: white;\n}\n

The lines inside the curly braces contains CSS rules, which consist of a rule name and rule value separated by a colon and ending in a semicolon. Such rules are typically written one per line, but you could add additional rules as long as they are separated by semicolons.

The first rule in the above example reads \"dock: top;\". The rule name is dock which tells Textual to place the widget on an edge of the screen. The text after the colon is top which tells Textual to dock to the top of the screen. Other valid values for dock are \"right\", \"bottom\", or \"left\"; but \"top\" is most appropriate for a header.

"},{"location":"guide/CSS/#the-dom","title":"The DOM","text":"

The DOM, or Document Object Model, is a term borrowed from the web world. Textual doesn't use documents but the term has stuck. In Textual CSS, the DOM is an arrangement of widgets you can visualize as a tree-like structure.

Some widgets contain other widgets: for instance, a list control widget will likely also have item widgets, or a dialog widget may contain button widgets. These child widgets form the branches of the tree.

Let's look at a trivial Textual app.

dom1.pyOutput
from textual.app import App\nclass ExampleApp(App):\npass\nif __name__ == \"__main__\":\napp = ExampleApp()\napp.run()\n

ExampleApp

This example creates an instance of ExampleApp, which will implicitly create a Screen object. In DOM terms, the Screen is a child of ExampleApp.

With the above example, the DOM will look like the following:

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nM1Ya0/jOFx1MDAxNP3Or6i6X3YlXGKOY8fxSKtcdTAwMTXPpSywo1x1MDAwMVxyj9VcYrmJaT3Na1x1MDAxMpfHIP77XqdMXHUwMDFlbVxiZVx1MDAxN0ZEUZv4OtfX1+fc4+R+pdfr67tU9j/0+vLWXHUwMDE3oVxuMnHTXzXt1zLLVVx1MDAxMoNcdFx1MDAxN/d5Ms38oudY6zT/sL5cdTAwMWWJbFwidVx1MDAxYVxuX1rXKp+KMNfTQCWWn0TrSsso/8P8XHUwMDFliUj+niZRoDOrXHUwMDFhZE1cdTAwMDZKJ9lsLFx1MDAxOcpIxjpcdTAwMDfv/8B9r3df/NaiXHUwMDBilIiSOCi6XHUwMDE3hlp4njvfepTERaiUIVx1MDAwN3PqkbKDyrdhMC1cdTAwMDOwXkHAsrKYpn56sbN1pD45eiNcdTAwMWKyfNP5Nvy6d1WNeqXC8FjfhbM8XGJ/PM1kZc11lkzkqVxu9Fx1MDAxOOz2XFx7+VxcnkBcbqqnsmQ6XHUwMDFhxzLPXHUwMDFizySp8JW+M21cYpWtXCJcdTAwMWVcdTAwMTU+qpZbk1x1MDAwMeJamHmO5zpcdTAwMGV1XHUwMDEwqc23cECY5VLsXHUwMDEwh9G5mLaSXHUwMDEw1lx1MDAwMGL6XHUwMDA1XHUwMDE1R1x1MDAxNdVQ+JNcdTAwMTGEXHUwMDE2XHUwMDA3VVx1MDAxZlx1MDAwZvvcrs335sdMa1x1MDAwM46lXHUwMDFhjbVpxNjyXHUwMDEwcT1GZ75r+ZBF/m3P5pRcdTAwMTKMcWkxI6aDoFx1MDAwMMKX+fyNRZY+5qmfm5tatCbQnXlcdTAwMTTVkVRbY+dcIkv5vlx1MDAxYVxmvk7GfyX88CxcdTAwMWRcdTAwMGZcdTAwMGVLX1xy2Gl5q/ul4WG1y+2Ze1x1MDAxMm1cdTAwMGUv7evp9v6BPls7+8jRfrtbkWXJzfN+XHUwMDFiUawuO5HK7eNVlchpXHUwMDFhiFx1MDAxOfZt10XE5sjjXHUwMDBl4aU9VPFcdTAwMDSM8TRcZqu2xJ9UdFmpxbtA0kacdYba5CmG2thQXHUwMDE0XHUwMDEw4i1N0e7le69cdTAwMTSldidFObeAXG6GLP+HoTpcdTAwMTNxnopcZljQwlLWxlK+wErmeraDXFxcdTAwMWK9Piu7kMihOr1cdTAwMDSJ1YInsT5W31x1MDAwYjS5XHUwMDE2hWKEsIsw41x1MDAxY1HW6LUrXCJcdTAwMTXeNdawgCxEvnMrojSUXHUwMDFiafrrb/VcdTAwMTTnXHUwMDEyXCIpXFyTxjNcdTAwMWKhXHUwMDFhXHUwMDE5aPd9mJvMXHUwMDFhqNdcbkSu7Fx1MDAxMKkgXGJrXGL0IVx1MDAxMFx1MDAwMT6zwTKCk2RqpGJcdTAwMTGetMXZScZM+nqGxVx1MDAxNkZS+qRmYlx1MDAwNCDkUJXdpVx1MDAxOXn+PdGXXyfDk+PRwblzQsefkvPLd89IXHUwMDE3W8hlhHheXHUwMDFiI1x1MDAxZNuxXHUwMDEwI9h+U0pSukhJj0GlmFx1MDAxM+tHalx1MDAwMqRcdTAwMTHFXHUwMDFlcV+fml3KXHUwMDE27MfnQ0rOXHUwMDBmtlx1MDAwMrw33tldu9zDn9+jYM78nu5/vr45INuHXHUwMDA3XHUwMDE5XHL+vMNTTLbdV/CLT4PB3u7EP/Q2iH1cdTAwMTKFf+/EXHUwMDE3ozdcdTAwMTX49sS/QOCZkVZe7a/eSOBcdPXmW3+UXHUwMDEzwinUYUKX34J3o+3dVlx1MDAxM9ZZTVxisZhdaNzbXHUwMDE1XHUwMDEz0lJMsDNfREBcdTAwMWFhXHUwMDE3wp2fKu8vx2GbvGPUaO2Q82M/kzJ+SspZo/+rSfkzMjgv5WWMnZSbVZJcdTAwMTbOMfxcdTAwMTTlQCZAv+FcXF7Bu0vxO+Wc43BcdTAwMGJe7lx1MDAxMXNaOYdcdTAwMTm1XFzOjYJcdTAwMTNujjdjXHUwMDFlslxid5vkLlx06Fx1MDAxMIsz7FJcdTAwMTcvyLlcdTAwMDebXuDGf9loXHUwMDE3wf1sJuZaZHpTxYGKR2CslFxm2OhPzbhryEKO7VLCoVx1MDAxNlKOXHTyylmb6YnU7D0tXHUwMDAyckBcdTAwMWPYg1x1MDAxYYxWr5+98kNQ19b4sXMpqX1cdTAwMTlcdTAwMDfPXHUwMDA2hThUX8Tg1Vx1MDAwME7KmLdcdTAwMTBcdTAwMTW24LWh2HVcdTAwMTXfKmyHPVx1MDAxNVY7zVx1MDAxN8JcbkWut5IoUlx1MDAxYdL/MVGxnk9zkc9ccsPvsVx1MDAxNMG8XHUwMDE1plW3zVx1MDAxN4LUeGzu3KqrXsWU4qa8/rLa2nttXHUwMDExweaoYbfysFL/NzuQwmdfpOmxXHUwMDA2pJVrXHUwMDAwYFbBY+GuJta/VvJms+Xb0lVxmDRcdTAwMTYpNCVHmundP6w8/Fx1MDAwYlxiYlx1MDAxObwifQ== ExampleApp()Screen()

This doesn't look much like a tree yet. Let's add a header and a footer to this application, which will create more branches of the tree:

dom2.pyOutput
from textual.app import App, ComposeResult\nfrom textual.widgets import Header, Footer\nclass ExampleApp(App):\ndef compose(self) -> ComposeResult:\nyield Header()\nyield Footer()\nif __name__ == \"__main__\":\napp = ExampleApp()\napp.run()\n

ExampleApp \u2b58ExampleApp

With a header and a footer widget the DOM looks like this:

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1aa0/bSFx1MDAxNP3Or0DZL7tS4877UWm1glx1MDAxNpZ3aWFcdTAwMDNlVVWuPSReP2s7QKj633dsgu28XHUwMDFjXHUwMDEzXHUwMDEyNpXWQiGZmdy5nrnnzLk3/r6xudlKXHUwMDA3kWq92WypO8v0XHUwMDFjOzZvW6+y9lx1MDAxYlx1MDAxNSdOXHUwMDE46C6Uf07CfmzlI3tpXHUwMDFhJW9ev/bN2FVp5JmWMm6cpG96Sdq3ndCwQv+1kyo/+SN7PTF99XtcdTAwMTT6dlx1MDAxYVx1MDAxYuUkbWU7aVx1MDAxOD/MpTzlqyBNtPW/9efNze/5a8U72zH9MLDz4XlHxT0hx1tPwiB3XHUwMDE1Ulx1MDAwNLEkklx1MDAxNFx1MDAwM5zknZ4sVbbuvdZcdTAwMGWrsidralxyTi//7Jn/qItLwY5Odq7hh17nvJz12vG8s3TgPayDafX6sSp7kzRcdTAwMGVddeHYaS+bfKy9+F5cdTAwMTLqJSi/XHUwMDE1h/1uL1BJMvKdMDItJ1x1MDAxZOg2XG6KRjPo5ibKlrtsXHUwMDAxIDVcdTAwMTBcdTAwMTdYMIwpXHUwMDA2RJS3m31fSINRhFx05nTMo7ehp3dAe/RcdTAwMGLIr9Knr6bldrVjgV2OXHUwMDExyJKwcre3j/dZma+nnG4vzVx1MDAxYVx1MDAxMTJcdTAwMDQgTHD6YLuyXHUwMDFhKl99yCDjknAhip5sxmjfzsPg8/jq9cw4XHUwMDFhrlIryT5UvM1cdTAwMWPdXHUwMDE5j6FqXHUwMDFjVXbY/bgtrlx1MDAwZTBoR9euc9x7f+6eqm+FrZGgS9Vd2io6fryqM1x1MDAwYjto76vf+3JLOsHX+Pro+vh+f3u6WTOOw9umdpfu7sjoV00nLM1cdTAwMGXflfvTj2wzXHUwMDFkbilcdTAwMDNcdTAwMDTpXHUwMDAwpITgot9zXHUwMDAyV3dcdTAwMDZ9zyvbQsstMbhR8XdcdTAwMDL5I35WYVx1MDAwZsFM2DMuJEKYoca4r1/mNcU9XHUwMDAydbiHXHUwMDA0XHUwMDFhlOZcdTAwMTB8XHUwMDBl7tPYXGaSyIw1tqZgn0/Dvlx1MDAxY8c6oYBwKjFaPtSXXHUwMDE5h+V2h0F65tw/xJJBNcNcdTAwMDHEXHUwMDAw4lJcdTAwMDLKR0btmr7jXHJGdjBcdTAwMGZY7fnOnelHntqKol9/q65worQnuWky8p0tz+lmgd2y9L2peCTmU0efm8VcdTAwMDDfsW2vXHUwMDEyf5Z2xNQ24/0mZ1hcdTAwMTg7XScwvfNpftZCMVZW+lx1MDAxMIpT8EhcdTAwMTmcjcd87Vx1MDAwNGON8eh9ROdcdTAwMDcn51dXXHUwMDFjfGA+3btcbkkvXXc8YmhcdTAwMDDGXHRcdTAwMTFiXHUwMDFhXHUwMDFlXHUwMDExpVx1MDAwNuBcdTAwMDTBlVx1MDAwMpLSSUBcbq55YkxcdTAwMDA8XHUwMDFlwlx1MDAxNHLOKV9cdTAwMDEy6061I6ftfzlcdTAwMWFcdTAwMWO+9c47g0/3XHUwMDFkeb69dbK+h/DFQefm9oi8Oz6Kqf3nXHUwMDAw9Vx1MDAxMXnHlmBcdTAwMTdd2Pt7u651LLZcYjz3vfc7wVV3XHR2l76880TD9Fx0XHUwMDFiekturtt+6rZcdTAwMDdcdTAwMWbIXHUwMDE3c0dYd+q47y9hXHUwMDE1tlx1MDAwZb+ddtPww5dTR4pcdTAwMDO307uEnzrN7DZcdTAwMTE5XHUwMDE4gVx1MDAxMjUrXHUwMDEyOYTNXHUwMDE2OZhcbkkolOWIeaRaXHUwMDFmXHUwMDE260qqrJZUXHUwMDA1MzhcdTAwMDTymclNPaeSKZyKSmHxyKVQICQpYCtIaJZcdTAwMTmI01RcdTAwMGVcdTAwMDIjrTWq5syKlVxuZilcdTAwMWE+Mn5pimaOXHUwMDFhXHUwMDE4VzSFj7WYe8D8XHUwMDE00HExXHUwMDEzc1x1MDAxMFx1MDAxMEq0lm1eUKg/ktZcdTAwMTNzXHUwMDE4XGJDUlx1MDAwMTiejjnIXHImZSZkiMyulSFcdTAwMGZcdTAwMThEslFwXHUwMDE3XHUwMDAwxMSQXHUwMDFjMcrQpKrRniGh4bhcdTAwMDBcdTAwMTJz71x1MDAxNkWiwIwvgsQkNeN021x0bCfo6s7yLNNotPrZvG1gXHUwMDAwrNVcdTAwMWGRmlxmqURcdTAwMDSI4raz2zOjbGNcckKylIdSJlx0YkRWRlxmS2x1XHUwMDE5wnBwcai2VGDPdVxuSE2/gOtcZkn/UV5ip/Bcblx1MDAxOTp7ytVnXlx1MDAwN4KYz3JrOswn3PLMJH1cdTAwMWL6vpPq5T9ccp0gXHUwMDFkX+Z8PbcyfPeUaY/36tuq9o1cdTAwMTNBlFlcdTAwMWNVsOW7zVx1MDAxMir5h+L951dTR7cnQzi7KsFbWtio/l8oXHUwMDA3g1x1MDAwMM1OwpD2g2KEmlx1MDAxN0VOXHUwMDBmXHUwMDA3b4OrvnQv/Y8n9uG9+5f7z81/y1061uaQXHUwMDE31OSFMIJcdTAwMDRcdMr1a4XNM1x1MDAwM4RcdTAwMTBDY4Q9dldy0v84XHUwMDE304QlKIa0dOhFUjHn6F03OtlNXHUwMDBmLv0t92T70N/12zPU9/+p2NPtrmh5l252XoY3fcKG3j4jw3skxdnHblx1MDAwNqfKT0ArysQkJuOtXHUwMDE1ZoX64GWoeXmrfvvWlVkhrmVWzlxmJiBmkIFVM2uzjFxmccZYRqsvmpA9OVx1MDAxZZ+XkO1pXHUwMDE1o+JcdTAwMTdOyOYog/GErPDxXHUwMDE50oaBurSMIy2NafNSiJfsXHUwMDFlmGfx9o06oJ3j+z3w7XhcdTAwMWKsO1x1MDAwMDFhXHUwMDA2gZzAXHUwMDFjX9mvXHUwMDFiY9KGXHUwMDFiWId8MWBdlI3kOkmQ1VrWiyib40+nZF/sWIdcdTAwMDNcdTAwMTd+ci/aN1F/XHUwMDAw/1c2y1I2K1ren8XsPME0fcKG3q60dI04rXLNilx1MDAwNFx1MDAxM6SYjTdcdTAwMTeEzTHQvoAnKKb6/VtXwqawlrC5NCjEWFx1MDAwZVx1MDAwNdVcblx0u2FcdDvLPzlcdTAwMDGlmy8jmJ5cdTAwMTiPz1x1MDAxM0y7YZi+uGCaozfGXHUwMDA1U+FjLfRmVrBcdTAwMTma/UicoEBcdTAwMTJcZnDzXHUwMDEydn32tq7QXHUwMDAzwFx1MDAxMFRcYplVQ1x1MDAwNVx1MDAxNmhcdTAwMDR5WEslwXV+wIfIw6uDXHUwMDFlXHUwMDAyhqSMS0klg1JCMYlEgVxyqZNIJFx1MDAxONY+M4nGgUmAhFx1MDAxNEm0XHUwMDAwMJ9R0F48k5ld0G5Q8C2PuWqlmVBcdTAwMDBcdTAwMDGlXHUwMDAy6pVgXGLDyqjH6jdFXHUwMDE0wmH2KTBcdTAwMWVcdTAwMGWYX89cdTAwMWXxqT61XHUwMDE59YkhoONcZnFJXHUwMDAw1uGEJnyCyOA6USZMZ8Ukq1x1MDAxM6BcdKd+qmr27GDOrokwLu1tVP8/mc8gwLNcdI3RXGbmXFw011x1MDAxMvXqal1cdFxyXHUwMDBiXHUwMDAzScFcdTAwMDVikmnJUFx1MDAxZVRcdTAwMGaEpqVcdTAwMDTCNPuVnFxiQenqXGJNYoNBiHQ8M4xw5cHeks6kgVx1MDAwNOeSUo6ZoHLy2V/B9Z2QhTLCZ/HZokJj2XxcdTAwMDZcZk1j2lx1MDAxYlx1MDAwMfR2MSRk+ZxiwVx1MDAxZNyA+TOTXHUwMDEwPGzognxWrzxGfKJASL1OTGtcdTAwMDRcbinhXHUwMDEzLlx0gzJ9XGZcdTAwMDGYXHUwMDFmm1iIn5rNZlx1MDAwNXJ2TYTwLCrbXHUwMDE4mm+ZUXSW6ngrtkKHtGNcdTAwMGbVaXmPrVx1MDAxYkfdbk95vP46vzLBl69mxkIqu9PvPzZ+/Fx1MDAwYlx0sVx1MDAwYuIifQ== ExampleApp()Screen()Header()Footer()

Note

We've simplified the above example somewhat. Both the Header and Footer widgets contain children of their own. When building an app with pre-built widgets you rarely need to know how they are constructed unless you plan on changing the styles of individual components.

Both Header and Footer are children of the Screen object.

To further explore the DOM, we're going to build a simple dialog with a question and two buttons. To do this we're going to import and use a few more builtin widgets:

  • textual.layout.Container For our top-level dialog.
  • textual.layout.Horizontal To arrange widgets left to right.
  • textual.widgets.Static For simple content.
  • textual.widgets.Button For a clickable button.
dom3.py
from textual.app import App, ComposeResult\nfrom textual.containers import Container, Horizontal\nfrom textual.widgets import Button, Footer, Header, Static\nQUESTION = \"Do you want to learn about Textual CSS?\"\nclass ExampleApp(App):\ndef compose(self) -> ComposeResult:\nyield Header()\nyield Footer()\nyield Container(\nStatic(QUESTION, classes=\"question\"),\nHorizontal(\nButton(\"Yes\", variant=\"success\"),\nButton(\"No\", variant=\"error\"),\nclasses=\"buttons\",\n),\nid=\"dialog\",\n)\nif __name__ == \"__main__\":\napp = ExampleApp()\napp.run()\n

We've added a Container to our DOM which (as the name suggests) is a container for other widgets. The container has a number of other widgets passed as positional arguments which will be added as the children of the container. Not all widgets accept child widgets in this way. A Button widget doesn't require any children, for example.

Here's the DOM created by the above code:

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1d+1PiyFx1MDAxNv59/oop95e9VWO2+/T7Vt265Vx1MDAwYnVcdTAwMWSfOFx1MDAwYnrnllx1MDAxNSFcdTAwMDIjLyGIurX/+z1cdTAwMWSVXHUwMDA0Qlx1MDAwMihB2DvU7qhJ6Jx0n/Od7+tX/vz0+fOa/9j21v75ec17KLn1Wrnj9te+2OP3XqdbazXxXHUwMDE0XHUwMDA0f3dbvU4puLLq++3uP3/7reF2bj2/XXdLnnNf6/bcetfvlWstp9Rq/FbzvUb33/bfI7fh/avdapT9jlx1MDAxM95k3SvX/Fbn+V5e3Wt4Tb+Lpf9cdTAwMDf//vz5z+DfiHXlmttoNcvB5cGJiHlGjlx1MDAxZT1qNVx1MDAwM1NccuGCXHUwMDAzIzC4oNbdxpv5Xlx1MDAxOc/eoMFeeMZcdTAwMWVa29y+K3b2XHUwMDFlL5t5wXP7f0C+d212w7ve1Or1vP9Yf65cdTAwMDe3VO11vPBs1++0br1CrexX8TxcdTAwMWQ5Pvhet4VVXHUwMDEwfqvT6lWqTa/bXHUwMDFk+k6r7ZZq/iNcdTAwMWVcdTAwMTNkcNBtVoJcIsIjXHUwMDBmwVx1MDAwNeBQJkBcdTAwMTCquDBE8cHp4PvUYYRqo1x1MDAxOFBcdTAwMGVajtq11apjO6Bdv5DgXHUwMDEzWnbtlm4raF6zXHUwMDFjXqOhZGjkmfuvT6uZwzlcdTAwMDPA21x1MDAxOGZcYojBJVWvVqn69lx1MDAxYVx1MDAwMEdcdTAwMTMutVx1MDAxMs+3ipjiXHUwMDA1TUJcdTAwMDVQ4FLqsEmtXHUwMDAx7f1y4Fx1MDAxYv9cdTAwMWSt0qrbab9U3VrX/lx1MDAxMTHe2r0z6lhR54o0u5bnXHUwMDFi5sdlsVrq7JOTk8vCuVuQg7KGPNH3XHUwMDFl/LXBib++pFx1MDAxNXt4uv90kXso+p1tb72a71x1MDAxNlx1MDAxNfCb8cW6nU6rP225XHUwMDE5mTv3Yoeu/jLtXHLDYl9+XHUwMDBim73XLrvPwUulJFxco1trqfXgfL3WvMWTzV69XHUwMDFlXHUwMDFla5Vuw3j/XHUwMDE0sTeGMkN2RiGG6NGjr1x1MDAxMCOAXHUwMDE50IqEXHUwMDBlO1x0YtJreVkhRqVBXGZFXHUwMDA0XHUwMDAywoFTot6PMX7HbXbbblx1MDAwN1x1MDAwM3dcZs6oyThcdTAwMDNxXFxB41xmXHUwMDEwakK754Yr8/TO0Fx1MDAwYlpNP1978oKyXHUwMDFjQTUnIFx0KGOIUENX5dxGrf441LCBXHUwMDFio+VcdTAwMWLt9q//iFZ110NcdTAwMTOCMsXQxVx1MDAxYvVaxfr5Wlx0XHUwMDFmyutcZoWAX8OUPbigUSuX61x1MDAxMX8soVx1MDAwNS6W2dmfJn22OrVKrenWz4dcZkxccsmOV/KffXJMXFxcbkWS4pIqIyhorvTUgVx0rdO9XHUwMDFi/fitcX2+sd+oXHUwMDFlVcVV/2TZXHUwMDAzkyrHXHUwMDAwZ1x1MDAxNORzYFx1MDAwZcUloO8oYFxu/3/O/dmFpVx1MDAxOFx1MDAxM4daOVx1MDAxOKacKVx1MDAxMYtHrlxyIZpcbj3/eEzLcGfyxtvtq1x1MDAxZtWzwl3ha+OSi527+vLm+cLvf9z3v/Ltw69cdTAwMWRR3n2EXHUwMDFl8O2EhDxTuVAo7+/lbkuHeoPT80b9eKd5WZlDuVx1MDAxOVVvRsWq+9pe7m6HNjo7V+Vt/1x1MDAxZe6P6/OoXHUwMDA1Tjpt2rwvXHUwMDFkXHUwMDE1clx1MDAwN0e6sF1oXHUwMDFk+lx1MDAwN+8qd1x1MDAxMo9cdTAwMWFfQVOae/2jsXFSv5dU+OubZVM5OGo9PE5n7rLwMy7V6NFBXHUwMDFlkIgykis2fVx1MDAxZUh3tyXNXHUwMDAzKK1S8lx1MDAwMNdcdTAwMGXD9jCv/Cy7PMDH8TFcdTAwMTbDf6VBWDKTgc7Lmo9cdTAwMDFcdTAwMTk6msK/8qWO5zWTKJhcdTAwMWG6fm5cdTAwMTRsXHUwMDAyi1x1MDAxOaVgXHUwMDAzXHUwMDFiU1x1MDAwM+858MdEnoakwFx1MDAwM+CCXHUwMDEzXHUwMDFkYdyT4i49i35I3FEyMfCMcDTlhqlxgYcs1GFcXFx1MDAwZlximMgs8IjDjSQmyrVcdTAwMDbxx7hjXHUwMDE0SCEhTsRcdTAwMDQzmnEuYfZAXGasW3Qgdn2342/WmuVas4Inw3yGwVjq2fuuXHUwMDEzhzAqXHUwMDA1R3RRXHUwMDAyQZGE1W5cdTAwMWbPbdtmQ1x1MDAxMSlQR1xuIVxyXHUwMDA3yU3kipfeyDRF83LxILGuec3yRKOIUehPXG6lXHUwMDFj/idUqExcdTAwMDZWgYMyLyDMQe9cdTAwMThlKsms8VFcdTAwMWUzq+52/a1Wo1HzsfpPWrWmP1rNQX1u2PCuem559Cw+VvTcKFx1MDAwZbRticOkO/ztc1x1MDAxOCnBXHUwMDFmg9//+2Xs1etxXHUwMDE3tp+I84YlfIr+fJN0pJSz0cOv0MUlw3yKXjo1dDVcdTAwMGVcbnDyXGJ721vffpQ2L2BcdTAwMDOKXHUwMDA172Ohi09CLkaZI1x1MDAxOJJcdTAwMDFuOOJTRIpcdTAwMDVfZ9rRRFx1MDAxM2OoXHUwMDA0TcjSSEfJXHUwMDA1YGTDgnuIycPW9UFLXlx1MDAxZJT13f3ZUfuGnlx1MDAxZPKfynFeyjGj6l2tYucvSCdcdMfxXHUwMDBmXHUwMDEyXHUwMDE2+4qzXHUwMDFmLfAoiYDEXGJaY1wipVxiUtN3wKe33rKCtU5cdTAwMDNrRVx1MDAxY8OxNZShXHUwMDAxWC+BwFx1MDAwM6Q7iohcdTAwMGbocDczeOP7XHUwMDA03lx1MDAxZdJcIq+zYIE3gWuMXG68gY3v4Epa0KTo41x1MDAxMt2NSja9zDvYOqnkjqhqyZvzvavTi2qp+vWDx78mhlx1MDAxZlx1MDAxMlBkQ1x1MDAwNFx1MDAwNZ4kmqlcYvlcYr7OiaMkRiVjWiFtJ9nJvFx1MDAxOclcdTAwMTJcYoPijrxB3L2HK12RnD686lx1MDAxNVx1MDAwZp425FaDbPbORK/4kyvNiytlVL0/i/2QvvvxXHUwMDBmMjNcdTAwMDWbJem9jYIpZUZcdTAwMGZcdTAwMGYmQVx1MDAxMKlcdTAwMTjRfHpcdTAwMGWW3nxLmlx1MDAwNFx1MDAxOE1LXHUwMDAyXG5cdTAwMWNtXGZRQFnWSWA6XHUwMDBlXHUwMDA2XHUwMDA0oVx1MDAxZqniR3SyL46D5Votf+FcdTAwMWNsXHUwMDAyh1x1MDAxOeVgXHUwMDAzXHUwMDFiUyMvuZNd8sTIk8LYvs7pIy9dZC7p6JamXHUwMDBlhlx1MDAxNuGGcEKJNEORx4hxJDOCaFx1MDAwM1ooynl2kWdQaFx1MDAxOabBSKBcXI2LQ2rn63BGXHUwMDE4wlx1MDAwMGgqqVx1MDAxOY1LKvCo5lGVtrA+9zfFZXKf+1x1MDAxNH3SYcqLdoZzQTUxQiCY2s68cETyc9hDLzmAeVx1MDAxZLJk7OWCyX3uQ0alq6Vho7CtXHUwMDEwuLlcdTAwMTRcdTAwMWFcdTAwMTRmu7hNXHUwMDE0XHUwMDFjXHRCXGKtJeVcdTAwMTRcZo/ZtFJcdTAwMWTuid5sP3E/XHUwMDBly/tcdTAwMTT9OTOcof8ndr1TJrF6XHKfYT5lOmVbTkCTTDvAqMK4tLOhR1x1MDAwMY1Sh1wi2+DUgLQ/Mlx1MDAwNDTJXHUwMDFkaWPMoFSkhof9SpFZ29QhdpYrXHUwMDA1jf9Hc81gNpfSXGZcdTAwMTBkVlx1MDAxZtBe0YA41FBlQYrhXHUwMDAzXHUwMDAzeiRcdTAwMGKD41x1MDAwNVxyXHUwMDE0Nlx1MDAxMuproGiMXHUwMDE2KP4jV7xlsG7SXHUwMDE4XCJxbIojXHUwMDFjMHtcYkFcdTAwMDVXMZO0g4RcdTAwMDBcdTAwMTDFlE2ETOskk8ZcdTAwMTOYlYazRFdcdTAwMGVOxpx4XmiGzZ9cdTAwMDRmKFx1MDAwNdArpJl+XHUwMDFjMX0u1pJOgUA1ZFeYIEkzXFxhK1xmz1xyt8OMhklCMckwxSC7rmnpXHUwMDE4XHUwMDE0XtyuMEF6xWSYZFwi5Fxmg1xiXHUwMDA1kUJYZaBRxsWwjGDcc6pM+IyrimVvJmdAQFAk2kZgXHUwMDE2UiQ+e8Jgi2rkb1x1MDAxYUOKMKNeqcGM3Kx0ly/+vv/Acr9fX5y6hzfXh+R4N8EmgsmQXCJhMVx1MDAwMVx1MDAxZFHAY0ZR7mhO0dHAXHUwMDAwIKc0sNJwtp7ozvZcdTAwMTN35Fx1MDAxOfEstcNcdTAwMWaISOzroUhJXHUwMDE4g1kmdqW385KiXHUwMDFh5n5cdTAwMDdcdTAwMTBcbjhcdTAwMTOgsFx1MDAxMUKkeJ5cdTAwMWVhXHUwMDFjhjrBKNQoQI1cdTAwMWGxa55cZi1EzLDHP0wpr7jFgGk0KFSkXHUwMDBi6ep/8vvC2z2R3fLvKt8/qNxvbVx1MDAxZW/+7OqfV1d/RtW7WsVmNU9/tWrhXHUwMDFk0/RcdTAwMTPKnTQyMf5BpjT3x7fqU1x1MDAxOfLFYt4tnpeqV4WN+lnCmo2ZXHUwMDFh7eBcXO+c9HKHXHUwMDA3Z113vXrQudl4qO9OV+5rXpwjXHUwMDBiS02xyatJVeJyXHUwMDA14NoopF7TS4Z0d1vW5FxuLC25XG6kcotJrmJMco0ssHxNrlx1MDAxMrOrnbKbQXbNeiSFyqGjKSMpW69jXHUwMDFjv35v2lx1MDAxM7Xyv77bjVx1MDAxN+qtyve1783xIyxcdTAwMDKGylx1MDAxOVxmoNS9m2Hnn2l8ZVx1MDAwMmNcdTAwMWNcdTAwMWRfmWj5O6gwJ4mDL+jAXHUwMDAwis6wvcTR3vXm7Teyv5477uzU1pv89NvjzrJcdTAwMDer5Kg17LwzrpRcclZcdTAwMThcblZhuGNQh2CsZlx1MDAxZKxcdTAwMTFNXHUwMDFlMuF4b6TQilx0XHUwMDEzXHUwMDFkgF1cYlx1MDAxNW7vNftcdTAwMTe3x9c7ZVLNre+vXHUwMDFmk3rx8CdcdTAwMTWeXHUwMDE3XHUwMDE1zqh6V6vYrKjwatXC/KlwRuZOYtjjb5g9XHUwMDEzTi13v3/WKPTFjr7eJFx1MDAxN/6RPKrRq4TJ7TOVu9voXHUwMDFll76dNd2LgtgvbNx5lav1/nTlLlxyc+cscVwirFCEK1x1MDAxMd1cdTAwMTllXHUwMDEyXHUwMDE5SHe3pSVcdTAwMDNcIoVcZkhiXHUwMDFjvlx1MDAxODKgx5CBMczdjklcdTAwMTi7XHUwMDA2/O/M3PeQXHUwMDEwP1lcdTAwMGVcXP/VXHUwMDFlfybBpbrb7XpdZMLXPd9vNbuLJvFcdTAwMTPIbmyi+lxmXHUwMDBm8Vx1MDAwZT4vk/eM4VRwXCJcdTAwMDUjU4dweX/f39l+2uHXrFuu7l+oRv/SXfZcdTAwMTBcdTAwMTZ2N1x1MDAwMCUkl1JcdTAwMDCG8PB4nZLS4YBS1273xIBnuJXTlHzeLmoxoFx1MDAxNzyJfbt597he8lxi9b8+Ulwi+24+d978SefnReczqt7VKjYrOr9atZBcdTAwMTWdX61aqJvNLX29u8W3pPTy51x1MDAwN1x1MDAwZpVcXG5KevxG9TH+Qf6P2Hx0ksdo11x1MDAxZWFMaVxyYno6n+5cdTAwMTfLylx1MDAwNVx1MDAwNEvjXHUwMDAyii6KXHUwMDBijKPzKsZcdTAwMDVcZppIuFAruKp0eja/XHUwMDE5MN1cYlx0/r524SHx/fL8173bqblNXHUwMDFmKXG3Vyrh0yXzejVcXPiceP1cdTAwMDTSO8rr3/Y472D4KD6TwtpQ6+UgZpiR97Tlnp6ap7PKZvGylds4h0b+dNmjWlx1MDAxOe1IXHUwMDAzxE5cdTAwMDWFmEhXUjh2wi5+1FIwfCCUMY46fcG7QbKvxW5+s974arbyxe5cdTAwMWb8x5Ms/tzTY25cdTAwMTQ/o+pdrWKzovirVVx1MDAwYllR/NWqhflT/IzMnaRcdTAwMWPG33BKa98xvvDy28crh2h/cmxcdTAwMDVcdTAwMTNcdTAwMDXgTNLplUN6+y0px9CEpXFcZkVcdTAwMTbFMaZTXHUwMDBlVFx1MDAxOY5qLjqz6v9DOlx1MDAxY7XGUG1cdTAwMGZcdTAwMDOrs2jdMIFKT6FcdTAwMWImPUtqMKeLXHUwMDA2XHUwMDAxyX1cdTAwMDFcdTAwMDJQz3NcdTAwMDPTr0ms9o+L9fouK/hf20y15GX+zr1e9ohmmjtcdTAwMTiqWmKYXHUwMDA0fVx1MDAwMWIopIWRjkBtpbnKXFw2jFm6M2ZgwFx1MDAxMIFBRt+yd+l7ZMN957Kfq2z+qKpK+3ZrY+Obq9tnP2XDvGRDRtW7WsVmJVx1MDAxYlarXHUwMDE2spJccqtSXHUwMDBik3j4+Fx1MDAxYi4hX1x1MDAxNjqRL2thqN1cdTAwMGV++uSaXs1Lm1xcTVpylYQuKrnqMcl1XGZfJswgX1ZvWFx1MDAwNrs6dDnvu8hgXyaNn37byZ/vXHUwMDFmXHUwMDFmfbF/jM4+uet5Xb+WOokmXHUwMDFi0jyBSca283/rXHUwMDEzpcZ14uJ3mVx1MDAxONacXHUwMDFhLYDNsJAlfcXQkoZ1sP0/U6hcdTAwMTCQjGL0XHUwMDBl78sqXHUwMDE4c4BJxlXmKlx1MDAxOIyDXHUwMDE1zijeXGLsmmFcdTAwMTVcdTAwMGZyyZ3xu0RyorRcdTAwMWHaROpvseR9luXlQjBit8IhWIfcaFx1MDAxYbnqdTtcIq00XHUwMDExxK7GJnY19stcdTAwMDVcdEveh1x1MDAxZmOVlp0ne5L9XGZ8KCznU/Tn7JtcdTAwMDFFXHUwMDEy/ygz0NpcbrxcdTAwMTmW16RPiV5SXGKRXHUwMDAwXHUwMDBlXHUwMDE3WNtCWM+iw3tcdTAwMDFcdCZcdTAwMWNcdEoxXHUwMDA1zDBcdTAwMDZmxK45QlxikY7dt9A6ONGM6DFcdTAwMTBcIrD57Yt77IxcdTAwMDBjKJD4XHUwMDBiRYDYxXJcdTAwMWM+4IVcIktcdTAwMDEmxOFKcFCSSqVcdTAwMTFRTPw1XHUwMDFm2lx1MDAwMWk4J4Tad3wgXHUwMDE3TMeSJJPS59eOmKSEXCJUMlwiqNAmvpdcdTAwMTF37I5mTHGBXHUwMDE2XHUwMDBiyVTMpFVcdTAwMDKxZFe2n7hcdTAwMTPPXHUwMDBizCRJ3jaDM/vuVD3DsED6QMmSoplcdTAwMTLSXHUwMDAxu1x1MDAxZj/XytjN3IZljlx1MDAwMcegrmBcdTAwMTK4nYmhR+ya42ZAxNFcdTAwMDKBk1x1MDAwMlD7hoKw2kM+JFx1MDAxY5RiRiq7ZT5nXHUwMDEw31ODW9/h5i1vjV1mLJtcdTAwMDE4wM7+4kiJhGG2VaPvSVx1MDAxYeyJSKyu5VxiL1x1MDAwNONN67ehWfrowDBbXHUwMDAzKkBSXHUwMDEwOliCQlh8IzTlIFx1MDAwMCuqXHUwMDA3RGU1gSzRi4OTo/47L1x1MDAxOItsZVx1MDAxN+NklGKiYDO8TTF9ouiyolx1MDAxOJNcdTAwMGV7fte1XHUwMDA0VFx1MDAwNsNbPVtcdTAwMTSzkk4pad/LrNiIXfNDMbxcdTAwMTFcYlRnRnOmkVSNQzHlgFx1MDAwNo2pXHUwMDA1qN2bKbZcdTAwMDSKoZnYZvxvRsimXHUwMDA2seC9YkhcdTAwMDFcdTAwMDBZqaaMKjlmL0dKXHUwMDFkgdyIXGIuUPNgXHUwMDEye+N2s+lzI0esYsq+XCJcdTAwMDVcdTAwMThcdTAwMTNKo9aKb2kmXHUwMDFkXGZuJIlcdTAwMDSI3Ss0btMqYdl6ojPbT8yNk8Ds08tcctbcdtv2dnmD1kC/rpVf+lx1MDAwMMOnXFy7r3n9zXjc/XJcdTAwMTN8bMdXUJ9cdTAwMTaJPPusf/716a//XHUwMDAxsk3fXHUwMDAxIn0= App()Screen()Header()Footer()Container( id=\"dialog\")Horizontal( classes=\"buttons\")Button( \"Yes\", variant=\"success\")Button( \"No\", variant=\"error\")Static( QUESTION, classes=\"questions\")

Here's the output from this example:

ExampleApp \u2b58ExampleApp Do\u00a0you\u00a0want\u00a0to\u00a0learn\u00a0about\u00a0Textual\u00a0CSS? \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Yes\u00a0\u00a0No\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

You may recognize some elements in the above screenshot, but it doesn't quite look like a dialog. This is because we haven't added a stylesheet.

"},{"location":"guide/CSS/#css-files","title":"CSS files","text":"

To add a stylesheet set the CSS_PATH classvar to a relative path:

Note

Textual CSS files are typically given the extension .tcss to differentiate them from browser CSS (.css).

dom4.py
from textual.app import App, ComposeResult\nfrom textual.containers import Container, Horizontal\nfrom textual.widgets import Button, Footer, Header, Static\nQUESTION = \"Do you want to learn about Textual CSS?\"\nclass ExampleApp(App):\nCSS_PATH = \"dom4.tcss\"\ndef compose(self) -> ComposeResult:\nyield Header()\nyield Footer()\nyield Container(\nStatic(QUESTION, classes=\"question\"),\nHorizontal(\nButton(\"Yes\", variant=\"success\"),\nButton(\"No\", variant=\"error\"),\nclasses=\"buttons\",\n),\nid=\"dialog\",\n)\nif __name__ == \"__main__\":\napp = ExampleApp()\napp.run()\n

You may have noticed that some constructors have additional keyword arguments: id and classes. These are used by the CSS to identify parts of the DOM. We will cover these in the next section.

Here's the CSS file we are applying:

dom4.tcss
/* The top level dialog (a Container) */\n#dialog {\nheight: 100%;\nmargin: 4 8;\nbackground: $panel;\ncolor: $text;\nborder: tall $background;\npadding: 1 2;\n}\n/* The button class */\nButton {\nwidth: 1fr;\n}\n/* Matches the question text */\n.question {\ntext-style: bold;\nheight: 100%;\ncontent-align: center middle;\n}\n/* Matches the button container */\n.buttons {\nwidth: 100%;\nheight: auto;\ndock: bottom;\n}\n

The CSS contains a number of rule sets with a selector and a list of rules. You can also add comments with text between /* and */ which will be ignored by Textual. Add comments to leave yourself reminders or to temporarily disable selectors.

With the CSS in place, the output looks very different:

ExampleApp \u2b58ExampleApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258aDo\u00a0you\u00a0want\u00a0to\u00a0learn\u00a0about\u00a0Textual\u00a0CSS?\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u00a0Yes\u00a0\u00a0No\u00a0\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

"},{"location":"guide/CSS/#using-multiple-css-files","title":"Using multiple CSS files","text":"

You can also set the CSS_PATH class variable to a list of paths. Textual will combine the rules from all of the supplied paths.

"},{"location":"guide/CSS/#why-css","title":"Why CSS?","text":"

It is reasonable to ask why use CSS at all? Python is a powerful and expressive language. Wouldn't it be easier to set styles in your .py files?

A major advantage of CSS is that it separates how your app looks from how it works. Setting styles in Python can generate a lot of spaghetti code which can make it hard to see the important logic in your application.

A second advantage of CSS is that you can customize builtin and third-party widgets just as easily as you can your own app or widgets.

Finally, Textual CSS allows you to live edit the styles in your app. If you run your application with the following command, any changes you make to the CSS file will be instantly updated in the terminal:

textual run my_app.py --dev\n

Being able to iterate on the design without restarting the application makes it easier and faster to design beautiful interfaces.

"},{"location":"guide/CSS/#selectors","title":"Selectors","text":"

A selector is the text which precedes the curly braces in a set of rules. It tells Textual which widgets it should apply the rules to.

Selectors can target a kind of widget or a very specific widget. For instance, you could have a selector that modifies all buttons, or you could target an individual button used in one dialog. This gives you a lot of flexibility in customizing your user interface.

Let's look at the selectors supported by Textual CSS.

"},{"location":"guide/CSS/#type-selector","title":"Type selector","text":"

The type selector matches the name of the (Python) class. For example, the following widget can be matched with a Button selector:

from textual.widgets import Static\nclass Button(Static):\npass\n

The following rule applies a border to this widget:

Button {\nborder: solid blue;\n}\n

The type selector will also match a widget's base classes. Consequently, a Static selector will also style the button because the Button Python class extends Static.

Static {\nbackground: blue;\nborder: rounded white;\n}\n

Note

The fact that the type selector matches base classes is a departure from browser CSS which doesn't have the same concept.

You may have noticed that the border rule exists in both Static and Button. When this happens, Textual will use the most recently defined sub-class within a list of bases. So Button wins over Static, and Static wins over Widget (the base class of all widgets). Hence if both rules were in a stylesheet, the buttons would be \"solid blue\" and not \"rounded white\".

"},{"location":"guide/CSS/#id-selector","title":"ID selector","text":"

Every Widget can have a single id attribute, which is set via the constructor. The ID should be unique to its container.

Here's an example of a widget with an ID:

yield Button(id=\"next\")\n

You can match an ID with a selector starting with a hash (#). Here is how you might draw a red outline around the above button:

#next {\noutline: red;\n}\n

A Widget's id attribute can not be changed after the Widget has been constructed.

"},{"location":"guide/CSS/#class-name-selector","title":"Class-name selector","text":"

Every widget can have a number of class names applied. The term \"class\" here is borrowed from web CSS, and has a different meaning to a Python class. You can think of a CSS class as a tag of sorts. Widgets with the same tag will share styles.

CSS classes are set via the widget's classes parameter in the constructor. Here's an example:

yield Button(classes=\"success\")\n

This button will have a single class called \"success\" which we could target via CSS to make the button a particular color.

You may also set multiple classes separated by spaces. For instance, here is a button with both an error class and a disabled class:

yield Button(classes=\"error disabled\")\n

To match a Widget with a given class in CSS you can precede the class name with a dot (.). Here's a rule with a class selector to match the \"success\" class name:

.success {\nbackground: green;\ncolor: white;\n}\n

Note

You can apply a class name to any widget, which means that widgets of different types could share classes.

Class name selectors may be chained together by appending another full stop and class name. The selector will match a widget that has all of the class names set. For instance, the following sets a red background on widgets that have both error and disabled class names.

.error.disabled {\nbackground: darkred;\n}\n

Unlike the id attribute, a widget's classes can be changed after the widget was created. Adding and removing CSS classes is the recommended way of changing the display while your app is running. There are a few methods you can use to manage CSS classes.

  • add_class() Adds one or more classes to a widget.
  • remove_class() Removes class name(s) from a widget.
  • toggle_class() Removes a class name if it is present, or adds the name if it's not already present.
  • has_class() Checks if one or more classes are set on a widget.
  • classes Is a frozen set of the class(es) set on a widget.
"},{"location":"guide/CSS/#universal-selector","title":"Universal selector","text":"

The universal selector is denoted by an asterisk and will match all widgets.

For example, the following will draw a red outline around all widgets:

* {\noutline: solid red;\n}\n
"},{"location":"guide/CSS/#pseudo-classes","title":"Pseudo classes","text":"

Pseudo classes can be used to match widgets in a particular state. Pseudo classes are set automatically by Textual. For instance, you might want a button to have a green background when the mouse cursor moves over it. We can do this with the :hover pseudo selector.

Button:hover {\nbackground: green;\n}\n

The background: green is only applied to the Button underneath the mouse cursor. When you move the cursor away from the button it will return to its previous background color.

Here are some other pseudo classes:

  • :disabled Matches widgets which are in a disabled state.
  • :enabled Matches widgets which are in an enabled state.
  • :focus Matches widgets which have input focus.
  • :focus-within Matches widgets with a focused a child widget.
  • :dark Matches widgets in dark mode (where App.dark == True).
  • :light Matches widgets in dark mode (where App.dark == False).
"},{"location":"guide/CSS/#combinators","title":"Combinators","text":"

More sophisticated selectors can be created by combining simple selectors. The logic used to combine selectors is know as a combinator.

"},{"location":"guide/CSS/#descendant-combinator","title":"Descendant combinator","text":"

If you separate two selectors with a space it will match widgets with the second selector that have an ancestor that matches the first selector.

Here's a section of DOM to illustrate this combinator:

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1dWXPiSFx1MDAxMn7vX+HwPOzsRKOp+5iIiVxyfODbbjdu293bXHUwMDEzXHUwMDBljGSQXHUwMDExXHUwMDEylmSwPTH/favARlx1MDAwMiRZXHUwMDFjYmCnefAhQSmVyuP7KiuLPz9sbGyGz1x1MDAxZGvzt41N66lec2zTr/U2P+rjXctcdTAwMGZsz1WnUP//wHv06/13NsOwXHUwMDEz/Pbrr+2a37LCjlOrW0bXXHUwMDBlXHUwMDFla05cdTAwMTA+mrZn1L32r3ZotYP/6J+ntbb1e8drm6FvRFx1MDAxNylZplx1MDAxZHr+4FqWY7UtN1xm1Oj/Vf9vbPzZ/1x1MDAxOZPOt+phzW04Vv9cdTAwMDP9UzFcdTAwMDFcdTAwMTEg44dPPbcvLVx1MDAxNFx1MDAxY1x1MDAwYimAkMN32MGOumBomer0nVx1MDAxMtqKzpj9a1x1MDAwNVx1MDAwN7x5b1Pil1x1MDAxOWx/2nX2v5Ur0XXvbMephs/OQFx1MDAxN7V689GPSVx1MDAxNYS+17KubDNs6quPXHUwMDFkXHUwMDFmfi7wlFx1MDAxYaJP+d5jo+laQTDyXHUwMDE5r1Or2+GzPlx1MDAwNsDw6EBccr9tREee1H+UY4MxTlx1MDAwNGFcdTAwMDBxQfDwrP48ZtJcdTAwMDBEQCoxoOokXHUwMDE5k2vbc9SzUHL9ZDHOXHUwMDAxiCS7rdVbXHIlnmtcdTAwMGXfXHUwMDEz+jU36NR89cSi9/Xe7pix4bGmZTeaoTooRHQ9q693iFx1MDAwMFaCMFx1MDAxYd2FvkrnwOxcdTAwMWLBXHUwMDFm43pr1vzOq342XHUwMDAz/U9MQi3c7rhcdTAwMDXFrSj2bKl7V7rCsve1aVx1MDAxZcKb26dcdTAwMTPGYDBcdTAwMWNrxORC6yncXHUwMDFjnvjrY9aw7W9fji727ra64ZfLw9557+Wk0blNXHUwMDFltub7Xi/vuFeHl93eMdk5OfapufeMXHUwMDFlXHUwMDEx2WFcdTAwMGJcdTAwMThcdTAwMTddmVx1MDAwN/uVVv1ElFx0vGg7Z7vut8ZcdTAwMDLGLUi96zXs8ePOPpDXXHUwMDA3L0934qhSMZ9uat3eXHUwMDBm5S5m2H1rXHUwMDBmXHUwMDA33bpdvtm+XHUwMDBi7ytcdTAwMDe75fOr47mUO1wixce8N5JT3Obxdlx1MDAwZmBcdTAwMGK07zudg6Pj7svN+U6YT9zXv6JQ+Ngxa4OspYIsJJBcIiZcdTAwMDSPwq1juy110n10nOiYV29Fie5DTOCJXHUwMDE0O3L/I9lcdTAwMTWK8cNv2ZVzKiHjMMpZ7yXXbLNY2eQqspKrIFx1MDAwNl1OcqVcdMmV8vHkilR2RVx1MDAxY9LosS0sty7SXHUwMDFho4fuuWHVfunDNTZytFJr287zyHPrm6mSdFudrtmu5f/83dUnbPP375umXXO8xvfN7+6/41pcdTAwMGUsJY1cdTAwMWWeopFxyo7d0Ca+6Vh3o7ZcdTAwMWbaXG6oXHUwMDBlT7dt04xDz/rbtVx1MDAwZvJcdTAwMDBGz7dcdTAwMWK2W3Mu8kqe6abZSJhClOarkCtcdTAwMTAsOVx1MDAxMjy3s55fPDjWS3nr8XHr5MA5Obt2ryv3f6+z0nd9lVx1MDAwMYNCXHRcYodcdL5KMTOkgJwqV9W+SlN9XHUwMDE19F9z+KpcdTAwMDSTvirYhK9cdTAwMDKAMYNAXHUwMDE24KxZiem+etjYvfDc7Va9xfyn1sG31kNKYvpcdTAwMDGEp1x1MDAxZrcg9a7XsEVcdTAwMDHh9dLChd/c6d12rYtcdTAwMTBcdTAwMWY6XHUwMDEwiZfWeWc+xLqOWlg8blx1MDAxZoyLn1x1MDAxZkpfkajUQ9JcdKrW887Z9lx1MDAwM1/AuGZcdTAwMWJcXG57h597leujbafb8XdvXHUwMDFiuFCekaz4aNjXvzJcdTAwMTFcdTAwMThmJMr/RfFcdTAwMDGKU/lcdTAwMDCkilx1MDAwYlxiRkGUV9/DXHUwMDE42fa2qlx1MDAxOINmYVxmXG5ccrJcdTAwMWOMIVx1MDAxMjDGJFx1MDAxZpCAXHUwMDEzIFxiL2CubZHWOFx1MDAxZlx1MDAxZthXMPtFI2vnZ318XHUwMDAwretOLVxirEDh69vHMPTcYNnU4Fx1MDAxZFx1MDAwND1OXHKmuYlM581mXHRcXKbOlzPMXHUwMDA1Izj/bPmX8PzCe3q+2eptnZ1+uXj5/PKpzFL8d8xcdTAwMGb/LjpPODGApFxiXG7JuXJfQkb8lyNqKCpcdTAwMGJcdFx1MDAxM8rJXHRcdTAwMTfz+O9PXHUwMDEw3VxuoajxzFxmXHUwMDAxXHUwMDEyrKRcdTAwMDScocW7b1ZcdTAwMDZkrOneyy77elolnYY8wkfHj5X5gcBcdTAwMGaGUKh612vYolx1MDAxOMJ6aaEohrBeWnDk1ra43dsm24xZ1Yujp0alssK2sHyCkHwjOcWtXHUwMDFmnV6QzrF/+tXfvjWt44483vuyuELEUoiHiNWWx4lcdTAwMDeTXHUwMDEwXCKJRX7ikW1cdTAwMTcrWokgXFxmQlx1MDAxNypccrwo6DI99YBcdTAwMTFsfJvepLpARPCyS1x1MDAxMUulXHUwMDFlW31Y/vP3za+WwubJ9Fx1MDAwMpKRj1xy+UNd3Y7lz04w3oHf41x1MDAwNGNcXNRMR8wmXHUwMDExXCK2pGbMXHUwMDFiXHUwMDExXHUwMDA0XGZDOUVZkMPKWYvsud3mXvUlaODnXHUwMDEz379ebVx1MDAxNsEkMqBSXHUwMDAye3XF0UlcdTAwMDBNXCKo1GW41SBcdTAwMTFAeyElYMkk4iGQlaP2WZVcdTAwMWOet2jFNe/RzqfLXHUwMDFmJGJRJKIg9a7XsEWRiPXSQlEkYr20UFx1MDAxNIlYLy0svijyXHUwMDFlN0m+kWjY17/+flx1MDAwZSFlOodAurTBXHUwMDEwyT/5mf38VpRDMMmzgFx1MDAwYtVcZmNBwGVcdTAwMTFcdTAwMTSCS0VcdTAwMWZcYohhmv9nXG5x6i2bQbxcdTAwMDO9U1x1MDAxOcRA0kwvXHUwMDFjXHUwMDA0l1x1MDAwNDdkIL2EXGIh1oYp83thdm15NUuITFx1MDAxN1x0XHRjXHUwMDE0YFwigEQjPkhcdTAwMThVJ1x1MDAxMZVcdTAwMTJDKinjhfkgMDBcdTAwMDZUcixcdTAwMTCmXGIozdNJn2TSgMopKIVCxVx1MDAwZS7wuItcIlx1MDAwMDliKFZVyu2ifVlndVHC4UxcdTAwMGJcdTAwMGWDsOaHW7Zr2m5DnYwy3VsnSp51fX2nrj9cdTAwMDZ9LVx1MDAwMkY5xVxcqVx1MDAxMFx0XHUwMDE5X1x1MDAwM6p1UetoTmZgSVxiXHUwMDAxQFExiFV4fX3DMONuWq75vkzZXHUwMDA1xZhMJSVcdTAwMTTmXHUwMDAwqStcIlxuoZIv8rmhUMjAnDMpKEVcdTAwMTLolDAhlFNcdTAwMGLCba/dtkOl+0+e7YbjOu4rs6w9vWnVzPGz6qbi58ZDQkePOMpcIqO/Nlwin+n/M/z7j4/J7043Zv2aMONovFx1MDAwZvHfU0czXGJcdTAwMDFcdTAwMWU/PKynXHUwMDAySFx1MDAwNIQyesN70SxcdTAwMWK8rSqmYMTgXHUwMDAyMimFJJLEVozrzzNMXGYqMUOIUPVcdTAwMDY2LtdcdTAwMDIxXHUwMDA1xFx1MDAwNkZQXHUwMDFiPEbKolGk92h2XHUwMDA0XHUwMDE4XGZzhFx0xFx1MDAwNKugXHUwMDE1W58xXGJnXHUwMDAyS6DcgswwVzJXOJtcdTAwMTVx5FxmZ7lDXHUwMDA3MFxiplx1MDAxOCCgMCDAkkNcdTAwMWPzo9fIXHUwMDAxocH0Ulx1MDAxZklcdTAwMDBcdTAwMDE6XHUwMDE0z1x1MDAxNs6ywceoTFxiS8l1L1x1MDAxZmBcdTAwMDKDJJlUXHUwMDAwoFKrknKOXHUwMDEw4utcdTAwMWTO0m1ZvyaseFHRLD5tO4HNJCfKmfNcdTAwMDez7CrZqlx1MDAwNjMsXGaKXHUwMDEwg0JiXHUwMDE1zMZjXHUwMDE5M1x1MDAwMMVcdTAwMWF8MJX1i1x1MDAwYmVSKmvGkimszFWWTmr9XHUwMDEwKuhcIqFbXFxcdTAwMTGEKn5MrPyCXG6VIKpS/z81lJU0JiCSqidJmYLZQrCYXHUwMDE3vcVccmyoUKf8jFx1MDAwM4XFlabfkMGUwSy7XHUwMDE2MyqVTodSuVx1MDAxM0QqpElcdTAwMDQnhKJcdTAwMDZcdTAwMDRUhVfOgZZrUqR1XG5lpVRj1q9cdDOeMpRl1qlcdTAwMTQySVxyZyq39d08f9HYadbA2XFnL7Shf0rK7Z3wyjxdcapJdEfMSFxmQ0hcdTAwMThQQlx1MDAwMlx1MDAxOCpwcSpNoJKCXHUwMDFiiKtcdTAwMDTCJ0BcdTAwMTfGkGDFhJfcXHUwMDEw7jlP1XLrwobynpZcdTAwMWN0ePxcdTAwMTLUq/PPwJ6cXHUwMDFmvHytPF2H/o5ValaDa47I3T+wQFWQelx1MDAwYlx1MDAxYZZ37f3Kwy5s+7s35k7YRd0zZ1x1MDAxMVogwO9At1s/vapcdTAwMWOdiqudK+8kPFpd7d7et8ufnC6DNCxtmbJxdKqyWqHlg+RcdTAwMWLJKe5cdTAwMWM91pnjfjmzzmXpwWqJ3a8v95/cs8vSXU5reEtb70CkXGJHXHUwMDE3VO6gNH2elWFFXHUwMDFmXHUwMDE0ssjfXHUwMDBlmm1uq5r86Hjyo9JcdTAwMTBcbm5cdTAwMTSZ+khC6kNcdTAwMTPTpopaKFx1MDAxNlFExluk4UXPNypsIDByNKOwUa37luX+nFLS4CPvX1hJ41x1MDAxZJQ2XtJcdTAwMTjKmOljqYSZp1ZcdTAwMTSFXHUwMDAy+EDPUud2sexYtpouRlx0MahmWFx1MDAxYzLEYUxcdTAwMWT93Vx1MDAxMaA0KKSFXCJNXGJcciggZlSRKSooTZj0IypcZqQgT6hcdTAwMWVRX+5cdTAwMTkqjPOWL/gsjpiTJGd7wUZ8bo1KrtNcdTAwMDElkjCMXHUwMDEyKDIxXHUwMDAwXHUwMDA1XFxcdTAwMDfN1zNTUuMpSilMPVx1MDAwZu04XHUwMDA0ci6jRqS4KEA339F+c1x1MDAxZJZyQqJ1YsbptqtfMauNXHUwMDA2+lx1MDAxMP8929pNlE6KXHTlXGIjPsVcdTAwMWPf+YV74MnHo93OTfPo7vzm0uJcdTAwMGbWiscsjJShgdGwNNgoTShwQFx1MDAxMFx1MDAxNJz2o1x1MDAxMlx1MDAxYpNogVEr305pSGFcdTAwMDTlnXyG2upcXFx1MDAxYqV99tF1q/n5oXlwWeru032zu3OfXGZ+fyzcnH7cgtS7XsNcdTAwMTa2UdpaaaGgYYvacKEgcVx1MDAxN0/i393XLfFGcor7cIvLVVRmdyc7XHUwMDE31WvvM7l7MlvrNTeAkEhdtVx1MDAwMLFUp5mg+SdcdTAwMDey7WJVUVx1MDAwME1GXHUwMDAxglx1MDAxOHhJKCDflm5QXGJcdTAwMDSgXHUwMDEyqID+jaInXHUwMDBi5tzTLbBN67bmL3/nhkxUm2tTt1x1MDAxMdHngOtZJXmqWFx1MDAxOVx1MDAwNGCKXd26veolsvDR4fVpq1x0fdn4dmydpHhq3feCoNSshfXm3++tXHUwMDEwXHUwMDE5irlob1x1MDAwNWNe2f88YobgXHUwMDA1zjJwPumpXHSdVlxiXCLMgIRL3tCtVVx1MDAwNtf7N4dP5llcdTAwMGaf7l1cdTAwMTDzmJXdXHUwMDFmgH1RgL0g9a7XsEVcdTAwMDH29dJCUZ1W66WFojqtXG5cdTAwMTJ38ds1XHUwMDE0JO57tCX5gjmlnYO2ZI5bpdVnSlx1MDAwZt2DXHUwMDFitoPuXHUwMDFliLtXPkwhhStLh6RM7WdXUiBAOEf5XHUwMDBiOdl2saJ0XGLyTIBFoIGLXHUwMDA0WCxcdTAwMDFgTVIhLplUkFx1MDAxN69h2XTqfjB9bMAnvm9cdTAwMWW4QVhznGXzoHfYQkp7WKrgmb6Z3i+W3rQpOVx1MDAwMphROMWOdZnrOlbTNSlcdTAwMTaGXHUwMDE4W7XwVl3tN1x1MDAxY1x1MDAxNemXRK/PXHUwMDFjxIVJXHUwMDA3JdKgjI0tJVx1MDAxY+5Dj1VUXHUwMDE1s1CheVdcdTAwMWbP5KqLLqyWgMGERIxJXHUwMDE1WpmEOKr3bkTlTIIxlVGFL6WyOir/XHUwMDFhVThLSfajXzHLicb4XHUwMDEw/z11nIgtZlx1MDAxYe/DYlxcb6RI85c1s7HSaoZcdFx1MDAwMqhBqMBcdTAwMTKqZIpZbHnBoK2UXHUwMDE4sexe5LQm0F83JaTuXHUwMDFml0xFZ0FcdTAwMTJyO4OGnmIl8vVcdTAwMTX74ozXuVx1MDAxNEgp5ZLPsjn+Klx1MDAwN5DsucXRXHUwMDAwotuwXHUwMDAwJZjplUSQxL4oYFx1MDAxOEK4IbBKglx1MDAxMFxyNJnQK5BrjUZ2pt9cdTAwMThpd1VcdTAwMDJJSlx0XHUwMDEyglx1MDAwYoZcdTAwMTPaXSfbW9cpZmWYr35NXHUwMDE47pTxK70kk1qRwSp5UEbzdypcdTAwMWP4n+Wlc/FcYus9XHUwMDFmXHUwMDA1l1x1MDAwN95J2fs8+ywvXHUwMDFh97VcdTAwMWMhLIpN03RfIVx1MDAwMlxmoWyLxSdz+ztTXGJmXGIwWIU0T+D66a5GXHUwMDExRYm7aaHoPqOFm5MrxpBe76ZcdTAwMDRZ8qKM4jdxXFzewtC2traNsGlcdTAwMDVWXCKbiYHGadhM6HXSqMzIrYzzlrg4s2FcdTAwMGYq0796R1LdcVx1MDAxZW98fs99s1x1MDAxZvVS3He25knMXHUwMDEzvLTvvlx1MDAxMlx1MDAxOFhcbiwwXHUwMDAzXHUwMDFjXHUwMDAzmr6pRY6vycrwYa5cdTAwMThcdTAwMTFcdTAwMWWjRNH2MsRQaVx1MDAxMlx1MDAwMVxyyrnKp5N7WWi6XCJcdTAwMTBe9lZcdTAwMTZcdTAwMDUjjuxsMJLbXHUwMDExXHUwMDAwXHUwMDEyXHUwMDEzhiBVhEVSjmLvXHUwMDFh9ktKQvHMi0Fz90lqYaR6XoLrfnxAJUlo3uRcbvFcdTAwMTJFoF43ROVcdTAwMTMyrVx1MDAxM/BIMF79Kk3a7YIgXHUwMDA3lOlcdTAwMWRcIpwjpU+FP3NHLeubXHUwMDBm7ZOtW8e8XHUwMDExqGe13Fx1MDAxNv+yv1x1MDAxNqBcdTAwMDNcdTAwMTNcdTAwMTW1JjGHOmAw2bf1oiCHiEJNXHUwMDA25GBSXHUwMDExOimW/SVcdTAwMDCrNbE/XHUwMDFm4vjF9Nx/hb9svKV6O1hcdTAwMDXgkSDVbPhcdTAwMDPFZsTH9+NcdTAwMDVUXHUwMDA1T8R5fk/OfvCrjD9cdTAwMTAxMMaC6u2zIIjvXHUwMDEw1XdoXHUwMDE1YYnsr+wqXGJ/MGlwyoWKXHUwMDE5XHUwMDEyYUXdo4npqKjBjMGU31x1MDAwNLFcdTAwMTCMU5XIwLJbUVxuhlx1MDAxZtl5YWNkwlx1MDAwMynlYaQ3RVx1MDAxMIIgNDndIYz+ZMeM4CP3LIdcdTAwMTJF51lcYlx1MDAwMVwiXGbr73lcdTAwMTJsQlx1MDAxNjiQN0madYJcdTAwMWSpNqtfpaG5pmGOXHUwMDBmr1x1MDAwM2/WOp1qqGxrqH9lvrb5XHUwMDFhqaO72+zaVm8ryav6L1x1MDAxZFx1MDAwMPt61GHG0vf4519cdTAwMWb++lx1MDAxZow/wb0ifQ== Container( id=\"dialog\")Horizontal( classes=\"buttons\")Button(\"Yes\")Button(\"No\")Screen()Container( id=\"sidebar\")Button( \"Install\")match these*don't* match this

Let's say we want to make the text of the buttons in the dialog bold, but we don't want to change the Button in the sidebar. We can do this with the following rule:

#dialog Button {\ntext-style: bold;\n}\n

The #dialog Button selector matches all buttons that are below the widget with an ID of \"dialog\". No other buttons will be matched.

As with all selectors, you can combine as many as you wish. The following will match a Button that is under a Horizontal widget and under a widget with an id of \"dialog\":

#dialog Horizontal Button {\ntext-style: bold;\n}\n
"},{"location":"guide/CSS/#child-combinator","title":"Child combinator","text":"

The child combinator is similar to the descendant combinator but will only match an immediate child. To create a child combinator, separate two selectors with a greater than symbol (>). Any whitespace around the > will be ignored.

Let's use this to match the Button in the sidebar given the following DOM:

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1daXPayFx1MDAxNv2eX+HyfMmrXHUwMDFhNL0vUzX1ylx1MDAxYl6It1x1MDAxOMdxXqamXHUwMDA0yFi2QFjIYDw1//3dxlx1MDAxOFx0kFx1MDAwNDiIQCaaqlx1MDAxOCTRurp9l3P6dvf8/W5jYzPstZzN3zc2naeq7bm1wO5u/mrOd5yg7fpNuET639v+Y1Dt33lcdTAwMWKGrfbvv/3WsIN7J2x5dtWxOm770fba4WPN9a2q3/jNXHKdRvu/5t9cdTAwMTO74fzR8lx1MDAxYrUwsKKHXHUwMDE0nJpcdTAwMWL6wcuzXHUwMDFjz2k4zbBccq3/XHUwMDBmvm9s/N3/NyZd4FRDu1n3nP5cdTAwMGb6l2JcdTAwMDJcdTAwMTLKx0+f+M2+tJgxxKhiWFx1MDAwZe9w27vwwNCpweVcdTAwMWJcdTAwMTDaia6YU5unT1x1MDAwZr3CcfehXFyq1iv+x6B81Ph4XHUwMDE1PffG9byLsOe96MKu3j5cdTAwMDYxqdph4N87V24tvDVPXHUwMDFmOz/8XdtcdTAwMDc1RL9cbvzH+m3TabdHfuO37KpcdTAwMWL24Fx1MDAxY0fDky9a+H0jOvNkblDIYkQxoZSgSpPhRfNrirhFJOiAIcSU0HxMqlx1MDAxZN+Dnlx1MDAwMKl+Qf0jkqtiV+/rIFxcsza8J1xm7Ga7ZVx1MDAwN9Bf0X3d1/dcdTAwMTVieO7Wceu3IZxUKnqe09e6VlJcdTAwMTKCWNQn5iGtw1rfXHUwMDAy/lx1MDAxY1farVx1MDAxZLRcdTAwMDbK2WybLzFcdTAwMDGNbHvj5lx1MDAxMzehWMdcdTAwMWU3gkcquttb+1x1MDAwNz1153/aqYf+zrCtXHUwMDExe1x1MDAwYp2ncHN44Z9fs5ptfLkslfdvtjvh5aej7nn3+bjeqiQ3a1x1MDAwN4HfnbXdq6NPne5cdTAwMDe2e/wh4LX9XHUwMDFleSRsVyygXXJVOzwo3leP1Vx1MDAxNsPlhne61/xSX0C7Oal3vZr98Lh7gPTnw+enXHUwMDFiVSpcdTAwMTZrT3/Zne5P5S6m2eYneVx1MDAxM1xcf9zzd0J9dl1raX5Fb1ZXucGjLu70xNlflza9Pqtf1vjV0dk3iTtcIsWvs75I1OzgU1x1MDAxNGFcdTAwMWZbNfslXHUwMDEzQuiG5KBcdTAwMDX8p/Twuuc27+Fi89HzonN+9T5Knu9i8k6k7Vx1MDAxMTlHMjZT46eHXHUwMDE5XHUwMDFicoMkSNCZXHUwMDEzdnb3rWrC5lx1MDAxOVx0myBLLidh84SEzaO8PEjYkKtcdTAwMDVcdTAwMTOYxSxjYVx1MDAxOXuRxlx1MDAxOPW531xmL9znvj2JkbNFu+F6vZFu61spSLpcdTAwMDOXbbfpXHUwMDA07782zVx1MDAwNbf2x9fNmmt7fv3r5tfmf+JqbjsgjWmek5F2tjy3bix803NuRk0/dFx1MDAwMftcdTAwMGUvN9xaLY5mq6/PPpxcdTAwMDWD+oFbd5u2V55V8kwvzVx1MDAwNtec41RX5eDHWjOGZ/bVx+1tzfZ6qHhyt9XcVzLsnlx1MDAwNEer7qtCWkQhRDGd9FUmqUVcdTAwMTlTXHUwMDA0XHUwMDEx3vfVXHUwMDFjnVWjSWdVYtxZmWZcdTAwMTBBOc7BV7Oy3e5D8S44Oz54OC/uljpN++7y41x1MDAxN/cnul5cdTAwMTS6zkm969VsXuh6vbRQXHUwMDBlbne7lY5TXHUwMDBl6ZGHiXq+P299+NdpIS8ycNCz5e1e62z3ktxXXHUwMDFmcIufXHUwMDFmXHUwMDFl11x1MDAxNtBuVVx1MDAxY5/xc9ooXHUwMDFl1IJS4aSwfVK8JavYa9NIRvJcdTAwMDOjZlx1MDAwN5++P8ngUoyfXHUwMDFlXCJcdTAwMTdcIjHVUszBMrL1vKLIRZJcZuSipKWWhFxcVFx1MDAwMnKZpFx1MDAxOVpSjaj6sVnGXHUwMDAxgPdng9e99+b8XHUwMDBiYK96drvttFx1MDAwMbVXXHUwMDFlw9BvtpdNOKbg8nHCMc9LZDpvNvdQKHVgXzGwXHUwMDEzhYme2YErXb3fKNRDXSn0XHUwMDBlnXapt394WllxXHUwMDA3ZmZcXJ9LKbg0fkFGPVhcYmJhLFx1MDAxOcKcI+M835t6SI6QknHGuFx1MDAxNOqx5fTY/vmhf1cv71x1MDAxNLYudz63y92Ugbaf1GP+dnNS73o1m1x1MDAxN/VYLy3kRT3WS1x1MDAwYp7e3lGV/Vx1MDAxZLYjhHNRLj3Vi8VcdTAwMTW2hbyYx8LFncY8klx1MDAxZlx1MDAxODU7+PT9mYfSLJV5IFwiIFczOfuYabaeV1x1MDAxNbjwLOCiuSWWXHUwMDAzXFySmEdsaPSVeSBcZlxmXGKxXHUwMDFjgMtcdTAwMTRrjFx1MDAwMau8mcd2XHUwMDFmlb//unntXHUwMDAwNE9mXHUwMDE3mI38bEhcdTAwMWaq8DpO8HZ+MVx1MDAwNXyP84txUTPdMJtDQNem+lwiVlxuXHUwMDBijsXsJOLx+aDTPH1+vmS03PN4VTS/PFx1MDAxN1fcXHUwMDE3wcgspFx1MDAxNXDrXHUwMDE3X1x1MDAxY+NcdTAwMTDYklpcIqXRanBcYkqxXHUwMDE2XFzFRiOWwiGuXHUwMDBlXHUwMDBmzz5cXD8/bFx1MDAxZlx1MDAxNC78QtdcdTAwMGWfPznHPznEojhETupdr2bz4lx1MDAxMOulhbw4xHppIS9cdTAwMGWxXlrIq9iycHGnUZPkXHUwMDA3Rs1cdTAwMGU+LVx1MDAxMlxmZmKiNGpCXHUwMDExkuOnI2pcIjlSfJ6iSLaeV1x1MDAxM1x1MDAwZUnEMuBcdTAwMTAwXHUwMDEzslx1MDAxYzg0XHUwMDFiM8GaKVx1MDAwMmLKXHUwMDFjJkuvIDU58ZfNTKYg+lRm8lwiaaZcdTAwMTO+hKxcdTAwMDQvlFx1MDAwNKU6IcdSKCb57E6YXVx0X00nXHUwMDA0hG9cdTAwMTFwQLAyjlx1MDAxMJd8xFx1MDAwYlx1MDAxOcKWkERx+MC0XCKxQu6i3Vx1MDAxMFlAOFx1MDAxMMZcXEumheI0NnQzdEuhISgwXCKgZ1x1MDAwNHiimvRSjLjCwHDo/F7aXHUwMDE3dtle2lx1MDAwZe0g3HabNbdZh4tRrntdjDPLPMS+X1dcdTAwMWbbfTVcIlx1MDAwMb1IJSeIKOgyLWN31e2W4XpcdTAwMTYolzGEjL4pkXhwwzDnbjrN2nSZskuVMZlcbiBcdTAwMTSViMBcdTAwMTNcdMdA9LlSfEIqYlEphVx1MDAwNsJJNIJAKyak8ux2uOM3XHUwMDFhblxiyj/z3WY4ruS+NreMt986dm38KrxV/Np4WGiZXHUwMDE2R/lp9Gkj8pv+l+HnP39NvjvdnM0xYchRe+/if+eOaJjg1MlcdTAwMTbgXHUwMDE1nDGsZ1x1MDAxZvHMhoUrXHUwMDFh0SS2OIGghSRYN4uiSP/XXHUwMDEyWVx1MDAxYZCVMkNcdTAwMWKaxIZDXHUwMDE3XHUwMDBlKzC1qFCKQWDlbGTSRzTogiymXHUwMDAwXHUwMDAyMZCTgj3EJn5cZuq4mijMwWt+rGg2c+RcdTAwMDD9UI6FMj5EXHUwMDA0pFx1MDAxZlx1MDAxNvOiQdzAkKKwXHUwMDE5NWaIIU5cdTAwMDR7YzTLhFx1MDAxZqMyXHUwMDExqjX4rEJIKFxuXHUwMDFkPSlcdTAwMTO4P9dcZnPCzVxuO1x1MDAxMHytg1m6LZtjwopcdTAwMTdcdTAwMTbLIGWkwjOALIRcbj7HvJPs8tuKXHUwMDA2M8aBIyHMTWKkMjZA/lx1MDAxMs2IxbAwI7VcXFx1MDAwYinHxVpcXDTTXHUwMDFh+phA0NRcdTAwMWNDOoutXHUwMDFhioJcdTAwMTmz4CqVcFx1MDAxZsZE0IlZZVx1MDAxOEJcdTAwMTlcdTAwMDNcdTAwMGbG/9ZoVjCggHFMXHUwMDAwalx1MDAxM1xiaYyihHBGLUhcXFx1MDAwMJOAXHUwMDFlQ1xu44K+LZ5lXHUwMDE3ekal4lx1MDAxYfA/1lx1MDAxMlx1MDAxM4hqmuBcdKG4XHUwMDA1uFx1MDAxYVwirJTIyDUp0jpFs0KqMZtjwoznjGaZRTBcdTAwMTnjJWNcdTAwMDGNaCyQmme1nfZKl6Wb+6fS9dVFrfaJ2Fx1MDAxN1x1MDAxZj6ueDhjZlx1MDAwNY8wuYJAzkYyXG4j/eFcYlx1MDAwMbpcdTAwMDeEKlx1MDAwMbVpXHUwMDA0d+RcdTAwMDfOYlWtKICBbPBoKidgXHUwMDE4QGZcdLQknmyWUlx1MDAwYrvrlu9cXO+ycdTuak4qXi0sep++fbD3+Pzw+br49DlcZnadwu1F+7MkbFx1MDAxMTP2161cdTAwMTaWk3pzalZ23IPiw1x1MDAxZW5cdTAwMDR7f9V2w1x1MDAwZemceovQXHUwMDAyQ0FcdTAwMGI3O9WTq2LpRF3tXvnHYWl1tVu5a2ydeVx1MDAxZIF5WNiu6XrpxH/qra64i197PlDD58NcdTAwMTLx2lx1MDAxN6p1etxmndZt6ZBfflO70yoryVxuipp9zY5cdTAwMGJEYpmJNq2yXHUwMDAySDiVNCDgYkQxOnuWzTaLXHUwMDE1zbJmtUl6ltXCgoSGXHUwMDAx5+mcsyxLyLIk0v1rdqVUamqw1uKza96VlVj9YEpl5aJcdTAwMWE4TvN9Sk1Fjty/sJrKXHUwMDE0jDheU1x1MDAxOcqY6XjpfF2lbyZBgD4gTdjs2z9lR87V9DzOsVx1MDAwNWRcdTAwMWNcXItcdTAwMTGqXGJcdTAwMWatpsB3S0klluB5XHUwMDE4XHUwMDAzi1SUaOCZSopYrXnoiIybMlx1MDAwMFx1MDAwN8ebwLtcdTAwMDQhRpjW4lxyeHeVmXq2O2zEx/i4llx1MDAwMuJcdTAwMTJcdTAwMDeWLijhMYo4oMTMQlx1MDAxY1RcZtpcdTAwMWJcXJmTn89R0Vx1MDAxMVx1MDAxOFx0XHUwMDA19JxhXHSBMqGgXHUwMDAzsiCgsohzbpKb1lx1MDAxM1wirVx1MDAxMz9PN15zxMw2auhd/O/bpqfSdGpcdTAwMGVsUYE7izm217huPV2497RyfcnK4fn+XHUwMDE26dyffl7x4EWJtKggXHUwMDFjSHpcdTAwMDJsMJvXYWVqwfqFnH/33eugQ1x1MDAxOFn+XCK34z1WPFdq9+amVun2yrf+VlhOISA/J6jO325O6l2vZnPbvW6ttJBTs7ntXpePuHmNIOQk7nn9oX7uXFw9XHUwMDEwLb94lyeNtt1yUsZRXHUwMDE2tdle4otEzb6Cg+89MEEoSV1cdTAwMDFDMCFCKI5nh1x1MDAxONn9t6JcdTAwMTCDkiyIQVx1MDAwMOsuXHRizLbfXHUwMDFlxpIjSlx1MDAwMX2v39DEN+6313ZrTsVcdTAwMGWWvf/FXHUwMDE04DzThnsjome66pRcdTAwMTVr6ZtjMsaAiDE++3DG9lHTx1xi6JdDjypP5Vx1MDAxMr7URSfFXcfcbtRZx6csvdVZMZrqrVhbXGZpRvCLt45OPmCCWIJcbo6Y+PbRjF8wqSgllEpwVZkweJGw5Vx1MDAwNSZcdTAwMTKx+Fx1MDAwNNalsIHzvaND9CRLN6dXLr66PdNfetf7P9nAothATupdr2bzYlx1MDAwM+ulhbyWq62XXHUwMDE28lqulpO4eW15sV6dtnxOlPxcIjOKe1rd3T65bqn60Ze7Y7vje0/bjM0m7uDTd+daXHUwMDE00dTldUT3t9mLIYVp2C3bLL5cdTAwMGLVmlx1MDAwMb1cdTAwMTGWhd6ktPCi0NuU0dxcdTAwMDT8lrC1uWRMXHUwMDAzXHUwMDA0zFx1MDAwMb+tXHUwMDBl01x1MDAxYSxaM+de+MrXzcNmO7Q9b9k8a1xuXHUwMDFkSVltlyp4ppOmXHUwMDE3jFx1MDAxOUl1Ulx1MDAwNCleUDVcdTAwMDfDyp5cdTAwMTKzmlx1MDAwM1winHFLMCp14oBcYuHCMsMgS3BSxixJiOCCJMzZYNriQjAkJkvFVCBwJPKWXUJ+iEpxwVxmWWlcIoSmXHUwMDEwZzWmOKk8yyjlmsoppeJR+deoYltIMlx1MDAxZnPEXGYnauNd/O/8MYOn1mk55cpUjGfP65m4bDUjXHUwMDA2w8Si4OuvQ6hcdCt2XHUwMDExNsaY91x1MDAxY1x1MDAxM2RcdTAwMTFuZvQogYmAXGJcdTAwMTCb2Fx1MDAxNa3XNZNcdTAwMTckVlx0U6qppGZcdTAwMDe2t+T7VVx1MDAwZVx1MDAxY9njl6OBXHUwMDAzXHUwMDExJFx1MDAxMWdUSCRcdTAwMTiOZlx1MDAxN1xmXHUwMDAzh7RcdTAwMTQliGIzXHUwMDE3XHUwMDA3joR1XHUwMDE3M001yc71XHUwMDFiI4uHQVx1MDAxY7PGiygzUYlINLlOd3K18DqFqnSrNUdkr3OGq3RcdTAwMWWSselcdTAwMTnHRFx1MDAxMqlm5yFS2yd35O7sYLdXrX/0xd72WUWkxKtcdTAwMTVcdTAwMTlDJlx1MDAxNFx1MDAwMI7Go9jhZb8z9mrd31TmyVx1MDAxODkmSdPeJiFcZlx1MDAwNtJcdTAwMDH2IN8y2+1bho5Xi2YnXHUwMDExm9lnt142a05gXGLLRnjrtjde9lx1MDAxZE+e6qpGfjz7VNfQb6WxmZHXXHUwMDFhpy7Jor1ccnxA0EhcdTAwMDVcdTAwMWbIJGQ5x+L67P5fSW+mWFlcdTAwMTC2zNbiXHUwMDE0Y6FGx1x1MDAxNFx1MDAwNMA9zNToXG6qMY92hJRv92hNLU0pXHUwMDE1ZjNcdTAwMWUmaGxCXnxcdTAwMTVX4lx1MDAxYS6zmo/QkZLdXHUwMDBmXHUwMDAxOLJzwijggNxcdTAwMGXAXHUwMDExXHUwMDExXGKMWkNATli4XHUwMDBl0VrNwlW+XHUwMDE5a1x1MDAxOHGkJmbSqlZcdTAwMDSeiVTiYliwMqzM/zvDbFx1MDAwMrXe2CPVes1ReDXcNOTxbtDupt1qXYRgZcNeXHUwMDAwQ3Zrg5BcdTAwMWS93GbHdbrbXHTuddM/TFx1MDAwNOyr0Vx1MDAwNFx1MDAxYce84t//vPvn/yxzRNUifQ== Container( id=\"dialog\")Horizontal( classes=\"buttons\")Button(\"Yes\")Button(\"No\")Screen()Container( id=\"sidebar\")Button( \"Install\")Underline this button

We can use the following CSS to style all buttons which have a parent with an ID of sidebar:

#sidebar > Button {\ntext-style: underline;\n}\n
"},{"location":"guide/CSS/#specificity","title":"Specificity","text":"

It is possible that several selectors match a given widget. If the same style is applied by more than one selector then Textual needs a way to decide which rule wins. It does this by following these rules:

  • The selector with the most IDs wins. For instance #next beats .button and #dialog #next beats #next. If the selectors have the same number of IDs then move to the next rule.

  • The selector with the most class names wins. For instance .button.success beats .success. For the purposes of specificity, pseudo classes are treated the same as regular class names, so .button:hover counts as 2 class names. If the selectors have the same number of class names then move to the next rule.

  • The selector with the most types wins. For instance Container Button beats Button.

"},{"location":"guide/CSS/#important-rules","title":"Important rules","text":"

The specificity rules are usually enough to fix any conflicts in your stylesheets. There is one last way of resolving conflicting selectors which applies to individual rules. If you add the text !important to the end of a rule then it will \"win\" regardless of the specificity.

Warning

Use !important sparingly (if at all) as it can make it difficult to modify your CSS in the future.

Here's an example that makes buttons blue when hovered over with the mouse, regardless of any other selectors that match Buttons:

Button:hover {\nbackground: blue !important;\n}\n
"},{"location":"guide/CSS/#css-variables","title":"CSS Variables","text":"

You can define variables to reduce repetition and encourage consistency in your CSS. Variables in Textual CSS are prefixed with $. Here's an example of how you might define a variable called $border:

$border: wide green;\n

With our variable assigned, we can write $border and it will be substituted with wide green. Consider the following snippet:

#foo {\nborder: $border;\n}\n

This will be translated into:

#foo {\nborder: wide green;\n}\n

Variables allow us to define reusable styling in a single place. If we decide we want to change some aspect of our design in the future, we only have to update a single variable.

Note

Variables can only be used in the values of a CSS declaration. You cannot, for example, refer to a variable inside a selector.

Variables can refer to other variables. Let's say we define a variable $success: lime;. Our $border variable could then be updated to $border: wide $success;, which will be translated to $border: wide lime;.

"},{"location":"guide/actions/","title":"Actions","text":"

Actions are allow-listed functions with a string syntax you can embed in links and bind to keys. In this chapter we will discuss how to create actions and how to run them.

"},{"location":"guide/actions/#action-methods","title":"Action methods","text":"

Action methods are methods on your app or widgets prefixed with action_. Aside from the prefix these are regular methods which you could call directly if you wished.

Information

Action methods may be coroutines (defined with the async keyword).

Let's write an app with a simple action.

actions01.py
from textual.app import App\nfrom textual import events\nclass ActionsApp(App):\ndef action_set_background(self, color: str) -> None:\nself.screen.styles.background = color\ndef on_key(self, event: events.Key) -> None:\nif event.key == \"r\":\nself.action_set_background(\"red\")\nif __name__ == \"__main__\":\napp = ActionsApp()\napp.run()\n

The action_set_background method is an action which sets the background of the screen. The key handler above will call this action if you press the R key.

Although it is possible (and occasionally useful) to call action methods in this way, they are intended to be parsed from an action string. For instance, the string \"set_background('red')\" is an action string which would call self.action_set_background('red').

The following example replaces the immediate call with a call to run_action() which parses an action string and dispatches it to the appropriate method.

actions02.py
from textual import events\nfrom textual.app import App\nclass ActionsApp(App):\ndef action_set_background(self, color: str) -> None:\nself.screen.styles.background = color\nasync def on_key(self, event: events.Key) -> None:\nif event.key == \"r\":\nawait self.run_action(\"set_background('red')\")\nif __name__ == \"__main__\":\napp = ActionsApp()\napp.run()\n

Note that the run_action() method is a coroutine so on_key needs to be prefixed with the async keyword.

You will not typically need this in a real app as Textual will run actions in links or key bindings. Before we discuss these, let's have a closer look at the syntax for action strings.

"},{"location":"guide/actions/#syntax","title":"Syntax","text":"

Action strings have a simple syntax, which for the most part replicates Python's function call syntax.

Important

As much as they look like Python code, Textual does not call Python's eval function to compile action strings.

Action strings have the following format:

  • The name of an action on is own will call the action method with no parameters. For example, an action string of \"bell\" will call action_bell().
  • Actions may be followed by braces containing Python objects. For example, the action string set_background(\"red\") will call action_set_background(\"red\").
  • Actions may be prefixed with a namespace (see below) followed by a dot.
eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nNVaW2/bNlx1MDAxNH7vr1xivIduQK3y8M5cdTAwMDLDkNuKtFl6SdJ0XHUwMDFkhkGV6Fi1LGmScluR/75DJbFkxXZsx8kyIXAsklx1MDAxMlx1MDAwZlx1MDAwZs/3nVx1MDAwYv392dpap7zIbOfVWseeXHUwMDA3flx1MDAxY4W5f9Z54dpPbV5EaYJdtLov0pM8qEb2yzIrXr18OfTzgS2z2Fx1MDAwZqx3XHUwMDFhXHUwMDE1J35cXJQnYZR6QTp8XHUwMDE5lXZY/OI+9/yh/TlLh2GZe/UkXVx1MDAxYkZlml/NZWM7tElZ4Nv/wPu1te/VZ0O63Fx1MDAwNqWfXHUwMDFjx7Z6oOqqXHUwMDA1VES0W/fSpFx1MDAxMlx1MDAxNoQm1ChQajRcIiq2cL7ShtjdQ5lt3eOaOlx1MDAxZr6c9T92//HXeSo/fzvc3Oz725/qaXtRXHUwMDFj75dcdTAwMTdxJVaR4mrqvqLM04E9isKy7+ZutY+eXG79om9cdTAwMWKP5enJcT+xhVs/XHUwMDE5taaZXHUwMDFmROWFe1x1MDAxMalbr5TQXHUwMDFjd+62iHBPUy5cdTAwMDSVhlx1MDAxOcpGne5xajyptDJGa8XAXGLgLcE201x1MDAxOHdcdTAwMDJcdTAwMDX7gVRXLdlXP1x1MDAxOFx1MDAxY6N4SViP6fmC4jyyXHUwMDFldXa9YGk80CC4oVxcSqJlPU/fRsf9XHUwMDEyh3DuXHRmiKT1jlx1MDAxNbbaXG5DgSlupFx1MDAxOXW4ibOdsLKKP9u67Pt5dq2yTiVgQ2h3u902qaZZNXY7se+2j3bPw3LvaHt3a7d7MVx1MDAxNFx1MDAwN2b0rjFcdTAwMWL08zw964x6Lq+/1aKdZKF/ZVcgJeeodC6YqC0vjpJcdTAwMDF2JidxXFy3pcGgNsWq9fLFXHUwMDEyXHUwMDE4XHUwMDAwwshUXHUwMDEwXHUwMDE4YJQoQ+dcdTAwMDfBodj66O++zz92L96dXHL6XyCM5ZenXHUwMDBlXHUwMDAypT3FXHRcdTAwMTdcdTAwMWGhQCVt2FhcdTAwMDVcdTAwMDPtYVx1MDAxYpVMXHUwMDEypcBokPeCXHUwMDAx0K9ay0kwoFxiOM6N4kpcYqNcdTAwMTUsglx1MDAwMmCEK6VcdTAwMDU8Mlxm9lx1MDAwNjD4evr69MPml/M9+lv5XHUwMDFl5M5fK4OBxMU+XHUwMDEyXGaAT4dcdTAwMDE3XHUwMDA0jVx1MDAwMlx1MDAwMOaGXHUwMDAx/7xcdTAwMGXs8+DtRry1l4b7mdVvksMnXHUwMDBlXHUwMDAzXFygp5HrNdHGLVaNo0ChM+CK4ntcdTAwMTRChZt7gYBcdTAwMDfS9sQkXHUwMDEwXHUwMDAwUE9cdTAwMDNjaOSaKYq+aSFcdTAwMThcYsm4RCHF48KgSzdcdTAwMDb7hkT5/plMdXFA5MHu69XBQKNXXFxcdTAwMTVcZkp7Xk5EXHUwMDAwct9UXHUwMDA0XHUwMDEwyVx1MDAxOFx1MDAxN1x1MDAwNOZ3XHUwMDA04dveutjaevO7POSDzUMt4r9cdTAwMGbTlVwioPVUXHUwMDEzXHUwMDAwsFx1MDAxNFx1MDAwMFDdnpTCXHUwMDAwRdNcdTAwMTOKyjFcdTAwMDCAXHUwMDE2XHUwMDFlQ3rmoFxiekUj7+dcdTAwMDZmIMBMYP7bpq4lcIrRXHUwMDE5LG7phbt5knFcdTAwMGYzyixC+LU9pUm5XHUwMDFm/WOrmHas9Vd/XHUwMDE4xVx1MDAxN2NGUUFcdTAwMDBcdTAwMDV8l5Vo5H68lmCqUaCl2OaeXHUwMDE1XHUwMDE256+sX489uVx1MDAxZUfHXHUwMDBlMJ3Y9saRVEaYpYy6y7Sh41x1MDAwMCXx8XX5TtheUZpHx1x1MDAxMUpxMF2qpVx1MDAwMI1x+3Q8XHUwMDBizkBqMT+eTVx1MDAxMoP/Nt/RKflwXlx1MDAxY+yGcL71+mnjmVx04UmmXGIlwNBXqPHsXHUwMDA2uPKAMkFcZsewj90zqpvl0Golz4AzQ1JBbmH14EeB88PGb4rrhlN5aDivXHUwMDA3XHUwMDBlOFx1MDAxNWxcdTAwMTbCcYCKsvlcdTAwMDMguSnQUlx1MDAxMNbCtFtHXHUwMDEwXHUwMDA2iVlcdTAwMDJIPn9QSvzuMP37nLPy2+5OfiZcdTAwMDby8Nvm04Ywl8Zj2lx1MDAwNXVcdTAwMWHjTpef3fLJklx1MDAxOFx1MDAwNDFaXHUwMDFjqGZcdTAwMDSzWlx1MDAxMCOFzFx1MDAwM2JcdTAwMDOCXG7M3Fx1MDAxZdknP2z0+Vx1MDAxZvnkzM9cdTAwMTE3XGLM4mmAeZJgM0F9pepJkTafXHUwMDFlaWP2olx1MDAxNFVcctzfherZXHUwMDAx2Vx1MDAwMqhuY2dZVKs76y1cZlFcdTAwMGKMYqLJpFa6sZWVTSjmgeKCSiUx4aQzXHUwMDAy7Vx1MDAxMH036S1cdTAwMGJq5lx0TlxmwYlcYlx1MDAwN8x4xVx1MDAwNEetjUdccnZLKVx0XHUwMDEx0IghriFPMSXmmKku4bfvSjhXWSBsyOHn5UaUhFFyjJ01m9xcdTAwMTTTd+ZI39wq/axKXHUwMDFhcatQPdwwzbVq9PfS4MStoks86spdUlx1MDAxMalQlVxmdXk96nIklE3Cu0WaXV9cdTAwMWaJZDCHk5QrhZsloHaP4zJcdTAwMTFqXHUwMDE44J/AUNhVttktmWK/KDfT4TAqUfPv0ygp21x1MDAxYa5Uue5Q3rf+Lf7ANTX72nSQuTeOM3v9ba1GTHUz+v7ni4mjp1uyu7q3jLh+37Pm/8WZzGjWbr5hMlxumJ1yVPP8OcbsYPQpUlx1MDAxOTPGXHUwMDAzV1x1MDAxYmGaUIG23YpPmPKEQtunklx1MDAxM4lmJlqC1TQlXHUwMDAyw0m4dHziUUE4ZnWSMCMwXHUwMDAwmVA1XHUwMDEzzNNcdTAwMThHoaTKXGKuTENJV1SmMCNkgKB5XFwqWzpJmJPKZmeuXHLeQO05h8RBacxcdTAwMTh5Y0STzKRcdTAwMTGoYoJcdTAwMDRCXHUwMDE1Q+e0XHUwMDFjmc0+J6n5lXhCXHUwMDEzQVx1MDAxY98z9DL1vrboXGZcdTAwMDdhmFx1MDAwNoxJVCdy3/+azqZbs7u6t1xmeVV0hvQp280jOmNcZlWMucjcbDY7Kv9cdTAwMGbYTN95XHUwMDAy4Fx1MDAwZWJcdEZeLrdslz+V9ihSXHUwMDE51UZcdTAwMDOlzeJSm8qY5L1ALUtlxFx1MDAwM4NuTDrPjJmf1mzCcbBhXHUwMDFlikmFoe40gjeLN9dcdTAwMDdcdTAwMDGG4FuU4Fx1MDAwZnBcdTAwMTCwylL9omQ2O4dcdTAwMWbxhvJcdTAwMThzkSlobjjlnDVGNGmDaIyQjNCIMkOlXHUwMDA2s1x1MDAxY5vNPu5qRovS7arE0Fx1MDAxZThhdIpUKFx1MDAxMVx1MDAwM7fxXHUwMDFhjVFh9v+/ZrNcdTAwMTlcdTAwMDbtru4tW16QzqZcdTAwMTWP2PRcdTAwMTNNzShcdTAwMTdEwvxkZuS23GbBh4NPnzc+XHUwMDFkvTmKN/OMTCGzvlx1MDAxZvRPcjuNzlZVPTJ35pmAcbGghGFcdTAwMTiKITEh4+f6XGY8XHUwMDA21J1rOVx1MDAwZqu0udfPW8rcT4rMz1x1MDAxMVx1MDAxM7c5jcOE8lGD125Ii1OqMVA0S5DW0z3TXHUwMDExtFlcXF+yfqTHWkf1ozr7uKlcdTAwMWb5WeZcdTAwMTW2/Kveolx1MDAxZp/nNnz+08QqUuOXLY9xtDNduGc32qw06Vx1MDAwNu6XqMdcdTAwMTHnoiFE4bUy6ik6p5E925j0W6vqcm+tWMPh0zoz+H757PJfPFx1MDAxMrdyIn0= Optional namespaceAction nameOptional parametersapp.set_background('red')"},{"location":"guide/actions/#parameters","title":"Parameters","text":"

If the action string contains parameters, these must be valid Python literals. Which means you can include numbers, strings, dicts, lists etc. but you can't include variables or references to any other Python symbols.

Consequently \"set_background('blue')\" is a valid action string, but \"set_background(new_color)\" is not \u2014 because new_color is a variable and not a literal.

"},{"location":"guide/actions/#links","title":"Links","text":"

Actions may be embedded as links within console markup. You can create such links with a @click tag.

The following example mounts simple static text with embedded action links.

actions03.pyOutput actions03.py
from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nTEXT = \"\"\"\n[b]Set your background[/b]\n[@click=set_background('red')]Red[/]\n[@click=set_background('green')]Green[/]\n[@click=set_background('blue')]Blue[/]\n\"\"\"\nclass ActionsApp(App):\ndef compose(self) -> ComposeResult:\nyield Static(TEXT)\ndef action_set_background(self, color: str) -> None:\nself.screen.styles.background = color\nif __name__ == \"__main__\":\napp = ActionsApp()\napp.run()\n

ActionsApp Set\u00a0your\u00a0background Red Green Blue

When you click any of the links, Textual runs the \"set_background\" action to change the background to the given color.

"},{"location":"guide/actions/#bindings","title":"Bindings","text":"

Textual will run actions bound to keys. The following example adds key bindings for the R, G, and B keys which call the \"set_background\" action.

actions04.pyOutput actions04.py
from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nTEXT = \"\"\"\n[b]Set your background[/b]\n[@click=set_background('red')]Red[/]\n[@click=set_background('green')]Green[/]\n[@click=set_background('blue')]Blue[/]\n\"\"\"\nclass ActionsApp(App):\nBINDINGS = [\n(\"r\", \"set_background('red')\", \"Red\"),\n(\"g\", \"set_background('green')\", \"Green\"),\n(\"b\", \"set_background('blue')\", \"Blue\"),\n]\ndef compose(self) -> ComposeResult:\nyield Static(TEXT)\ndef action_set_background(self, color: str) -> None:\nself.screen.styles.background = color\nif __name__ == \"__main__\":\napp = ActionsApp()\napp.run()\n

ActionsApp Set\u00a0your\u00a0background Red Green Blue

If you run this example, you can change the background by pressing keys in addition to clicking links.

See the previous section on input for more information on bindings.

"},{"location":"guide/actions/#namespaces","title":"Namespaces","text":"

Textual will look for action methods in the class where they are defined (App, Screen, or Widget). If we were to create a custom widget it can have its own set of actions.

The following example defines a custom widget with its own set_background action.

actions05.pyactions05.tcss actions05.py
from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nTEXT = \"\"\"\n[b]Set your background[/b]\n[@click=set_background('cyan')]Cyan[/]\n[@click=set_background('magenta')]Magenta[/]\n[@click=set_background('yellow')]Yellow[/]\n\"\"\"\nclass ColorSwitcher(Static):\ndef action_set_background(self, color: str) -> None:\nself.styles.background = color\nclass ActionsApp(App):\nCSS_PATH = \"actions05.tcss\"\nBINDINGS = [\n(\"r\", \"set_background('red')\", \"Red\"),\n(\"g\", \"set_background('green')\", \"Green\"),\n(\"b\", \"set_background('blue')\", \"Blue\"),\n]\ndef compose(self) -> ComposeResult:\nyield ColorSwitcher(TEXT)\nyield ColorSwitcher(TEXT)\ndef action_set_background(self, color: str) -> None:\nself.screen.styles.background = color\nif __name__ == \"__main__\":\napp = ActionsApp()\napp.run()\n
actions05.tcss
Screen {\nlayout: grid;\ngrid-size: 1;\ngrid-gutter: 2 4;\ngrid-rows: 1fr;\n}\nColorSwitcher {\nheight: 100%;\nmargin: 2 4;\n}\n

There are two instances of the custom widget mounted. If you click the links in either of them it will changed the background for that widget only. The R, G, and B key bindings are set on the App so will set the background for the screen.

You can optionally prefix an action with a namespace, which tells Textual to run actions for a different object.

Textual supports the following action namespaces:

  • app invokes actions on the App.
  • screen invokes actions on the screen.

In the previous example if you wanted a link to set the background on the app rather than the widget, we could set a link to app.set_background('red').

"},{"location":"guide/actions/#builtin-actions","title":"Builtin actions","text":"

Textual supports the following builtin actions which are defined on the app.

  • action_add_class
  • action_back
  • action_bell
  • action_check_bindings
  • action_focus
  • action_focus_next
  • action_focus_previous
  • action_pop_screen
  • action_push_screen
  • action_quit
  • action_remove_class
  • action_screenshot
  • action_switch_screen
  • action_toggle_class
  • action_toggle_dark
"},{"location":"guide/animation/","title":"Animation","text":"

Ths chapter discusses how to use Textual's animation system to create visual effects such as movement, blending, and fading.

"},{"location":"guide/animation/#animating-styles","title":"Animating styles","text":"

Textual's animator can change an attribute from one value to another in fixed increments over a period of time. You can apply animations to styles such as offset to move widgets around the screen, and opacity to create fading effects.

Apps and widgets both have an animate method which will animate properties on those objects. Additionally, styles objects have an identical animate method which will animate styles.

Let's look at an example of how we can animate the opacity of a widget to make it fade out. The following example app contains a single Static widget which is immediately animated to an opacity of 0.0 (making it invisible) over a duration of two seconds.

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass AnimationApp(App):\ndef compose(self) -> ComposeResult:\nself.box = Static(\"Hello, World!\")\nself.box.styles.background = \"red\"\nself.box.styles.color = \"black\"\nself.box.styles.padding = (1, 2)\nyield self.box\ndef on_mount(self):\nself.box.styles.animate(\"opacity\", value=0.0, duration=2.0)\nif __name__ == \"__main__\":\napp = AnimationApp()\napp.run()\n

The animator updates the value of the opacity attribute on the styles object in small increments over two seconds. Here's what the output will look like after each half a second.

After 0sAfter 0.5sAfter 1sAfter 1.5sAfter 2s

AnimationApp Hello,\u00a0World!

AnimationApp Hello,\u00a0World!

AnimationApp Hello,\u00a0World!

AnimationApp Hello,\u00a0World!

AnimationApp Hello,\u00a0World!

"},{"location":"guide/animation/#duration-and-speed","title":"Duration and Speed","text":"

When requesting an animation you can specify a duration or speed. The duration is how long the animation should take in seconds. The speed is how many units a value should change in one second. For instance, if you animate a value at 0 to 10 with a speed of 2, it will complete in 5 seconds.

"},{"location":"guide/animation/#easing-functions","title":"Easing functions","text":"

The easing function determines the journey a value takes on its way to the target value. It could move at a constant pace, or it might start off slow then accelerate towards its final value. Textual supports a number of easing functions.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1XWVPjRlx1MDAxMH7nV1DeVyzmPrYqlYJcdTAwMDVcdTAwMTJcdTAwMDJLXHUwMDBlczib2ofBXHUwMDFh21x1MDAxM8uSkMZcdTAwMDaW4r+nJVx1MDAxM48vwrnUblX0YGumZ3q+6e6vu3Wztr7e8Ne5bbxfb9irjklcXFxcmMvGRjU/tkXpslx1MDAxNESkXHUwMDFll9mo6NQr+97n5fvNzaEpXHUwMDA21ueJ6dho7MqRSUo/il1cdTAwMTZ1suGm83ZY/lj9XHUwMDFlmaH9Ic+GsS+icEjTxs5nxeQsm9ihTX1cdNr/gvH6+k39O4POXHUwMDE0RTZcdTAwMDFWT1x1MDAwN3BKL05cdTAwMWVlaY1cdTAwMTMjrjSjWuDpXG5X7sBR3sYg7lx1MDAwMlxcXHUwMDFiJNVU42Tvl7TZ/nX76rh1fDDO+4j+dNVcdKd2XZK0/HUysYLp9EeFXHLS0lx1MDAxN9nAnrnY9/812sz8dF9syj5cdTAwMDCYiots1OuntqwuXHUwMDFmkGa56Th/XV9cdTAwMDNNZ03aq5WEmStcdTAwMThRwSMqXHUwMDE0XHUwMDE3XGZRQZhUU2m1n2lcdTAwMWNxXHUwMDAxcsyRRIxcdTAwMTK5gOxDloBcdTAwMWZcdTAwMDDZO1Q/XHUwMDAx2rnpXGZ6gC+Np2t8YdIyN1x1MDAwNXgrrLu8uzOONGZIXHUwMDEwPpX0rev1fWVcdTAwMGXCXCIlg6C0tVx1MDAxM7DCmiuhUZBUXHUwMDA35vtxXHUwMDFkXHUwMDBin1x1MDAxN43YN0V+Z6tGXHJsXHUwMDA2bDXcnVx0pLB5lMdm4nQsXHUwMDA0x1xcXHUwMDEzWtlqKk9cXDpcdTAwMDBhOkqSMJd1XHUwMDA2K+Kk9Kbw2y6NXdpb3GLT+Fx1MDAxZUliSv8hXHUwMDFiXHUwMDBlnVx1MDAwN1x1MDAxOL9lLvWLK2q9W1WQ962JV2ielS2yIa80XHUwMDA28lRPeFtcdTAwMGbRUlx1MDAwZqbvnzdWrm4uebGevXNg2L02+3+78UTa4pkwXFzkraCaIaxkYPZDvKUnh1x1MDAwN7y9d/7n3uFheaJHgra3+t9cdTAwMDFvRUQgXHUwMDE2pdRaXHUwMDAwbdlcdTAwMTJvMaFIYES1XHUwMDE0mi9cdTAwMDB7PdpShFwiKSmjclx1MDAwNXFxJCFtUEyWuEs5Ykpr9sbUhdAgwY7/U3dmwVxuR1ZPM/jwifRcdTAwMDXr2pXsRfL+skuYpkpjXHUwMDFj8utD9G3r38nu2cfty7//6F1cZoa7R1x1MDAwM3aQvDJ9y1xm+o7Xr7pUXHUwMDAwNSWjeJagNX2VjJCQXFxTRojmSC9cdTAwMDBcdTAwMGL0VYJY3XlcdH1pJKSCUsqpRFqH8lx1MDAxZqovXHUwMDAyMFx1MDAwNHMhJWGKXHUwMDEyvURmgpnAXHUwMDAyIf6mbFZcbmCrXHUwMDAw+Htj85zsVamMMYso4lpqSoRcdTAwMTYqNLHV06QogsZcdTAwMTZxJjWD5lx0yVx1MDAwN/VpXHUwMDEyKVwiJHTDoJJzzOb0YYVcIkisWFIou1x1MDAxMlx1MDAwYvVwqrkn6Gp1y/H2xMTj7ZVflXjEfVmnSjpcdTAwMDRcbkPoKlx1MDAxZUo6O+3Wyadd0vz482D3tDX+1L44z/Hzklx1MDAwZV5kx1dLOuDsiDBcdTAwMDZdPuJcdTAwMDRcbvB8q89cdHiRay5cdTAwMTTl0PD/R855acvAXHUwMDAyrJBklrt7wYggirFw2UcnlbJcdTAwMWE8M6lQKlmIlCcklW6W+pb7Ulx1MDAwN1x1MDAxNJqb3TNDl1xcz/mtjtHKUG5oZy1Z2rpsVlx1MDAxZjdza7dcdTAwMTLXS+uqarvzwe1cdTAwMWR8XHUwMDE1T8U+m7l3XHUwMDA3zjagrthfyjdZ4XouNcnxLI5nsYrcW8zhq1x1MDAxMVPIXG6Pp9Vps9y/lGdcdTAwMTe90+ukvfOlO+52bfNbp1x1MDAxNYH0x1x1MDAxNaZcXCqFkObznTiQKVx1MDAxMlxcK2jEXHUwMDE5NFLQkn81XnHyKF5BktaKMvyMYv1cdTAwMTJesSrLvlx1MDAxOa/GJlx1MDAxOX1cdTAwMTPEmlx1MDAwMJkwa+2uXCI2TJ63PNhcdTAwMDdcdTAwMTZMeFx1MDAwNi5w8d0lg7rG2NnL7eUoeNetn0przdaKXHUwMDE4tnLAze3a7T8uXFzYXHUwMDFjIn0= timevalue

Run the following from the command prompt to preview them.

textual easing\n

You can specify which easing method to use via the easing parameter on the animate method. The default easing method is \"in_out_cubic\" which accelerates and then decelerates to produce a pleasing organic motion.

Note

The textual easing preview requires the textual-dev package to be installed (using pip install textual-dev).

"},{"location":"guide/animation/#completion-callbacks","title":"Completion callbacks","text":"

You can pass a callable to the animator via the on_complete parameter. Textual will run the callable when the animation has completed.

"},{"location":"guide/animation/#delaying-animations","title":"Delaying animations","text":"

You can delay the start of an animation with the delay parameter of the animate method. This parameter accepts a float value representing the number of seconds to delay the animation by. For example, self.box.styles.animate(\"opacity\", value=0.0, duration=2.0, delay=5.0) delays the start of the animation by five seconds, meaning the animation will start after 5 seconds and complete 2 seconds after that.

"},{"location":"guide/app/","title":"App Basics","text":"

In this chapter we will cover how to use Textual's App class to create an application. Just enough to get you up to speed. We will go in to more detail in the following chapters.

"},{"location":"guide/app/#the-app-class","title":"The App class","text":"

The first step in building a Textual app is to import the App class and create a subclass. Let's look at the simplest app class:

from textual.app import App\nclass MyApp(App):\npass\n
"},{"location":"guide/app/#the-run-method","title":"The run method","text":"

To run an app we create an instance and call run().

simple02.py
from textual.app import App\nclass MyApp(App):\npass\nif __name__ == \"__main__\":\napp = MyApp()\napp.run()\n

Apps don't get much simpler than this\u2014don't expect it to do much.

Tip

The __name__ == \"__main__\": condition is true only if you run the file with python command. This allows us to import app without running the app immediately. It also allows the devtools run command to run the app in development mode. See the Python docs for more information.

If we run this app with python simple02.py you will see a blank terminal, something like the following:

MyApp

When you call App.run() Textual puts the terminal in to a special state called application mode. When in application mode the terminal will no longer echo what you type. Textual will take over responding to user input (keyboard and mouse) and will update the visible portion of the terminal (i.e. the screen).

If you hit Ctrl+C Textual will exit application mode and return you to the command prompt. Any content you had in the terminal prior to application mode will be restored.

Tip

A side effect of application mode is that you may no longer be able to select and copy text in the usual way. Terminals typically offer a way to bypass this limit with a key modifier. On iTerm you can select text if you hold the Option key. See the documentation for your terminal software for how to select text in application mode.

"},{"location":"guide/app/#events","title":"Events","text":"

Textual has an event system you can use to respond to key presses, mouse actions, and internal state changes. Event handlers are methods prefixed with on_ followed by the name of the event.

One such event is the mount event which is sent to an application after it enters application mode. You can respond to this event by defining a method called on_mount.

Info

You may have noticed we use the term \"send\" and \"sent\" in relation to event handler methods in preference to \"calling\". This is because Textual uses a message passing system where events are passed (or sent) between components. See events for details.

Another such event is the key event which is sent when the user presses a key. The following example contains handlers for both those events:

event01.py
from textual.app import App\nfrom textual import events\nclass EventApp(App):\nCOLORS = [\n\"white\",\n\"maroon\",\n\"red\",\n\"purple\",\n\"fuchsia\",\n\"olive\",\n\"yellow\",\n\"navy\",\n\"teal\",\n\"aqua\",\n]\ndef on_mount(self) -> None:\nself.screen.styles.background = \"darkblue\"\ndef on_key(self, event: events.Key) -> None:\nif event.key.isdecimal():\nself.screen.styles.background = self.COLORS[int(event.key)]\nif __name__ == \"__main__\":\napp = EventApp()\napp.run()\n

The on_mount handler sets the self.screen.styles.background attribute to \"darkblue\" which (as you can probably guess) turns the background blue. Since the mount event is sent immediately after entering application mode, you will see a blue screen when you run this code.

EventApp

The key event handler (on_key) has an event parameter which will receive a Key instance. Every event has an associated event object which will be passed to the handler method if it is present in the method's parameter list.

Note

It is unusual (but not unprecedented) for a method's parameters to affect how it is called. Textual accomplishes this by inspecting the method prior to calling it.

Some events contain additional information you can inspect in the handler. The Key event has a key attribute which is the name of the key that was pressed. The on_key method above uses this attribute to change the background color if any of the keys from 0 to 9 are pressed.

"},{"location":"guide/app/#async-events","title":"Async events","text":"

Textual is powered by Python's asyncio framework which uses the async and await keywords.

Textual knows to await your event handlers if they are coroutines (i.e. prefixed with the async keyword). Regular functions are generally fine unless you plan on integrating other async libraries (such as httpx for reading data from the internet).

Tip

For a friendly introduction to async programming in Python, see FastAPI's concurrent burgers article.

"},{"location":"guide/app/#widgets","title":"Widgets","text":"

Widgets are self-contained components responsible for generating the output for a portion of the screen. Widgets respond to events in much the same way as the App. Most apps that do anything interesting will contain at least one (and probably many) widgets which together form a User Interface.

Widgets can be as simple as a piece of text, a button, or a fully-fledged component like a text editor or file browser (which may contain widgets of their own).

"},{"location":"guide/app/#composing","title":"Composing","text":"

To add widgets to your app implement a compose() method which should return an iterable of Widget instances. A list would work, but it is convenient to yield widgets, making the method a generator.

The following example imports a builtin Welcome widget and yields it from App.compose().

widgets01.py
from textual.app import App, ComposeResult\nfrom textual.widgets import Welcome\nclass WelcomeApp(App):\ndef compose(self) -> ComposeResult:\nyield Welcome()\ndef on_button_pressed(self) -> None:\nself.exit()\nif __name__ == \"__main__\":\napp = WelcomeApp()\napp.run()\n

When you run this code, Textual will mount the Welcome widget which contains Markdown content and a button:

WelcomeApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Welcome!\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b Textual\u00a0is\u00a0a\u00a0TUI,\u00a0or\u00a0Text\u00a0User\u00a0Interface,\u00a0framework\u00a0for\u00a0Python\u00a0inspired\u00a0by\u00a0\u00a0 modern\u00a0web\u00a0development.\u00a0We\u00a0hope\u00a0you\u00a0enjoy\u00a0using\u00a0Textual! Dune\u00a0quote \u258c\u00a0\"I\u00a0must\u00a0not\u00a0fear.Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0little-death\u00a0that \u258c\u00a0brings\u00a0total\u00a0obliteration.I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass \u258c\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner \u258c\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only \u258c\u00a0I\u00a0will\u00a0remain.\" \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0OK\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

Notice the on_button_pressed method which handles the Button.Pressed event sent by a button contained in the Welcome widget. The handler calls App.exit() to exit the app.

"},{"location":"guide/app/#mounting","title":"Mounting","text":"

While composing is the preferred way of adding widgets when your app starts it is sometimes necessary to add new widget(s) in response to events. You can do this by calling mount() which will add a new widget to the UI.

Here's an app which adds a welcome widget in response to any key press:

widgets02.py
from textual.app import App\nfrom textual.widgets import Welcome\nclass WelcomeApp(App):\ndef on_key(self) -> None:\nself.mount(Welcome())\ndef on_button_pressed(self) -> None:\nself.exit()\nif __name__ == \"__main__\":\napp = WelcomeApp()\napp.run()\n

When you first run this you will get a blank screen. Press any key to add the welcome widget. You can even press a key multiple times to add several widgets.

WelcomeApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Welcome!\u2503\u2582\u2582 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b Textual\u00a0is\u00a0a\u00a0TUI,\u00a0or\u00a0Text\u00a0User\u00a0Interface,\u00a0framework\u00a0for\u00a0Python\u00a0inspired\u00a0by modern\u00a0web\u00a0development.\u00a0We\u00a0hope\u00a0you\u00a0enjoy\u00a0using\u00a0Textual! Dune\u00a0quote \u258c\u00a0\"I\u00a0must\u00a0not\u00a0fear.Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0little-death\u00a0 \u258c\u00a0that\u00a0brings\u00a0total\u00a0obliteration.I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0permit\u00a0it\u00a0 \u258c\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn \u258c\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0 \u258c\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\" \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0OK\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

"},{"location":"guide/app/#awaiting-mount","title":"Awaiting mount","text":"

When you mount a widget, Textual will mount everything the widget composes. Textual guarantees that the mounting will be complete by the next message handler, but not immediately after the call to mount(). This may be a problem if you want to make any changes to the widget in the same message handler.

Let's first illustrate the problem with an example. The following code will mount the Welcome widget in response to a key press. It will also attempt to modify the Button in the Welcome widget by changing its label from \"OK\" to \"YES!\".

from textual.app import App\nfrom textual.widgets import Button, Welcome\nclass WelcomeApp(App):\ndef on_key(self) -> None:\nself.mount(Welcome())\nself.query_one(Button).label = \"YES!\" # (1)!\nif __name__ == \"__main__\":\napp = WelcomeApp()\napp.run()\n
  1. See queries for more information on the query_one method.

If you run this example, you will find that Textual raises a NoMatches exception when you press a key. This is because the mount process has not yet completed when we attempt to change the button.

To solve this we can optionally await the result of mount(), which requires we make the function async. This guarantees that by the following line, the Button has been mounted, and we can change its label.

from textual.app import App\nfrom textual.widgets import Button, Welcome\nclass WelcomeApp(App):\nasync def on_key(self) -> None:\nawait self.mount(Welcome())\nself.query_one(Button).label = \"YES!\"\nif __name__ == \"__main__\":\napp = WelcomeApp()\napp.run()\n

Here's the output. Note the changed button text:

WelcomeApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Welcome!\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b Textual\u00a0is\u00a0a\u00a0TUI,\u00a0or\u00a0Text\u00a0User\u00a0Interface,\u00a0framework\u00a0for\u00a0Python\u00a0inspired\u00a0by\u00a0\u00a0 modern\u00a0web\u00a0development.\u00a0We\u00a0hope\u00a0you\u00a0enjoy\u00a0using\u00a0Textual! Dune\u00a0quote \u258c\u00a0\"I\u00a0must\u00a0not\u00a0fear.Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0little-death\u00a0that \u258c\u00a0brings\u00a0total\u00a0obliteration.I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass \u258c\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner \u258c\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only \u258c\u00a0I\u00a0will\u00a0remain.\" \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0YES!\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

"},{"location":"guide/app/#exiting","title":"Exiting","text":"

An app will run until you call App.exit() which will exit application mode and the run method will return. If this is the last line in your code you will return to the command prompt.

The exit method will also accept an optional positional value to be returned by run(). The following example uses this to return the id (identifier) of a clicked button.

question01.py
from textual.app import App, ComposeResult\nfrom textual.widgets import Label, Button\nclass QuestionApp(App[str]):\ndef compose(self) -> ComposeResult:\nyield Label(\"Do you love Textual?\")\nyield Button(\"Yes\", id=\"yes\", variant=\"primary\")\nyield Button(\"No\", id=\"no\", variant=\"error\")\ndef on_button_pressed(self, event: Button.Pressed) -> None:\nself.exit(event.button.id)\nif __name__ == \"__main__\":\napp = QuestionApp()\nreply = app.run()\nprint(reply)\n

Running this app will give you the following:

QuestionApp Do\u00a0you\u00a0love\u00a0Textual? \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Yes\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0No\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

Clicking either of those buttons will exit the app, and the run() method will return either \"yes\" or \"no\" depending on button clicked.

"},{"location":"guide/app/#return-type","title":"Return type","text":"

You may have noticed that we subclassed App[str] rather than the usual App.

question01.py
from textual.app import App, ComposeResult\nfrom textual.widgets import Label, Button\nclass QuestionApp(App[str]):\ndef compose(self) -> ComposeResult:\nyield Label(\"Do you love Textual?\")\nyield Button(\"Yes\", id=\"yes\", variant=\"primary\")\nyield Button(\"No\", id=\"no\", variant=\"error\")\ndef on_button_pressed(self, event: Button.Pressed) -> None:\nself.exit(event.button.id)\nif __name__ == \"__main__\":\napp = QuestionApp()\nreply = app.run()\nprint(reply)\n

The addition of [str] tells mypy that run() is expected to return a string. It may also return None if App.exit() is called without a return value, so the return type of run will be str | None. Replace the str in [str] with the type of the value you intend to call the exit method with.

Note

Type annotations are entirely optional (but recommended) with Textual.

"},{"location":"guide/app/#return-code","title":"Return code","text":"

When you exit a Textual app with App.exit(), you can optionally specify a return code with the return_code parameter.

What are return codes?

Returns codes are a standard feature provided by your operating system. When any application exits it can return an integer to indicate if it was successful or not. A return code of 0 indicates success, any other value indicates that an error occurred. The exact meaning of a non-zero return code is application-dependant.

When a Textual app exits normally, the return code will be 0. If there is an unhandled exception, Textual will set a return code of 1. You may want to set a different value for the return code if there is error condition that you want to differentiate from an unhandled exception.

Here's an example of setting a return code for an error condition:

if critical_error:\nself.exit(return_code=4, message=\"Critical error occurred\")\n

The app's return code can be queried with app.return_code, which will be None if it hasn't been set, or an integer.

Textual won't explicitly exit the process. To exit the app with a return code, you should call sys.exit. Here's how you might do that:

if __name__ == \"__main__\"\napp = MyApp()\napp.run()\nimport sys\nsys.exit(app.return_code or 0)\n
"},{"location":"guide/app/#css","title":"CSS","text":"

Textual apps can reference CSS files which define how your app and widgets will look, while keeping your Python code free of display related code (which tends to be messy).

Info

Textual apps typically use the extension .tcss for external CSS files to differentiate them from browser (.css) files.

The chapter on Textual CSS describes how to use CSS in detail. For now let's look at how your app references external CSS files.

The following example enables loading of CSS by adding a CSS_PATH class variable:

question02.py
from textual.app import App, ComposeResult\nfrom textual.widgets import Button, Label\nclass QuestionApp(App[str]):\nCSS_PATH = \"question02.tcss\"\ndef compose(self) -> ComposeResult:\nyield Label(\"Do you love Textual?\", id=\"question\")\nyield Button(\"Yes\", id=\"yes\", variant=\"primary\")\nyield Button(\"No\", id=\"no\", variant=\"error\")\ndef on_button_pressed(self, event: Button.Pressed) -> None:\nself.exit(event.button.id)\nif __name__ == \"__main__\":\napp = QuestionApp()\nreply = app.run()\nprint(reply)\n

Note

We also added an id to the Label, because we want to style it in the CSS.

If the path is relative (as it is above) then it is taken as relative to where the app is defined. Hence this example references \"question01.tcss\" in the same directory as the Python code. Here is that CSS file:

question02.tcss
Screen {\nlayout: grid;\ngrid-size: 2;\ngrid-gutter: 2;\npadding: 2;\n}\n#question {\nwidth: 100%;\nheight: 100%;\ncolumn-span: 2;\ncontent-align: center bottom;\ntext-style: bold;\n}\nButton {\nwidth: 100%;\n}\n

When \"question02.py\" runs it will load \"question02.tcss\" and update the app and widgets accordingly. Even though the code is almost identical to the previous sample, the app now looks quite different:

QuestionApp Do\u00a0you\u00a0love\u00a0Textual? \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Yes\u00a0\u00a0No\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

"},{"location":"guide/app/#classvar-css","title":"Classvar CSS","text":"

While external CSS files are recommended for most applications, and enable some cool features like live editing, you can also specify the CSS directly within the Python code.

To do this set a CSS class variable on the app to a string containing your CSS.

Here's the question app with classvar CSS:

question03.py
from textual.app import App, ComposeResult\nfrom textual.widgets import Label, Button\nclass QuestionApp(App[str]):\nCSS = \"\"\"\n    Screen {\n        layout: grid;\n        grid-size: 2;\n        grid-gutter: 2;\n        padding: 2;\n    }\n    #question {\n        width: 100%;\n        height: 100%;\n        column-span: 2;\n        content-align: center bottom;\n        text-style: bold;\n    }\n    Button {\n        width: 100%;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Label(\"Do you love Textual?\", id=\"question\")\nyield Button(\"Yes\", id=\"yes\", variant=\"primary\")\nyield Button(\"No\", id=\"no\", variant=\"error\")\ndef on_button_pressed(self, event: Button.Pressed) -> None:\nself.exit(event.button.id)\nif __name__ == \"__main__\":\napp = QuestionApp()\nreply = app.run()\nprint(reply)\n
"},{"location":"guide/app/#title-and-subtitle","title":"Title and subtitle","text":"

Textual apps have a title attribute which is typically the name of your application, and an optional sub_title attribute which adds additional context (such as the file your are working on). By default, title will be set to the name of your App class, and sub_title is empty. You can change these defaults by defining TITLE and SUB_TITLE class variables. Here's an example of that:

question_title01.py
from textual.app import App, ComposeResult\nfrom textual.widgets import Button, Header, Label\nclass MyApp(App[str]):\nCSS_PATH = \"question02.tcss\"\nTITLE = \"A Question App\"\nSUB_TITLE = \"The most important question\"\ndef compose(self) -> ComposeResult:\nyield Header()\nyield Label(\"Do you love Textual?\", id=\"question\")\nyield Button(\"Yes\", id=\"yes\", variant=\"primary\")\nyield Button(\"No\", id=\"no\", variant=\"error\")\ndef on_button_pressed(self, event: Button.Pressed) -> None:\nself.exit(event.button.id)\nif __name__ == \"__main__\":\napp = MyApp()\nreply = app.run()\nprint(reply)\n

Note that the title and subtitle are displayed by the builtin Header widget at the top of the screen:

A\u00a0Question\u00a0App \u2b58A\u00a0Question\u00a0App\u00a0\u2014\u00a0The\u00a0most\u00a0important\u00a0question Do\u00a0you\u00a0love\u00a0Textual? \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Yes\u00a0\u00a0No\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

You can also set the title attributes dynamically within a method of your app. The following example sets the title and subtitle in response to a key press:

question_title02.py
from textual.app import App, ComposeResult\nfrom textual.events import Key\nfrom textual.widgets import Button, Header, Label\nclass MyApp(App[str]):\nCSS_PATH = \"question02.tcss\"\nTITLE = \"A Question App\"\nSUB_TITLE = \"The most important question\"\ndef compose(self) -> ComposeResult:\nyield Header()\nyield Label(\"Do you love Textual?\", id=\"question\")\nyield Button(\"Yes\", id=\"yes\", variant=\"primary\")\nyield Button(\"No\", id=\"no\", variant=\"error\")\ndef on_button_pressed(self, event: Button.Pressed) -> None:\nself.exit(event.button.id)\ndef on_key(self, event: Key):\nself.title = event.key\nself.sub_title = f\"You just pressed {event.key}!\"\nif __name__ == \"__main__\":\napp = MyApp()\nreply = app.run()\nprint(reply)\n

If you run this app and press the T key, you should see the header update accordingly:

A\u00a0Question\u00a0App \u2b58t\u00a0\u2014\u00a0You\u00a0just\u00a0pressed\u00a0t! Do\u00a0you\u00a0love\u00a0Textual? \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Yes\u00a0\u00a0No\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

Info

Note that there is no need to explicitly refresh the screen when setting the title attributes. This is an example of reactivity, which we will cover later in the guide.

"},{"location":"guide/app/#whats-next","title":"What's next","text":"

In the following chapter we will learn more about how to apply styles to your widgets and app.

"},{"location":"guide/command_palette/","title":"Command Palette","text":"

Textual apps have a built-in command palette, which gives users a quick way to access certain functionality within your app.

In this chapter we will explain what a command palette is, how to use it, and how you can add your own commands.

"},{"location":"guide/command_palette/#launching-the-command-palette","title":"Launching the command palette","text":"

Press Ctrl + \\ (ctrl and backslash) to invoke the command palette screen, which contains of a single input widget. Textual will suggest commands as you type in that input. Press Up or Down to select a command from the list, and Enter to invoke it.

Commands are looked up via a fuzzy search, which means Textual will show commands that match the keys you type in the same order, but not necessarily at the start of the command. For instance the \"Toggle light/dark mode\" command will be shown if you type \"to\" (for toggle), but you could also type \"dm\" (to match dark mode). This scheme allows the user to quickly get to a particular command with a minimum of key-presses.

Command PaletteCommand Palette after 't'Command Palette after 'td'

ViewerApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \ud83d\udd0eCommand\u00a0Palette\u00a0Search... \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

ViewerApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \ud83d\udd0et Toggle\u00a0light/dark\u00a0mode Toggle\u00a0the\u00a0application\u00a0between\u00a0light\u00a0and\u00a0dark\u00a0mode Quit\u00a0the\u00a0application Quit\u00a0the\u00a0application\u00a0as\u00a0soon\u00a0as\u00a0possible Ring\u00a0the\u00a0bell Ring\u00a0the\u00a0terminal's\u00a0'bell' \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

ViewerApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \ud83d\udd0etd Toggle\u00a0light/dark\u00a0mode Toggle\u00a0the\u00a0application\u00a0between\u00a0light\u00a0and\u00a0dark\u00a0mode \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

"},{"location":"guide/command_palette/#default-commands","title":"Default commands","text":"

Textual apps have the following commands enabled by default:

  • \"Toggle light/dark mode\" This will toggle between light and dark mode, by setting App.dark to either True or False.
  • \"Quit the application\" Quits the application. The equivalent of pressing ++ctrl+C++.
  • \"Play the bell\" Plays the terminal bell, by calling App.bell.
"},{"location":"guide/command_palette/#command-providers","title":"Command providers","text":"

To add your own command(s) to the command palette, define a command.Provider class then add it to the COMMANDS class var on your App class.

Let's look at a simple example which adds the ability to open Python files via the command palette.

The following example will display a blank screen initially, but if you bring up the command palette and start typing the name of a Python file, it will show the command to open it.

Tip

If you are running that example from the repository, you may want to add some additional Python files to see how the examples works with multiple files.

command01.py
from __future__ import annotations\nfrom functools import partial\nfrom pathlib import Path\nfrom rich.syntax import Syntax\nfrom textual.app import App, ComposeResult\nfrom textual.command import Hit, Hits, Provider\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Static\nclass PythonFileCommands(Provider):\n\"\"\"A command provider to open a Python file in the current working directory.\"\"\"\ndef read_files(self) -> list[Path]:\n\"\"\"Get a list of Python files in the current working directory.\"\"\"\nreturn list(Path(\"./\").glob(\"*.py\"))\nasync def startup(self) -> None:  # (1)!\n\"\"\"Called once when the command palette is opened, prior to searching.\"\"\"\nworker = self.app.run_worker(self.read_files, thread=True)\nself.python_paths = await worker.wait()\nasync def search(self, query: str) -> Hits:  # (2)!\n\"\"\"Search for Python files.\"\"\"\nmatcher = self.matcher(query)  # (3)!\napp = self.app\nassert isinstance(app, ViewerApp)\nfor path in self.python_paths:\ncommand = f\"open {str(path)}\"\nscore = matcher.match(command)  # (4)!\nif score > 0:\nyield Hit(\nscore,\nmatcher.highlight(command),  # (5)!\npartial(app.open_file, path),\nhelp=\"Open this file in the viewer\",\n)\nclass ViewerApp(App):\n\"\"\"Demonstrate a command source.\"\"\"\nCOMMANDS = App.COMMANDS | {PythonFileCommands}  # (6)!\ndef compose(self) -> ComposeResult:\nwith VerticalScroll():\nyield Static(id=\"code\", expand=True)\ndef open_file(self, path: Path) -> None:\n\"\"\"Open and display a file with syntax highlighting.\"\"\"\nsyntax = Syntax.from_path(\nstr(path),\nline_numbers=True,\nword_wrap=False,\nindent_guides=True,\ntheme=\"github-dark\",\n)\nself.query_one(\"#code\", Static).update(syntax)\nif __name__ == \"__main__\":\napp = ViewerApp()\napp.run()\n
  1. This method is called when the command palette is first opened.
  2. Called on each key-press.
  3. Get a Matcher instance to compare against hits.
  4. Use the matcher to get a score.
  5. Highlights matching letters in the search.
  6. Adds our custom command provider and the default command provider.

There are three methods you can override in a command provider: startup, search, and shutdown. All of these methods should be coroutines (async def). Only search is required, the other methods are optional. Let's explore those methods in detail.

"},{"location":"guide/command_palette/#startup-method","title":"startup method","text":"

The startup method is called when the command palette is opened. You can use this method as way of performing work that needs to be done prior to searching. In the example, we use this method to get the Python (.py) files in the current working directory.

"},{"location":"guide/command_palette/#search-method","title":"search method","text":"

The search method is responsible for finding results (or hits) that match the user's input. This method should yield Hit objects for any command that matches the query argument.

Exactly how the matching is implemented is up to the author of the command provider, but we recommend using the builtin fuzzy matcher object, which you can get by calling matcher. This object has a match() method which compares the user's search term against the potential command and returns a score. A score of zero means no hit, and you can discard the potential command. A score of above zero indicates the confidence in the result, where 1 is an exact match, and anything lower indicates a less confident match.

The Hit contains information about the score (used in ordering) and how the hit should be displayed, and an optional help string. It also contains a callback, which will be run if the user selects that command.

In the example above, the callback is a lambda which calls the open_file method in the example app.

Note

Unlike most other places in Textual, errors in command provider will not exit the app. This is a deliberate design decision taken to prevent a single broken Provider class from making the command palette unusable. Errors in command providers will be logged to the console.

"},{"location":"guide/command_palette/#shutdown-method","title":"Shutdown method","text":"

The shutdown method is called when the command palette is closed. You can use this as a hook to gracefully close any objects you created in startup.

"},{"location":"guide/command_palette/#screen-commands","title":"Screen commands","text":"

You can also associate commands with a screen by adding a COMMANDS class var to your Screen class.

Commands defined on a screen are only considered when that screen is active. You can use this to implement commands that are specific to a particular screen, that wouldn't be applicable everywhere in the app.

"},{"location":"guide/command_palette/#disabling-the-command-palette","title":"Disabling the command palette","text":"

The command palette is enabled by default. If you would prefer not to have the command palette, you can set ENABLE_COMMAND_PALETTE = False on your app class.

Here's an app class with no command palette:

class NoPaletteApp(App):\nENABLE_COMMAND_PALETTE = False\n
"},{"location":"guide/design/","title":"Design System","text":"

Textual's design system consists of a number of predefined colors and guidelines for how to use them in your app.

You don't have to follow these guidelines, but if you do, you will be able to mix builtin widgets with third party widgets and your own creations, without worrying about clashing colors.

Information

Textual's color system is based on Google's Material design system, modified to suit the terminal.

"},{"location":"guide/design/#designing-with-colors","title":"Designing with Colors","text":"

Textual pre-defines a number of colors as CSS variables. For instance, the CSS variable $primary is set to #004578 (the blue used in headers). You can use $primary in place of the color in the background and color rules, or other any other rule that accepts a color.

Here's an example of CSS that uses color variables:

MyWidget {\nbackground: $primary;\ncolor: $text;\n}\n

Using variables rather than explicit colors allows Textual to apply color themes. Textual supplies a default light and dark theme, but in the future many more themes will be available.

"},{"location":"guide/design/#base-colors","title":"Base Colors","text":"

There are 12 base colors defined in the color scheme. The following table lists each of the color names (as used in CSS) and a description of where to use them.

Color Description $primary The primary color, can be considered the branding color. Typically used for titles, and backgrounds for strong emphasis. $secondary An alternative branding color, used for similar purposes as $primary, where an app needs to differentiate something from the primary color. $primary-background The primary color applied to a background. On light mode this is the same as $primary. In dark mode this is a dimmed version of $primary. $secondary-background The secondary color applied to a background. On light mode this is the same as $secondary. In dark mode this is a dimmed version of $secondary. $background A color used for the background, where there is no content. $surface The color underneath text. $panel A color used to differentiate a part of the UI form the main content. Typically used for dialogs or sidebars. $boost A color with alpha that can be used to create layers on a background. $warning Indicates a warning. Text or background. $error Indicates an error. Text or background. $success Used to indicate success. Text or background. $accent Used sparingly to draw attention to a part of the UI (typically borders around focused widgets)."},{"location":"guide/design/#shades","title":"Shades","text":"

For every color, Textual generates 3 dark shades and 3 light shades.

  • Add -lighten-1, -lighten-2, or -lighten-3 to the color's variable name to get lighter shades (3 is the lightest).
  • Add -darken-1, -darken-2, and -darken-3 to a color to get the darker shades (3 is the darkest).

For example, $secondary-darken-1 is a slightly darkened $secondary, and $error-lighten-3 is a very light version of the $error color.

"},{"location":"guide/design/#dark-mode","title":"Dark mode","text":"

There are two color themes in Textual, a light mode and dark mode. You can switch between them by toggling the dark attribute on the App class.

In dark mode $background and $surface are off-black. Dark mode also set $primary-background and $secondary-background to dark versions of $primary and $secondary.

"},{"location":"guide/design/#text-color","title":"Text color","text":"

The design system defines three CSS variables you should use for text color.

  • $text sets the color of text in your app. Most text in your app should have this color.
  • $text-muted sets a slightly faded text color. Use this for text which has lower importance. For instance a sub-title or supplementary information.
  • $text-disabled sets faded out text which indicates it has been disabled. For instance, menu items which are not applicable and can't be clicked.

You can set these colors via the color property. The design system uses auto colors for text, which means that Textual will pick either white or black (whichever has better contrast).

Information

These text colors all have some alpha applied, so that even $text isn't pure white or pure black. This is done because blending in a little of the background color produces text that is not so harsh on the eyes.

"},{"location":"guide/design/#theming","title":"Theming","text":"

In a future version of Textual you will be able to modify theme colors directly, and allow users to configure preferred themes.

"},{"location":"guide/design/#color-preview","title":"Color Preview","text":"

Run the following from the command line to preview the colors defined in the color system:

textual colors\n
"},{"location":"guide/design/#theme-reference","title":"Theme Reference","text":"

Here's a list of the colors defined in the default light and dark themes.

Textual\u00a0Theme\u00a0Colors \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Light\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Dark\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0$primary-background-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-background-darken-3\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0$primary-background-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-background-darken-2\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0$primary-background-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-background-darken-1\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-background\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-background\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0$primary-background-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-background-lighten-1\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0$primary-background-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-background-lighten-2\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0$primary-background-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-background-lighten-3\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0$secondary-background-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-background-darken-3\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0$secondary-background-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-background-darken-2\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0$secondary-background-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-background-darken-1\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-background\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-background\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0$secondary-background-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-background-lighten-1\u00a0\u00a0\u00a0 \u00a0\u00a0$secondary-background-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-background-lighten-2\u00a0\u00a0\u00a0 \u00a0\u00a0$secondary-background-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-background-lighten-3\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0

"},{"location":"guide/devtools/","title":"Devtools","text":"

Note

If you don't have the textual command on your path, you may have forgotten to install the textual-dev package.

See getting started for details.

Textual comes with a command line application of the same name. The textual command is a super useful tool that will help you to build apps.

Take a moment to look through the available subcommands. There will be even more helpful tools here in the future.

textual --help\n
"},{"location":"guide/devtools/#run","title":"Run","text":"

The run sub-command runs Textual apps. If you supply a path to a Python file it will load and run the app.

textual run my_app.py\n

This is equivalent to running python my_app.py from the command prompt, but will allow you to set various switches which can help you debug, such as --dev which enable the Console.

See the run subcommand's help for details:

textual run --help\n

You can also run Textual apps from a python import. The following command would import music.play and run a Textual app in that module:

textual run music.play\n

This assumes you have a Textual app instance called app in music.play. If your app has a different name, you can append it after a colon:

textual run music.play:MusicPlayerApp\n

Note

This works for both Textual app instances and classes.

"},{"location":"guide/devtools/#running-from-commands","title":"Running from commands","text":"

If your app is installed as a command line script, you can use the -c switch to run it. For instance, the following will run the textual colors command:

textual run -c textual colors\n
"},{"location":"guide/devtools/#live-editing","title":"Live editing","text":"

If you combine the run command with the --dev switch your app will run in development mode.

textual run --dev my_app.py\n

One of the features of dev mode is live editing of CSS files: any changes to your CSS will be reflected in the terminal a few milliseconds later.

This is a great feature for iterating on your app's look and feel. Open the CSS in your editor and have your app running in a terminal. Edits to your CSS will appear almost immediately after you save.

"},{"location":"guide/devtools/#console","title":"Console","text":"

When building a typical terminal application you are generally unable to use print when debugging (or log to the console). This is because anything you write to standard output will overwrite application content. Textual has a solution to this in the form of a debug console which restores print and adds a few additional features to help you debug.

To use the console, open up two terminal emulators. Run the following in one of the terminals:

textual console\n

You should see the Textual devtools welcome message:

textual\u00a0console \u258cTextual\u00a0Development\u00a0Console\u00a0v0.36.0 \u258cRun\u00a0a\u00a0Textual\u00a0app\u00a0with\u00a0textual\u00a0run\u00a0--dev\u00a0my_app.py\u00a0to\u00a0connect. \u258cPress\u00a0Ctrl+C\u00a0to\u00a0quit.

In the other console, run your application with textual run and the --dev switch:

textual run --dev my_app.py\n

Anything you print from your application will be displayed in the console window. Textual will also write log messages to this window which may be helpful when debugging your application.

"},{"location":"guide/devtools/#increasing-verbosity","title":"Increasing verbosity","text":"

Textual writes log messages to inform you about certain events, such as when the user presses a key or clicks on the terminal. To avoid swamping you with too much information, some events are marked as \"verbose\" and will be excluded from the logs. If you want to see these log messages, you can add the -v switch.

textual console -v\n
"},{"location":"guide/devtools/#decreasing-verbosity","title":"Decreasing verbosity","text":"

Log messages are classififed in to groups, and the -x flag can be used to exclude all message from a group. The groups are: EVENT, DEBUG, INFO, WARNING, ERROR, PRINT, SYSTEM, and LOGGING. The group a message belongs to is printed after its timestamp.

Multiple groups may be excluded, for example to exclude everything except warning, errors, and print statements:

textual console -x SYSTEM -x EVENT -x DEBUG -x INFO\n
"},{"location":"guide/devtools/#custom-port","title":"Custom port","text":"

You can use the option --port to specify a custom port to run the console on, which comes in handy if you have other software running on the port that Textual uses by default:

textual console --port 7342\n

Then, use the command run with the same --port option:

textual run --dev --port 7342 my_app.py\n
"},{"location":"guide/devtools/#textual-log","title":"Textual log","text":"

Use the log function to pretty-print data structures and anything that Rich can display.

You can import the log function as follows:

from textual import log\n

Here's a few examples of writing to the console, with log:

def on_mount(self) -> None:\nlog(\"Hello, World\")  # simple string\nlog(locals())  # Log local variables\nlog(children=self.children, pi=3.141592)  # key/values\nlog(self.tree)  # Rich renderables\n
"},{"location":"guide/devtools/#log-method","title":"Log method","text":"

There's a convenient shortcut to log on the App and Widget objects. This is useful in event handlers. Here's an example:

from textual.app import App\nclass LogApp(App):\ndef on_load(self):\nself.log(\"In the log handler!\", pi=3.141529)\ndef on_mount(self):\nself.log(self.tree)\nif __name__ == \"__main__\":\nLogApp().run()\n
"},{"location":"guide/devtools/#logging-handler","title":"Logging handler","text":"

Textual has a logging handler which will write anything logged via the builtin logging library to the devtools. This may be useful if you have a third-party library that uses the logging module, and you want to see those logs with Textual logs.

Note

The logging library works with strings only, so you won't be able to log Rich renderables such as self.tree with the logging handler.

Here's an example of configuring logging to use the TextualHandler.

import logging\nfrom textual.app import App\nfrom textual.logging import TextualHandler\nlogging.basicConfig(\nlevel=\"NOTSET\",\nhandlers=[TextualHandler()],\n)\nclass LogApp(App):\n\"\"\"Using logging with Textual.\"\"\"\ndef on_mount(self) -> None:\nlogging.debug(\"Logged via TextualHandler\")\nif __name__ == \"__main__\":\nLogApp().run()\n
"},{"location":"guide/events/","title":"Events and Messages","text":"

We've used event handler methods in many of the examples in this guide. This chapter explores events and messages (see below) in more detail.

"},{"location":"guide/events/#messages","title":"Messages","text":"

Events are a particular kind of message sent by Textual in response to input and other state changes. Events are reserved for use by Textual, but you can also create custom messages for the purpose of coordinating between widgets in your app.

More on that later, but for now keep in mind that events are also messages, and anything that is true of messages is true of events.

"},{"location":"guide/events/#message-queue","title":"Message Queue","text":"

Every App and Widget object contains a message queue. You can think of a message queue as orders at a restaurant. The chef takes an order and makes the dish. Orders that arrive while the chef is cooking are placed in a line. When the chef has finished a dish they pick up the next order in the line.

Textual processes messages in the same way. Messages are picked off a queue and processed (cooked) by a handler method. This guarantees messages and events are processed even if your code can not handle them right away.

This processing of messages is done within an asyncio Task which is started when you mount the widget. The task monitors a queue for new messages and dispatches them to the appropriate handler when they arrive.

Tip

The FastAPI docs have an excellent introduction to Python async programming.

By way of an example, let's consider what happens if you were to type \"Text\" in to a Input widget. When you hit the T key, Textual creates a key event and sends it to the widget's message queue. Ditto for E, X, and T.

The widget's task will pick the first message from the queue (a key event for the T key) and call the on_key method with the event as the first argument. In other words it will call Input.on_key(event), which updates the display to show the new letter.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXGtT28hcdTAwMTL9zq+g2C97q4Iyj57XVm3dXG6Eh8NcdTAwMWJCSPbuXHUwMDE2JWzZViw/sGRcZknlv99cdTAwMWVcdTAwMDGWLEu2McaYutdcdFx1MDAxOEujUVvTp/ucnpF+rqyurkV3XHUwMDFkb+2P1TXvtuxcdTAwMDZ+pev2197Z7TdeN/TbLdzF4s9hu9ctxy3rUdRcdP94/77pdlx1MDAxYl7UXHTcsufc+GHPXHLCqFfx20653XzvR14z/Lf9feg2vT877WYl6jrJSda9ilx1MDAxZrW79+fyXHUwMDAyr+m1olx1MDAxMHv/XHUwMDBmfl5d/Vx1MDAxOf9OWVx1MDAxN/gtL25cdTAwMWJvTWzjhGW3XHUwMDFltluxnYxpJYFcbjlo4IdcdTAwMWbxTJFXwb1VtNZL9thNa3ew7a9v1GuNw6P92uXn453d/r5Mzlr1g+Asulx1MDAwYu4vgluu97opm8Ko2254XHUwMDE3fiWqP16z1PbBcVx1MDAxNTeso1x1MDAwMYPd3XavVm95of3udLC13XHLfnRnt1x1MDAxMTLY6rZqcSfJllv8XHUwMDA0TDlcXFFiuFx1MDAxMoNcdTAwMWT2UC7AoZRRZncxo1x1MDAxNM9cdTAwMTi12Vx1MDAwZXBcdTAwMDTQqN9I/EqsunLLjVx1MDAxYZrWqlxm2kRdt1x1MDAxNXbcLo5T0q7/+HVcdTAwMWRJQSpcbmBwREApXHUwMDE4NKl7fq1cdTAwMWVZczRzXHUwMDE4lZRcbs2pXHUwMDAyloxL6MVDXCK0XHUwMDA2LiE51FrQKVVit/gne0HrbrfzcN3WYktT1tuPWymfSlx1MDAwZe51Ku69XHUwMDAzUCk5XHUwMDEzaCmXLLmg6GdcctzZ6lx1MDAwNUGyrV1u5PhMXHUwMDE4ud1ow29V/FYte4jXqlx1MDAxNOxcdNww2mw3m36EZlx1MDAxY7f9VpRtXHUwMDEx9/uh2233655byem5cF/HdpeAyL6Sv1ZcdTAwMTO3iT9cZv7+511u6+Ihta+RwUy6W0m//3r3RDxLkt36iGdcdTAwMDVUXHUwMDEyTVJccibhueyT7cMvnXWz8dm/8b5cdTAwMDU7p+ftT0uPZ0Ul4llcdTAwMWJcIllcdTAwMTbP3MGQxlx0XHUwMDA3/Mf0y8HZOIRxjfiQglBcbjRcdTAwMGbNyjhMMcE5XHUwMDA1wVjyVVx1MDAxZsBMNTWMS6lcdTAwMTKY/1x1MDAxZs6vXGLnwiG1r+xgPlx1MDAxMcxdr1x1MDAxY937clx1MDAwZaKFVEWIplxuXHUwMDA3jFx1MDAwYlx1MDAwNXpqSJ9/b1xis8v5XHUwMDA1bFx1MDAxZVx1MDAxZV1Hd5dHSjZngzTNuuDjcWFcdTAwMWIpylxcMzRwR2mBVCSDaClExognYfg3KEuvKlx1MDAwMHJcdTAwMTKyTK7pXHUwMDAwszpcdTAwMTXFXHUwMDFlUMqBMMFcdTAwMDQkXHUwMDA2z1xypVx1MDAwMyf6mfK0gc9E3m1cdTAwMTJ4Ulx1MDAwM1xcPWq4ncjb0Xe1T0dbknxcdTAwMDH6w19cdTAwMWK0+/Uuv9v7gzfV0Y3r9Xav3fN+dHHEXHUwMDBmw8Nob/gsj+d3LepS/T46+lx1MDAxY0PLWMxcZn3/NFxcaCFcXDDVcmGQ1k6NlvyLufRo0Vx1MDAwNWjR4DxcdTAwMGIv4ymsyEFcZuNZxFx1MDAwMKXKMMlnSGuh/bDotFZtt6Iz/0csiMjQ1m236Vx1MDAwNzGvXHUwMDE4bI6d0mrBXHUwMDFia5Oz59393vDu/vx77fPfa/9KX9rQi1x0XHUwMDFj2meGXHUwMDBl/lx1MDAxMPi1Vky9sFx1MDAwM6875OCRj9pv0KDpVyrphFFGi1xc7LNbmibOt7t+zW+5weexXHUwMDA2z560kPNcdTAwMTYmLcGUXCKYJNnUMCTVjdb2xkWTXHUwMDFk7PbPvm9dnVx1MDAxZJWbfPlhSFx1MDAxZKo5Q6bJjaaSXHUwMDBmYVFcYuEg61dcdTAwMDb5qFx1MDAwNinUc3A5hzymKCVcblxy0U+H5Wx57H5kw89cdTAwMDcnXHUwMDFmLjpy0z24U7J1XHUwMDFhfe3fVvNcdTAwMTNOjK2JeWxSesw/4fKlMfSeQlx1MDAwMCHTXHUwMDA0LZWeXsiNv8xLXHUwMDBiIDlcdTAwMGVAmMzMvFx1MDAwMDSPxEa1llx1MDAwNNW+nIFcbr7hzOYtOrNNSFx1MDAwNlx1MDAxMzObNzGz3VPbXHUwMDFjUFwiqSpcdTAwMDIlXHUwMDAwXHUwMDAzxSVMXHLJ8VR7SSEpNHU4IZwrKrVG2TNcdTAwMDRJhTnN4GZNWTHNxGtULavx6azqoqRiUo5iXHUwMDExUypcdTAwMDDmKq2I1Pa/XHUwMDE4hSaAY00kWnMlXHUwMDA1g5HSikBcdTAwMWLwXHUwMDFi0DdaWUny3WPdf1x1MDAxYcpcdTAwMTdDu9yzVq5cdTAwMTNcdTAwMDdcdTAwMDNcdTAwMTWOlK3uXHUwMDBiW1xiXHUwMDAzwVPtam4njmeDwXzYNci5w/WcXCJ7dvrkR61xcvDtU/PM6NBcdTAwMWNdnlx1MDAxZbM8e9BcdTAwMWPCLP1AUqRMbFx1MDAxNchcdTAwMTF7XHUwMDE4sVU9Jlx1MDAwNGZcdTAwMDKQXHUwMDFj4/6IWXMuJmVcdTAwMDPBXFzrScWebF8jPpx0t5J+n4mcayOzW1x1MDAxM26BmVRSdIqpXHUwMDAz2aHZuNBcdTAwMDY2TrpcdTAwMDem8uV6//r6Q+922Vx1MDAwM1x1MDAxOVx1MDAwMHFQgiB70KhGbVx1MDAxMW0okiFcdTAwMWVcdTAwMWNFiFx1MDAxMdxcdTAwMDBcdTAwMGVcdTAwMDFJz7W8XHUwMDBlPVx1MDAxN1x1MDAxOHpcdTAwMTFcdTAwMWJcdTAwMGIrM92Prdzp175931xc3708//bps/6q6mqDPYeev1C3k1h//lx0p7T2W2f/ekdvVIOvXHUwMDE3f7ntvV23tHnWfltFMVxyxTVkQe0sr2HTU5fxw7e0iFx1MDAxN2NcdTAwMTGvucPmhvh56Fx0rVx1MDAwNDpH+ov9L8iJ20XLiVx06WuinLidKCdcbivVKchlQGlcYqXUwPRcdTAwMDJfbrPaycc72DpcdTAwMTW9nql978nLzt2yQ1JcdTAwMTnioGRcdTAwMWWpU1x1MDAwYtxupFYvNbeD3DdcdTAwMDeAXCJcdTAwMGJANEzb4MhcdTAwMTZcXFx1MDAxM1uufPNcdTAwMTR80yGIWlx1MDAwM1x1MDAwZvB6uDVv9brn9bx8XFzroYNcdTAwMDawXHK8ajRcdTAwMDbVUbtTXHUwMDA06aEvk8XvsEFjcVtYXHUwMDA2oHRMOqXcIJtcdTAwMTdPwO748V5S7GqgXHUwMDBlXHUwMDAwXHUwMDE4SbmmXFyZXGaCNThcZlx1MDAxOH1BXGZT7lCFSoZcYsZcdTAwMDQjyYBcZlx1MDAxMG2UwzhcdTAwMDGJTYhSSo+sl1x1MDAwMkzzgmj2XHUwMDAypPo161x1MDAwMONzwWq2XHUwMDBlgNFXgWGUaaF5stZg9VF3S0ehKJSzVlx1MDAwMcbn12FrcDhccjNKcCEwL+SWXHUwMDAwXGJcdTAwMDAj2MagXHUwMDFlXHUwMDAypkdsekslgPVCJ473jvhv0t9K+r0oflV8t9lOe2lqdkFcdTAwMTSuXHUwMDEyY4Rb5snp9Ms+x1x1MDAxN3qWNIBh8HK0XHUwMDE0dnEnXHUwMDE1SpiMXHUwMDFlwKjgMGUwglx1MDAwMTJcdTAwMTHgJmPY0+LYfUEzt1x1MDAwMKByuFxiZcaRoCG9IPUhZiFOXHUwMDE5oF2zyILnkJJnrFxmeZff7yTNLi5PXCLYgNOrvd3u7dFx+aJeOjmeVrP/pc7Pt672zlx1MDAwZvC6d3+cd8PN2+rB/DiUVjzh7i+k2XnxQlx1MDAxNiaBaOtPU0M0/2IuPUTNWIgqjvKBXHUwMDAzkZZoPFx1MDAxN6LjanR5cmF0/lx1MDAwZrRWXHUwMDEyg/lbWX89k2Bvty5R9/5cdTAwMWXL4MVcbvVcdFkmS/SHXHKdXHSBjFx1MDAxNM/BI/9QWpjpk+TJlemTm6PNXHUwMDEyLdPti7vatquDcNlcdTAwMTGokIMgK2RcXFKquExcdTAwMTWm4/k+TVx1MDAxZGOsXG5cdTAwMDDQ3OiXypGU58zyjep1ZVxiVUy/XHUwMDAw/F4x0yxUrW9ZsKzW3Vx1MDAxNuKwm1x1MDAwZu7FqvVhg8aCuFCtKz6O6zKM2IRNL9bHXHUwMDBm97LCWHNcdTAwMDcpP1hcdTAwMWOPwlhcdTAwMTPqXGKhha1+XHUwMDEzolPMZr4wNlxmo1x1MDAwNWpNlN2a4agksXNcdTAwMDBqQVx1MDAxZFwiXHUwMDA0xWCj8Vx1MDAwN1LceLDIRtgpU/1cdTAwMDIludeU7OOTw2p6qlxcoTI2wOyEoFxyeCzV6GHeXHUwMDFlXHUwMDFjylx1MDAxNzNtz/GyKM5cdGpx1KlKiFFjhMM1XHUwMDAwupZUWinJzYhRb0qxXHUwMDE3+rB9jXhv0t1K+n2mSXtKivVcdTAwMDBcdTAwMDVcdTAwMTTtXHUwMDA0zZk+jqldz5elXm1cdTAwMDM2LkNZ+Ytz9/Ro2eNcdTAwMThyfMdII6XAK0+0XHUwMDFjjmNcXChcdTAwMDe4jitIhEry2ktqhTBcdTAwMDQlXHUwMDAxWfD0Qalx2bioXHUwMDA2/CvtnqhcdTAwMGa3lZ0621x1MDAwZZ4/Z/9Wup1UVsg/4YuSsrGgL1x1MDAxMlx1MDAxZkZcdTAwMTXSXHUwMDE2VJqc2rtKp7+RZfxlXla4XHUwMDAzjIO7lo6ZXHUwMDE33OcxYc+IXHUwMDE0KJhmwftcdTAwMWKesI9cdTAwMTY9YT8hc02csI+ed2eLLr6zhVx1MDAxOUzDVlx1MDAwZk9ccstcdTAwMDNSql5cdTAwMWNdRKWQ7ZQvvG+eOP9SKoBludtcdTAwMGXD9bpcdTAwMWKV669cdTAwMGZNpHNcdTAwMGUyPs24Xb+YWmJcdTAwMWJ7XHJoh1x1MDAxMW0pIVx1MDAxNUSl65TzhqZQXGbZruFcdTAwMDaZJSbr1PrsoVx1MDAxYq1cdMdcdTAwMTbI2URcXLdcdTAwMTlcdTAwMDUuoFx1MDAwZeTmXHUwMDE1bkmjT1x1MDAwMO7sPitk4UM+7O3wXHUwMDFhuez0qaTSb7tl3W5tXHUwMDA3VyGr1Fp998dOUSrJ+N2Uj1x1MDAwNHhcdTAwMTFvxcG3t0pcdTAwMWJOhcy6q33+XHUwMDA3XHUwMDExhkrQRKpcdTAwMTdcXPolXHUwMDE0cVx1MDAwMKhRQJhGRZ6TVzhx7FJoSbhBXHUwMDExh/pqhFcqXHUwMDAzXHUwMDAyMM+8wsKwubmrXHUwMDE3XHUwMDA0fifMd1ZerHNcYlx1MDAxN1ZdmelXJ1+VTndcci25n8pcdTAwMDErl1x1MDAwM6M/qkZvXHUwMDE2b11gbMWAhY5cbkQwpDU8VVa9VzlcdTAwMWElNXqytM8jXHUwMDAwMY9qTd5cdTAwMTIphyiD0TteXHUwMDFmQSCnVkPtgklbXHUwMDE2tus8XHUwMDE4KtBRXHUwMDA1JCVSXCLD2VxmJdk34ammcH5AKo5xRD1hXHUwMDEyvXlrbrauv3/8us+jq1x1MDAxYrfqXHUwMDFk7+1cdTAwMTXd47okjootlIP5X2AsI4qAyD5uRWEoI1xc2ck5JVKqeTZPvVwiRLyQp1x1MDAwMrpcdTAwMGXG2te4dW9cdTAwMTGOKkFcdTAwMTQ5KqPx04fQW6f21HOXlEo/tpplJk5O9XW9XFzz5e6yeyrHXHUwMDFjT1x1MDAxOUFMYlx1MDAwZeFcIsNcdTAwMDCEdOzDkYjUklx1MDAxMmDPm8ii7ErrvHvX5lx1MDAxMlNcdTAwMDHiqDpLXHUwMDA1fGlcXHX8o1x1MDAwM3Rx4UNrJGmM8enDqiq3dvfDm1x1MDAxZF1vymr3MvhQqvpmtsLH4vgqRUnlUKGZNMo+XHUwMDA3avg2S8GIozVFSYNcdTAwMTRcdTAwMDFkXHUwMDE2RfNjq9xcdTAwMTBHWSdcdTAwMTSA0VvTvFx0XHUwMDFi7iBcdTAwMTNcdTAwMDVqn+oh7TrQrL9yaiTmhtd43Fx1MDAwN1+Iu45Zxc9cdTAwMDE4t3Cd2lt31zuNnb1bV1x1MDAxZl/pfr9zXHUwMDFjnFx1MDAxZpr9ZVx1MDAwZq2KO0rZKVx1MDAxMeuvOlWHi52VY9RcdTAwMDP0JFx1MDAwZZpcdTAwMTIqnuWtXHUwMDBmRfmc0KpcdTAwMWNcdTAwMTR3qGONQHVl1ySPuirTjp2HQkuJoExcdTAwMTgyqqwk2FuDXHUwMDA0vEJsfbqzrjxcdTAwMTSp19xO5yzCLtdcdTAwMWXn9NBqv/JQ2Uu6Wbvxvf5GXHUwMDFls4pftiRcdTAwMTZcdTAwMDPAeplnbf75a+XXf1x1MDAwMfpa2G0ifQ== events.Key(key=\"T\")events.Key(key=\"e\")events.Key(key=\"x\")Message queueon_key(event)Event handlerevents.Key(key=\"t\")

When the on_key method returns, Textual will get the next event from the queue and repeat the process for the remaining keys. At some point the queue will be empty and the widget is said to be in an idle state.

Note

This example illustrates a point, but a typical app will be fast enough to have processed a key before the next event arrives. So it is unlikely you will have so many key events in the message queue.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1daXPbOLb93r9cIpX+8l5Vi1xy3Fx1MDAwYlxcXHUwMDAwUzX1yrvlxI5cdTAwMTfF25splyxRiy1LskSvXf3f50JxLGohJVlL5ExUldgmaVx1MDAxMCbP3Vx1MDAwZi7++u3Dh4/RUzP8+I9cdTAwMGZcdTAwMWbDx0K+Vi228lx1MDAwZlx1MDAxZv/wx+/DVrvaqPMp6Pzcbty1XG6dKytR1Gz/488/b/Kt6zBq1vKFMLivtu/ytXZ0V6w2gkLj5s9qXHUwMDE03rT/z/+/l79cdP/ZbNxcdTAwMTSjVtC9SSYsVqNG69u9wlp4XHUwMDEz1qM2j/7//POHXHUwMDBmf3X+j82uVq2HnWs7R7tzU6D7j+416p15XCKR4tOye0G1vc53isJcIp8t8WzD7lx1MDAxOX/oY+FY1pu721x1MDAwN42709rGTqV1+bTXyHTvWqrWakfRU+3bQ8hcdTAwMTcqd63YnNpRq3FcdTAwMWSeVItR5fszi1x1MDAxZH/9vWK+XeFcdLyebjXuypV62PZ/u3w92mjmXHUwMDBi1ejJXHUwMDFmXHUwMDEz4vVovl7uXGbSPfLIP2VcdTAwMDBFYFx1MDAxZFx1MDAxOaW1QjSWXk/7XHUwMDAxMnxSKyc0WWONUYawb25rjVx1MDAxYb9cYp7b76Lz6U7uMl+4LvNcZuvF12uiVr7ebuZb/Lq61z18/6tcdTAwMDOSioxUyqFcdTAwMDDFd3u9pFx1MDAxMlbLlci/XHUwMDE2XHUwMDBiXHUwMDAxSJJSW5RGQXe27bDzZow2xlxup8zrXHQ/hWa22IHHv/tcdTAwMWZsJd9qvjy/j52pxqbvf9yIYav7y3fNYv5cdTAwMWJcdTAwMTAkXHUwMDExgjZcdTAwMWFcdTAwMTGlfT3PeLvmk/W7Wq17rFG4XHUwMDFlgp12lG9Fq9V6sVov9/9KWC8mnKnl29Fa4+amXHUwMDFh8TT2XHUwMDFi1XrUf0Vn3JVWq/FQXHTzxSEjJ55r+uG6wuQ/3e8+dOHT+eH1+3//MfTq5HfqP1x1MDAwM2+zO9xv8a9//zGZXFzHxbZPrqU1XHUwMDEy/VvrXHUwMDAyZJRgXHUwMDE3XHUwMDFmt+uHNbm/cqLXilLKp1P8urX0gk1cdTAwMTiAQaeVllx1MDAwNqxm+e5cdTAwMTVsXHUwMDE5KFx1MDAwM1JoaYGUk6J/arOTa1xmLFx0x4IpXHUwMDE0KiMsdoW2K9fGXHUwMDA1pNFJLdFcYsDYbL7LtZaSQHfVzy+5/oFynfxOO2f73+aEct1cblx1MDAwYtE3VFx1MDAwZlx1MDAxMW5cdTAwMGKq/+h34TYgyFkruiBcdTAwMTkl23pcdTAwMTez6vhuf0tcdTAwMWWXXHUwMDBln45WN6urXHUwMDBme2+TbdmPwe+/126w0zJbmy1VQGCsklpb/qe74uJHXHUwMDAwZ1x1MDAwMudcdTAwMDRcdTAwMTFYh85p0zeziUT7d1WgsKSVXHUwMDFhYrCpK1x1MDAxYa+SbFx1MDAwN0TXXHUwMDE5XCJWMNrNXnRfcfVXXGZ9L6/2+jS7cnZcdTAwMWGFZ1x1MDAwZW9Xwp2zbPSw1vxcdTAwMTiH6SveovAx+vh64u8/foZhe67+Y9xcdTAwMWJ2h/0uqDNUjqky3zPPmLhcdTAwMWIjksRdXHUwMDFhUsiuKcmx5T39MS+tvFx1MDAwYpcm71xiJlCzkvd0XHUwMDE3XVx1MDAwZpF4wH6JZ/3EXHUwMDEzVSrmc4wt8m3/w6KtdalRj46qz/7Rg+g5upm/qdY6XHUwMDBm+fVwXHUwMDA3qT7mvfdzXG4+hU//c1x1MDAxZD79819cdTAwMWbDf3383/izbYdcdTAwMWRcdTAwMDeV5+d6fnmlVi3XOyEjXHUwMDBmXHUwMDEwtnpQXHUwMDFmVTnGfb3gplosxq1ggWeU5zFb2XGsV6NVLVfr+VoudcJvt8RSOpckmyDY9+T34cZcdTAwMGagm0BQyX6y95VTKp5+NjuGtnaWXzalXHUwMDBljJLkUKFjV1x1MDAwN/pssVCBs8ZZJVxmsG+ippHN6W0xsVtG1mo5uWBOY4tLXHUwMDE2Tyh7sEVCXHUwMDFlZTZcdTAwMGLbUT17tT290fwvXHUwMDFmdpSJXHUwMDFmfsPlM/FSxtI1/TZeXHUwMDE44jBC4vg+ffpzXl49XCJS9VxiqkDNSo/MxMZrhWTi7+W/wcQ/LtrEjzCKI03845QmXHUwMDFlY2++XzSVcFx1MDAxY+lrM777fWLv7k5LVXyqf3Lbd+XqMUVN/Vx1MDAwZURTXHUwMDA11pJBpYXSZGLa6ttcYi4wXHUwMDE2vOdttXFcdTAwMTLtNLI5vY2X6KQlqWBcdTAwMGU58DT7tpYt3lZutrPQus3kSoXyVrNcdTAwMDJcdTAwMGbTm81fw85j2FG+w/BcdTAwMWIuoe9cdTAwMDAmMVx1MDAxZFxiXHUwMDAyyLDcqq5uXHUwMDFlpaDSn/PSKijWQGlcblxuXVx1MDAwMDNTULNwXHUwMDFlkDg45Hm8ISX4jp2HaNHOw1xiczvSeYimdFx1MDAxZYSEJNmUvqpsSajx81x1MDAwM6JcXCg+fd543nzOrZ6a1np0YrYuXHUwMDEzZLPQarTbmUo+KlQmrsXNXFw+Ob5cdKRcdTAwMDGSTrGEOid7xVNcdTAwMDdWXHUwMDBiw+60clx1MDAwNlx1MDAwNcr5pe/Yq1xiWFx1MDAxMtBnTaWMXHUwMDE1Sbt1OFx1MDAxMoFlh0YpQuzUXHUwMDA2+0WXWMU4K/Fcclx1MDAxOYRpRJescjSB6L5cdTAwMWS1XHUwMDA2XHUwMDEyXHUwMDEzzmA58lwimKB2/PDl7Ph+t+lobbN+vXffzl2VaDdcdTAwMDGzfbj7cWjFwFxuzViUgpW0xD6wXHUwMDAyaTYhPi5nXHUwMDE42XmCXHUwMDE1XHUwMDAzIUn74rU0zlxmMyw2IFx1MDAxMqhYj1x1MDAwMCFcdTAwMDNlXHUwMDAwrdJcIlnygcpyW5pUuIa1WrXZXHUwMDFlXG5W0olMXHUwMDA3p4W39+P7Puu3bq0sXHUwMDFh1fUoV6GV+pdqWTxfvVx1MDAwNauL83wk6MBcYumENlx1MDAwNlx1MDAxND952Vx1MDAwM1apXHUwMDAzjUaQY8xqXHUwMDA2K01Fcvi9lNegYVx1MDAxMKksMEpcdTAwMDBoR+xxgoonZ16hKiHQRMqxUlx1MDAxNYBcYjFcdTAwMTftO1RcdTAwMWRJY1x1MDAxNDtoPydUjUgsXHUwMDE2+EJcdTAwMDJpJeT4Sb6TxtZOY2vvYHf/5jh7lFx1MDAxN7lneX605GDV4K1vJzRX6ND2eekqQKk1Y4RcdTAwMDVcdTAwMTdcZsXM3dvAeimEnlx1MDAxYljRSkOCZe8nXHUwMDA1KyWnvdjYgEOYgGWSoULrRu+F1fPTs8v80VWrjodPS1x1MDAwZVYrXHUwMDAy0pbh4Yz3XHUwMDA2bS9WkVxyL1tV7SyHcYyT6VJeXHUwMDEyLtmvmlx1MDAxN1ZZliSHxPie9Wqqx0o6UbNqUFx1MDAxMlmnjFx1MDAxZmWd5MzVYSV/U9k938k837ab1y6iXHUwMDA0rL6V7ThztIKgXHUwMDAwpFJAXHUwMDA2vVbqJTFLclx1MDAwMTgvs6y1PMV5jlxcRyNcdTAwMDPHYZZX4GRUTD2+XHUwMDAyVstcdTAwMDDNNzWPXG6MjnHQvytX45xcdTAwMDWcS23l5cTQJOP9cVbVVm+fNr82M/fb59tfT7ZKZ/NMMlx1MDAwZb9hd9iX7354klx1MDAxMZO9bClYN1x1MDAxMlx1MDAxODF+TJj+mJc0x1xiUqZJmKFAWWG0L1xisveC81x1MDAwYlx1MDAwYpFGSlx1MDAxOFx1MDAwZZhcdTAwMDBcdTAwMDaLs07rd8hH8sszXHUwMDE4Y2jIXHUwMDAya1x1MDAwYqN7LkpJP+aGplx1MDAxYVH0XFz4mkmshaVe6E+WaEw3XHUwMDFh/YnG3Fx1MDAxNElFJWWSLJJcdTAwMDEhrFx1MDAxNuOHvMeZ6PK2urlXOz0pn12VVk4zN/nscjtmLFuBJasth6LaOWN7ef3SYoDs7CiUgFx1MDAxY/xOtVxc56VcdTAwMTY5xDEzgWGnT7BMKVx1MDAxZrfi8OSMXHUwMDEywnFIKzTH6U5cZjpmyidtOEpZbqlMxWryXHUwMDEylORo11x1MDAxYZRKqFx0XHUwMDAyiIddsV68P195PG89Xrebh1FUeWjN2Cmby9oy1tVWO4UgjZZcdTAwMWPi92BVkVx1MDAwZZBDXGb5Ld6FfiFasqVlUmhfTsS3xLy/1qB0zv1cdTAwMDRryyhZsI2w1lx1MDAxOFx1MDAwYuPzYS42L53UT7BfrFx1MDAxZcv1c7neLlx1MDAxZp4svVxcXHUwMDEz+Fx1MDAxNaPgWIU5xyFMb9LVSzU/dDZR5Dx+xfyoalx1MDAxOEjl2CfloE5rXHUwMDAwXHUwMDE4YoX8UiQgskpY9lx1MDAxNy1cckg1O1fOL2Waw+qUX0LtP5MuLEt4pZ2T/S9zQplcdTAwMWVBdVOJnqVcdTAwMTRoXHUwMDA1mHh9YZRor+7fXVx1MDAxY8r26flDTWy3rlx1MDAwZlx0sVx1MDAwMctcdTAwMWXlZVhjXHUwMDA2noPKVpmkYC+lN84z2lx1MDAwNb6K7UChXHUwMDE22s3Cu5yG6SZYs/ty5IKZboXWYeZi5XRtu1x1MDAwNNW7jWN+zc/H+9OTvH5ccjuPYUcloYbfsDvsd82yqIhcIpnpZm3/4S6bXHUwMDA2lNPG2lx02lWkPufl1U8yVT9ZXHUwMDFi0Kz00yyIbpI9VH415i39KH505uk90eRHWNu50+S1SF7B4pRcdTAwMTakXHUwMDEwx09LRe58y17WWpXN58+5q0+5i82tKImJsUyyyY4/kSSQVkiUvYkpPlx1MDAxMXSWt4DlmExN10dmetfBXGKNPnhcXLDnXHUwMDEwfn740l5dfb4o7aysRcdcdTAwMWKHub3bcHqj+WvYdzTsKIdk+Fxyl9AhUTpR6ZGViJMszE9/ysur8kSaynNcInCzUnkz8Ua01Vx1MDAwNMLJJc+4z9hcdTAwMWJZOO9+hP2eO+9cdTAwMWUokXfv0Fx1MDAxOHJajC+ZT1dP5d3jXG6gK2Tz+0dcctkw2fz7oN3rXHUwMDAwkP1cdTAwMDF2v1x1MDAxZItnf57SYEBWKOUkkmDJmF+eUmtcdTAwMTlcYoNgXHUwMDEwWVx1MDAxM0DM6YhcdTAwMTHvNV9iXHUwMDFjkTZcdTAwMWPokVx1MDAxOFJ+UKCs027BoYRcdTAwMDGn1Fx1MDAwNML7dty6WGewfthKUlx1MDAxYeI07pFd29a/NL9cdTAwMWWph3ZOZa5lTX6+XFy/uFh25r3HXHUwMDAwaWLAcrTvpO1cdTAwMDMsXHUwMDA0YKywXGZcdTAwMDGNen7VMq1cXKDQovJcdTAwMGLGKL6ONF7cdc5cdTAwMDFbXHUwMDEyy3hGUv1oRY7OeziSXHUwMDBiwapcdTAwMDJQM1slkkJcdTAwMGa1NpFcdTAwMWVcboJDPclcbmV8XHL7aaWZrZePi5vnj7uVtdrGp690t+QsXHUwMDA0NjGBdVx1MDAxNlx1MDAwMY10IJXp5TIri1x1MDAwMVx1MDAwZqJQa4NcdTAwMWNo4bRcXOZE4r0mzzpSgr84I4d0XHUwMDE2lFx1MDAxMFx1MDAxOKklq1/tXHUwMDFjuXjrk1dcdTAwMWFcdTAwMDKAdKhcdTAwMTesV1x1MDAxN4VVh8mpXHRcdTAwMTZRYmtcdTAwMDPjpybOro4q2Vx1MDAxM1co1Vx1MDAwYitcdTAwMDeZ3U27+1x1MDAxNZeed69cdTAwMDKPUCNAsONDivqwKlx1MDAwM9ZyymotSKrp8lx1MDAxMims+1x1MDAxOSCVpc5cdTAwMTH/XHUwMDE1uGD/fWFQdYlcdTAwMTU4VEJIXHUwMDAxMD7N8su5OVxcKa1cdTAwMWPU90pfi632Sq7cLN4tOVItXHUwMDA0WmhALaxfx1xyXHUwMDAzQFx1MDAwNb8kz1x1MDAxYlxcS1O2Yk4j3c9cdTAwMDCqRiqrLL2BKrM0QFx1MDAxZOGrJrZcdTAwMWSQmpBgolx1MDAxOOvgYCVcdTAwMTNlbr/uPt6b/U+nlXpNbL+xXHLpXCJJ9zbo8Pek0X49f1/HXCJCXHUwMDFikFGsblx1MDAxOcxg5Pz8VTRcdTAwMTSwl+GZhOBLwkNcdTAwMDCrPVx1MDAxMY1naDtsIVx1MDAxMFx1MDAwM/6qNJZxTKRcdTAwMTbMub+9P6jIkthcdTAwMTD7N7vV0+yXyu7Jp+5b6sHjJEnLmVx1MDAwZjsqaTn8ht1hX75cdTAwMWIlvHNPWpJJ5mVcdTAwMDJcdTAwMTkt0NnxbUz6Y17SrCWbj1S5VTogXCIgwYrMoJnjYlx1MDAxOVx1MDAxYSm38VYlL4LK8bFmV1xyXHUwMDE3767Hqkhv5PLLQFx1MDAxYcdxXHUwMDExsNUkNp+q56o0Mn9cdTAwMTh/zHNm84+wRlx1MDAwM2z+cIpcXKWRid1cdTAwMTaMXHUwMDA1/uBcdTAwMDTiWL86XHUwMDE3XHUwMDBlz8zuZeuomTvcKOWz0cZyu3ygfdHU8lx1MDAxN5IgXHUwMDA0qD5Z9JlcdTAwMThAJdnP8vHJVFx1MDAxZV9cbptcdTAwMWZcYtHPQrGDXHUwMDE505DxfI/hXHUwMDBiNMuqRf5ucNFcdTAwMWFcYt/01yyc6eCthptALlOhmsjmd4lcdTAwMGLAQLKDXHUwMDAziGp8mJrjzY2mOP1aKT2stHdcdTAwMTGuXHUwMDFmSup+6Vm/nZ1iiP/zvTbQ8S/0dlx1MDAwNnHCXHUwMDA1gFKC8ett3Vx1MDAxYzeUmFxym99cdTAwMGbgJP5cIv4uXHUwMDA38fdcdTAwMDex+V1ipYyjXG5jhJ2gJ8Xh5Vx1MDAxNT5cItjbp88n7UyYscfl+vrSyzVh4Fh/WVx1MDAwZehZtEH0eoNeqn3mRYBcdTAwMDZcdTAwMTNfXHUwMDA3O3suv7K+14+z7Fx1MDAxNzlcdTAwMTVPxCWS+c1ghVtpYJy8qS/FL6nunJsxnT/prXbOXHUwMDBlvM9cdMU6vVxmTsmdvKTfzcw5sFx1MDAxM3ScuXiWpcszudW42C192s/WcpWd99G7Vvt9kzjqY1x1MDAwZlxydN/CbemMX4PnwDdQ0qzyZuBhTkHLXHUwMDAz4djnXHUwMDA1WjAtb1/kLteal+p0o3L7eFx1MDAxODZcdTAwMGJPutTtstxcdTAwMDO5SbIwv4b9NeyoxNnwXHUwMDFidof9rlx1MDAwN2dofFJValwi209TYubMt4OUksZffJD+lJdWl7IvlKZLwVx1MDAxN8VnpUtnwvdcdTAwMTNcdTAwMWOzcyxml7xLV/fFv0++31xi12BcdTAwMDF8v+RilFG+i9dcdTAwMDR+XHUwMDBlbd5dXHUwMDE3zvbWqfglX4n29s/3dlx1MDAxM1uWLlx1MDAxN99cdTAwMGZE4FxmXGKjhe9MKmNdsTqRjPeE/Fx1MDAxZbdW+35mMD82rla+RYe0VnC8KmxM+mJ0P1xiXHUwMDFjz5M0XHREgYP1KIlcbv062Fx1MDAwNYuu8f06ZmVL0iuoyV3LJOtQ313WjV9B3dws2Mdy9sneX8rdtdXSefnpxC053Vx1MDAwZnyvZd+e1oKxMr4v6je8YmB8/zZcdTAwMGXBnW9cZjlHvFJcdTAwMDDI01CgXHUwMDFjuWHL6MFcdTAwMDZcdTAwMTZcdTAwMDU4w+FcdTAwMWNcdTAwMDdxclx1MDAxMK+ao1xiJFx0i6amWICZ4TWNmpKywMuT01x1MDAxY6hcdJrsrWx9Pmt8LbSL5nzncdvIjcuzXFz0XHUwMDE2tC6Qm4K+0Vwi61dcdTAwMDPot1x1MDAxMOjrXsp/f1x1MDAwMM5cbuf33o5b/1k32mV/XHUwMDA2pee/alx1MDAxYq9cdTAwMDHGqSmkpG88pFl76njm/rUntFwiYlxys3By6qKgSoksKraBrN7dXHUwMDA0u9qf4Vnt071dWa3uX1x1MDAxZdRcdTAwMWWbd4XjZpKnvixI1a7TXHUwMDE23PBLVtag6stoXHUwMDAyXHUwMDA2orPNXHUwMDE2P1x0RtN0O/KltdmdXHUwMDAxVFx02Vwiszgtmke1IKhKkbJTu2CsXHUwMDAyXHUwMDFim/FcdTAwMGJra7dnj2H2aKNw0FxcvTtcbqN2e+UmKVx1MDAwMb8sYLXsK7K2s8Yho9U521x1MDAwN1ZcYpRU7Eii9WCdikSV2md3XHUwMDA2YDVglVx1MDAxNvGC/ftcdTAwMDNrusuarFn5PVx1MDAwMitcdTAwMWRN41x1MDAwN1rR7v6FuSiXTu7yrvX5oZy5XFw3XHUwMDA3S0/665CHtGVb70j5/d178CqFsYHjqMY3aFx1MDAxMHK69oMjSH8qkMR4XHUwMDA08MyJYYtUtPRcdTAwMGK8wCm/XHUwMDE3sVSDi1RYu0jPXHUwMDFmgDm0hn45MTTJuHef39v7slx1MDAwZke3mYeHwu1D+2v1tjF97vK9XGY7KiU6/IbdYV++XHUwMDFipVx1MDAxM3BWOiFxa3KV2JBcdTAwMDVcdTAwMTjbXHUwMDFjSdP4XHUwMDBiK9Kf8pKmREHadG1gtd9Lxlx1MDAwN4ZcdTAwMWHQzrNcdTAwMTVcdTAwMWONVFx1MDAwN8O4hFx1MDAxY15r86adXHUwMDEyp7NYXHUwMDE4S5a/uS+wY1fW51x1MDAwZXhAXHUwMDE3V7UjuYSPXHUwMDBiJFx1MDAxM46wcoNkwsdp2IRp7F70q7xgXHUwMDAyPkflrPJ8eYqVK5cp5FpHn3dL9XJ5ud1JXHUwMDA2XHUwMDAzXHUwMDA3N563Yp3xLYJVv0D6XHUwMDEyhjWecKuJY/qp/MlcdTAwMTRCIYuWIa2VUsbK2HL0eEZJd1x1MDAxYTmBYdUh41x1MDAxYkl9zyhJQyB/SOwzs/XOibyj5Iy95qjUM9HHhmmzoj+3r0ru6GrroJJTh3hw/by29LSjXGaA8cuZlFx1MDAxM35xiO03XHUwMDFk6L03v+LL75dn58c7mlx0mZA63YTe5EdOyzqahPtcdTAwMWGbx0/NOvoxXFxCflx1MDAxZonuIN/MeUU3vlRHVysnbnPzZrN+9OX86Gz1dOP8aHXppdrIQHg2l187KGz/JvZcdTAwMWShdlxuXHRcdTAwMWRcdTAwMTA//DluxFwi/G5cdTAwMTVcdTAwMWOFalwi8LtFqGE2XGI5WGXH1SpU7Fx1MDAxYmhcdTAwMWOQa09qd7pni/Nfgv1cdTAwMDNcdTAwMDU7k/Ja/WfghU4o2iPK7GmbLfL9/Fx1MDAxMo2x5Xun0ag8rjdk5n5/7aZyer5xXHUwMDFh3W++jzK7XHRYvK1Fllx1MDAwZYfxXHUwMDFkKF9k3O/J6Jv+k1x1MDAxMobN99xk3PcpXHUwMDAxLYxfy2ZcdTAwMDTqYYxhwsDybKzwsZ52XHUwMDAzIaBf1sUgolx1MDAwNW9cdTAwMGVmJbpJXCLAt6PWqcSQXGL4Vfq93cavsl9X7E21JU736qtHxbtnVXu8v3xTgn2xcFx1MDAwNe3/Ut3pKY5cdTAwMDMmSTFcXK3SSrJTXHUwMDAwen6kLU9cdTAwMGLhgFx1MDAwNpTPXHUwMDEzW6GG0EJ8W1x1MDAxZOFcdTAwMTkqwneCwngg8lx1MDAxYVx1MDAxNfl9j1x1MDAwNIpcdTAwMDWnLCa1SKmATeus41x1MDAxMuvsfomCJ++MX1x1MDAwZrpoP118udtsVWBlz62V98rm4K603Fx1MDAwMTzrhYBcdTAwMTHg9SqrWCn7o1wi0lx1MDAwMVjR2fC4t0nYjNvqsL8qXHUwMDFjKE/NxyHrXHUwMDAxfTXIoNDaoensXHQ2WFxyQmfYu47npX4qoDqdXFxcdTAwMGLyPVx1MDAxNk18O4GRXHUwMDFkoGzt881u5ejE1Y+b+TD6VMmJJc808fNcdTAwMGV8Po3YzKPyPn0/Ulx1MDAxNYdcdTAwMDLWOWPQTFu4TGurMz1SwTdcdTAwMDNmQ/iTXCJVxnfY6ocqR8BW9qzaXHUwMDFkzbTbyVx1MDAxZEa3ollcXL9ccq9BrV/Jh+aSY9WpQGnyvo6RxJI7oFUx4JDUNyxjZ9HAdGo1rbHO1GBcdTAwMDVgpLr4XHUwMDFlQ+9cdTAwMGarI2ihKVxcZn5cIuArumOD9TjKStrP5chd01x1MDAxOYX526eVqkhcdTAwMDDrXHUwMDEy1dhd4DpcdTAwMTU1wSGnjL3rb5vag1xiUDFI+Jx05KZSrSNq7DqQ7GyA51OzN+KGRFhaXHUwMDA27KZ4rq5veyjt4EJr3+uCT8/DY305MbRsfV3BXHUwMDFjfcpcdTAwMWTa7fVjzK5uhZtccnszfTX8v3zYUbX74TfsXHUwMDBl+/LdwnRNYlx1MDAxZiCZzOf1IZiRQo+vZtJcdTAwMWbzslx1MDAxNu9BpatcdTAwMTlcdTAwMGWcXHUwMDE1XHUwMDA3rEJcdTAwMTjfXHUwMDFicY5qhkarmSHFe3ROk1Y/YvvQN+Vme1x1MDAxYlx1MDAwMVx0tvNGKfZcdTAwMDaI/1xi6rkqvXhcdTAwMWbFn/Scq/cj7OeQ6n00RfneQvJcdTAwMGVHXHUwMDE2pfKZj/EzrNnzfFttnn2l/f1wd0OYnZvPoV1uR1x1MDAxNYVcbsjvU+1cdTAwMTe/KG1s31xuQ41s+i1pY3zLrFx1MDAxZVx1MDAwZeaM+1x1MDAwMVx0XHUwMDAyo3zlnuLNe+OJKl+0l4o/woe6XHUwMDAzjqpB1Fx1MDAxNlx1MDAxMN5FUPXbi0X6mG82j1wiXHUwMDFlkk9/gy7Pulp8kdruMFx1MDAxZu+r4cPqsEC18/FcItBcdTAwMTFcdTAwMDBcdTAwMGay0M/5r79/+/s/t9XlXHUwMDAwIn0= events.Key(key=\"e\")events.Key(key=\"x\")events.Key(key=\"t\")Tevents.Key(key=\"x\")events.Key(key=\"t\")Teevents.Key(key=\"t\")TexText"},{"location":"guide/events/#default-behaviors","title":"Default behaviors","text":"

You may be familiar with Python's super function to call a function defined in a base class. You will not have to use this in event handlers as Textual will automatically call handler methods defined in a widget's base class(es).

For instance, let's say we are building the classic game of Pong and we have written a Paddle widget which extends Static. When a Key event arrives, Textual calls Paddle.on_key (to respond to Left and Right keys), then Static.on_key, and finally Widget.on_key.

"},{"location":"guide/events/#preventing-default-behaviors","title":"Preventing default behaviors","text":"

If you don't want this behavior you can call prevent_default() on the event object. This tells Textual not to call any more handlers on base classes.

Warning

You won't need prevent_default very often. Be sure to know what your base classes do before calling it, or you risk disabling some core features builtin to Textual.

"},{"location":"guide/events/#bubbling","title":"Bubbling","text":"

Messages have a bubble attribute. If this is set to True then events will be sent to a widget's parent after processing. Input events typically bubble so that a widget will have the opportunity to respond to input events if they aren't handled by their children.

The following diagram shows an (abbreviated) DOM for a UI with a container and two buttons. With the \"No\" button focused, it will receive the key event first.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1bbVPa2lx1MDAxNv7ur3C4X3pnarrfX85M54yioFSxVk+tPZ5xYlx1MDAxMiElJDRcdCDt9L/fXHUwMDE1UFx1MDAxMt5cdTAwMDIqcHBu80Eh2eysvfZ6nv2slZ2fW9vbhbjXclxuf2xcdTAwMTece8v0XFw7NLuFt8n5jlx1MDAxM0Zu4MMl0v9cdTAwMWVcdTAwMDXt0Oq3rMdxK/rj3bumXHUwMDE5Npy45ZmWY3TcqG16Udy23cCwguY7N3aa0Z/J36rZdN63gqZcdTAwMWSHRnqTXHUwMDFkx3bjIFx1MDAxY9zL8Zym48dcdTAwMTH0/jd8397+2f+bsc52zWbg2/3m/Vx1MDAwYql5nODxs9XA75uKOUKcMCT1sIVcdTAwMWLtw91ix4bLd2Cxk15JTlx1MDAxNXrRt6vuXHUwMDE34pu8yI/l/snt58reSXrbO9fzzuOeN3CEadXboZNejeIwaDiXrlx1MDAxZNeTu4+dXHUwMDFm/i5cbsBcdTAwMDfpr8KgXav7Tlx1MDAxNI38JmiZllx1MDAxYveSc1xiXHLPmn6t30d65j6ZIcVcciQ0JZprLDVVw6vJ77UhMeJcdTAwMTQrjSTmmo3bVVxmPJhcYrDrP6h/pJbdmlajXHUwMDA25vl22kZcdTAwMTFL48yYu4+jVdRgjFx1MDAxMlwiwVxmqlx1MDAxMeHDJnXHrdXjpFxyIYZCTCjJXHUwMDA3t8qY4vSnhGtFqVx1MDAxNul8JbdvXHUwMDFk2f3Q+GfcoXUzbD04rlx1MDAxMCVfMqYnVlx1MDAxZozHVTa2MpNOxFWl7Fx1MDAxZlQ+fyzvX1rfj3ul8/tw2NdIIMbOfVxcXHUwMDE4Xvj19ne3M7tcdTAwMWRp/XbRXHUwMDFiLmituL+6b1x1MDAxZvPDkPpnJdwrnXlfet3p1pphXHUwMDE4dDP9PnxKo6ndss1cdTAwMDEjYCEoSyiDS8SG1z3Xb8BFv+156bnAaqQkspUxeIK7RsafIS5G0fjZR+JcIohSXHUwMDA0WOApiOZcdTAwMTFX/vRtKnFplENcXFx1MDAwMlx1MDAxOVx1MDAwMlx1MDAxMyBcdTAwMGLBXHUwMDA2zPVcdTAwMTLiikPTj1pmXGJ8MIW85HzyXCJcdTAwMTNkRVx1MDAxMJFMJ3S2fLrKj05O5Vx1MDAxM6IzXHKCwI/P3Vx1MDAxZv2lUVx1MDAxOFx1MDAxYyuGiIBBaI24XHUwMDFjaVUym67XXHUwMDFimdd+XHUwMDE4g+W7rdab/2ZdXHUwMDFkOWDCYLlcdTAwMWRpvOu5tSTOXHUwMDBiXHUwMDE2XGbKXHRHIFx1MDAxMLsgXHUwMDA0hlxymq5te5lwtMBcdTAwMDJcdTAwMTP6XGaPXHUwMDE2WZOD0K25vuldjFx1MDAxOJhcdTAwMGLJXHUwMDAxJUzBpFwiszGJwfGYscyiNVx1MDAwZpP5JLWhmKRSXHUwMDFhhDIsZKJcdTAwMTVENjL6XHUwMDFkMGJwwCsgUilBJScrQyU1XHUwMDE0cCSTQlxugShXelxuKqk2XHUwMDE0xVopLjSGcJ5cdTAwMDAp5lxcwigofjpG+6Y+XHUwMDFio09bQTJ2mGG85/q269fgYrr0ParkRTDRR7HVTqzcXHUwMDAxhlx1MDAwNYBzjlxiU8BcdTAwMWNEZFx1MDAxYdXMVlx1MDAxMvRcdTAwMDZcdTAwMTeKSFx1MDAwNbHNlCZYPDRcdTAwMTiuwFx1MDAwNce355tUvPlcdTAwMDbS8NI8s8t2T/rt3a87++VZJjGsMdZIYyqJXHUwMDEyRLFcdKswg+mHmcNEXHUwMDEzglx1MDAwNPydMMszo7hcdTAwMTg0m25cZs7/XHUwMDE4uH487uS+N3dcdTAwMTO011x1MDAxZNNcdTAwMWW/XG7Dyl5cdTAwMWKnhVbS46h4TD9tp7Dpf1x1MDAxOX7+5+3U1juzozk5JuI47W8r+39cdTAwMTajhY5cdTAwMTVcdTAwMGbwPIXVXGK4eFx1MDAxNq1cdTAwMDGSXHRcdTAwMDaxsbjSyJ/nXHJlNaKUQSVcdTAwMTJIJ1x1MDAwYp5iZIzVNJBcdTAwMWWiSFx1MDAwM2q1TtbF1WlccpHOxZDGVKp8XHUwMDFleFx1MDAwYphVipHVZi2pkGp8iM5kpV5FOLBkLL+dWl9PXp5cXHjNXHUwMDFmn2/+wlx1MDAxZr2DYlxye3Xkur1ytJhcXM/t97LyudM9ZvsnxyG3yz3SJmxfLKFfcmlcdTAwMWZcdTAwMWSWXHUwMDFh1onaZfii6Z1cdTAwMWX4X2tL6HdF7n1d3TbE5VGn1PqEb9pRvdHhJXRXtf/vnPuCXGZ2vebOy+On33BBa0vlxoUon3+7u6yfdSp+3Tv+XHUwMDE27CzBXHUwMDBi91fki/zavjmpoHL5XHUwMDE2k2bN6Z0tqT7AJFx1MDAxMZCOrro+QIhW46dcdTAwMWZXbYq51ELRxXOR/LDY1FVb07xVXHUwMDFiUjJDrmnV5lNW7Uxq9LBqKyk4XHUwMDE4K+g6K1x1MDAwMkxohTR6QjxOr1xiLFpcdTAwMDEoPqbnb6795IJrv79OKvReULsuXFz704tcdTAwMDOZPHGkOOA5d6PR/6TSwFx1MDAxYy06Xlx1MDAxYZhr+fM1NkWKzkIrRlxcI0GzYTFcdTAwMGaunVx1MDAxYoJlLf7c2bPo0W3ZP2qflXv/Llxc+Ty0YkFcZqIxpDmSUsGoXHUwMDFhRSucMrhmSlx1MDAwMVx1MDAxMFx1MDAwNWFMrlx1MDAwZaxcdTAwMTkwpFx1MDAxMluMg5VgjDBkY5lq31o09sVheFOMnfaN3K1Wrq7qny5+XHUwMDFjfvitsZelsVfk3tfV7ao09uvywqo09uvygqf3iuq2XFxkRSGc84tcdTAwMGb3tVJpg72w/JRgXlx1MDAwNjN9IGm3XHUwMDBmn3JcdTAwMTSYwpqwpyiwXFyhMSsjoCSjcMc1XHUwMDA2wVRRgejiXHUwMDFhI3/+NlVjyHyNodelMdRcdTAwMTSNISc0htKYScroXG52NMxcdTAwMGJH/IRwfFlCsNeO48B/k5xcdTAwMWLo6uvClVx1MDAxM11cdTAwMTfeXHUwMDBlvnXM0DX9XHUwMDE4pHbUtixcdTAwMTjd7CxBjna+pCxhjphcdTAwMWXPXHUwMDEynjecXFxE56dcdTAwMGV49lNHjFx1MDAwNYQ8hPvimf7ljYjrxdZt1W+ffqrY9ztcdTAwMDeXZ/VNz/SpVlx1MDAwNoaoXHUwMDE1QkHSz4SczFx1MDAxZJCAbjjTjJOV7lx1MDAwNVgsecCISkQ5ZWtOXHUwMDFl0MfK4Un7qnL48axcdTAwMTL39lx1MDAwZXo8OPF/J1x1MDAwZstKXHUwMDFlVuTe19XtqpKH1+WFVSVcdTAwMGavy1x1MDAwYqtKXHUwMDFlXpdcdTAwMTde8DjhmTnJ9IGk3T58yntKwUEkp09cdTAwMTBWlpPgmTlcdFGCXHUwMDEzQcjie1x1MDAwYvKnb0O1XHUwMDBiQzRfu+i1aZdcdTAwMDWTXHUwMDEypSmiXHUwMDAyr0C6LDNcdTAwMWWXnpRUgylcIt5cdTAwMDG8huvOSOZo9Fx1MDAwNTKSeWPJXHUwMDA188z9j1jJmc9cdTAwMWMx0kJLxjPhPVx1MDAwZs75tLmhcKaKXHUwMDE5QiCtklx1MDAwMoLi2YpK/6Gjklx1MDAwNlx1MDAxNZJizYHeOFErrDFgXGaWXGJGXHUwMDE4p5RQwtkkulx1MDAwNTe45lopRGjyglx1MDAwN1x1MDAxZFx1MDAwNztnyXNi8ZyNRM/fXHUwMDAw+Vx1MDAwMrAvuFx1MDAwMXLh3YbIYJgqrFx1MDAwNeVIJvOVaTPYaUhcZlxmPoZlilxuLlx1MDAxNOTakztcclx1MDAxN9pcdTAwMDCZXHUwMDBm6lx1MDAxMZO4XHUwMDEwyVx1MDAwZUBcdTAwMDSrXHUwMDA1pJFcbk/YXHUwMDA0M4+0XHUwMDA0vIE9SMBcZuNcdJte0+7H2ZGcXHUwMDFjXHUwMDEzMZx2t5X9/1xmOpstTmBcdTAwMDKoktn8fVx1MDAxZZvlXHUwMDE3pjeVzSQxINDAXHUwMDEzXGYpxHXqj1x1MDAwMZkpQzCMOaeKXHUwMDBi/qJXw+ZQXHUwMDE5NVx1MDAxOMijxFx1MDAwNM4kKKUpVEZcZlx1MDAwMcJcdTAwMDR0iVJcdTAwMTIxltnv/Vh0oUpcYs1AvKyXzJgmXHUwMDE5sfQvktlcdTAwMGVQXHUwMDA3lVx1MDAwMCTMQEpKJsgkdYCjKbhZUVxuLCM0aM7n0Vl+1XTMqMQmXHUwMDA07UEkSEz0hFEw/UQgcCeiXHUwMDE45CdWr5rOdmaHc3JMXHUwMDA28lx1MDAxM1x0LbdcXCwzr7OOcVx1MDAxYeeYwaLCXHUwMDE3L1x1MDAxNvNcdTAwMWatfetL7XDvwlx1MDAxM7FC3S46vmttOqdcdEZcckWJgHDjXHUwMDEyKzZZK9ZUKFhQYGXNvlT2nPddmSWcO87YJKVcdTAwMTGR9pxcdTAwMTaKM1vQXHUwMDFlOEtCqqVhTp7xXGJoXHUwMDFlZ1xyw2pKyaLt69L9j6/V71cq+u53j3ixcVJ9eSWkKE87ptM+/G7+1Y0vT2k1qsYzXHUwMDFl+i6pXHUwMDEyMn0gabePiJrN36BcdTAwMDKUeMrjsFxccM6qhEiSkzrBIVx1MDAwNVx1MDAxNotcdTAwMDMzf/o2XHUwMDE2mCpcdTAwMGaYmsHStCRg5qpccsKnQJNMpEZcdTAwMThcdTAwMDNXcs7Uup/OPjFcdTAwMWPTWU9cdTAwMGIhmUeGI4WQdJCPhVx1MDAxMKeT2GR8cHpvXHUwMDFhTu/9deHiujDjXHUwMDA1Tj3y46W9wDlnkVx1MDAxOa92TDd4gMmtXHUwMDA3oFx1MDAxN8xW6zxcdTAwMDa/XHUwMDBlXHUwMDA1XGZMnWs/OCf1ZaHjOt29Kbx+1z+SXvs4T1x1MDAwMOUkXHUwMDEz9/PX1q//XHUwMDAxXHUwMDA3vCMgIn0= App()Container( id=\"dialog\")Button( \"Yes\", variant=\"success\")Button( \"No\", variant=\"error\")events.Key(key=\"T\")

After Textual calls Button.on_key the event bubbles to the button's parent and will call Container.on_key (if it exists).

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXG1T4spcdTAwMTL+7q+wOF/2Vq3ZeX85VVu3XHUwMDE01PXdVVddr6e2YohcdTAwMTBcdFx0Jlx1MDAwMcSt/e+ngyxJeFx0iODi3U1ZXHUwMDAymaHT09P9zNOdXHTfV1ZXXHUwMDBiUadhXHUwMDE3/l4t2Fx1MDAwZpbpOuXAbFx1MDAxN97H51t2XHUwMDEwOr5cdTAwMDdNpPs59JuB1e1ZjaJG+PeHXHUwMDBmdTOo2VHDNS3baDlh03TDqFl2fMPy61x1MDAxZpzIrof/jf9cdTAwMWaadftjw6+Xo8BILrJml53IXHUwMDBmnq5lu3bd9qJcdTAwMTCk/1x1MDAwZj6vrn7v/k9pV3bMuu+Vu927XHKJekKiwbOHvtdVVWpMJNOc9js4YVx0Llx1MDAxNtllaL1cdTAwMDWF7aQlPlVwS+dccr0vNre3qvdRdHrepHf1teSqt47rnkZcdTAwMWT3yVx1MDAwZaZVbVx1MDAwNnbSXHUwMDFhRoFfsy+cclSFdjxwvv+90Fx1MDAwN1x1MDAxMyTfXG78ZqXq2WGY+Y7fMC0n6sTnUDI+06t0ZSRnXHUwMDFl4lx1MDAxZVx1MDAxOFx1MDAxYlgozjiTQlLBkvF2es1cXCiKNSWSQC86oFnRd2EmQLO/UPdIdLsxrVpcdTAwMDVcdTAwMTT0yklcdTAwMWZFLI1To27/XHUwMDFjr6JcdTAwMDZjlFx1MDAxMEmJplx1MDAxYVx1MDAxMd7vUrWdSjWK+1x1MDAxMGIoxISS/OlSKSPZ3UlRQlDCiEh0jK/f2Cl3neOfQZtWzaDRs10hjD+kdI/V3lx1MDAxY/SstHel5t1sX3ok2tza31x1MDAxMZfhiSutVl3gvqyMK0b2Q1ToN/x4nyf24XBcdTAwMTe7Zpl9uSi192osujQv9uhosWZcdTAwMTD47WnlLkjd31xcbKb3+2kvmIjtvUuctNkom09Yg8GtXHUwMDE54kRwOPrtruPVoNFrum5yzrdqXHQ8raT0XHUwMDFkXHUwMDAyxYyeKUTkaixcIjKlkYbwTHSYhIj5Vl5aRFx1MDAxNLmIKIjBJII5QfLliFx1MDAxOFx1MDAwNaZcdTAwMTc2zFx1MDAwMHBmXHUwMDA0KsrJqEiGUJAqzVxilkLPXHUwMDFmXHUwMDA15+mdiVx1MDAxN/hedOo82l1ZXHUwMDA2x4ohXCJcdTAwMTCRWiMuM722zLrjdjJcdTAwMTPbdWPQfL3RePeftKVDXHUwMDFiVOjK5JnO665Tif28YMGg7CBcdTAwMTNcdTAwMDKRXHUwMDAzXGaj36HulMtuylx1MDAxZi3QwFx1MDAwNJnBzjSrvVx1MDAxZjhcdTAwMTXHM92zjIK5IfmE4iNiUlx1MDAwYjYuJqmG6eaYTc9S8peVJY1JgpBBXHUwMDE0xKRElFx0hjHLxCSh2kCEMM41XHUwMDA355dcXCwsJpGhpeSKgzaMyVx1MDAxMVx1MDAwMcm4QVx1MDAxNVx1MDAxMlxuaU0wk1SKwVx1MDAwMMVUUIlcdTAwMTVHM/CUrqazRiggXGKZJULDyFxmolxyxys7Xlx1MDAwNVx1MDAxYZN17yf5niZcIroxbDXDrlxyXHUwMDExo1xu0JMpXHUwMDAwUoKFZDzVrWI24oXIoFx1MDAxOMGUY1x1MDAwNeingIj3OvRcdTAwMTfggu2VJyvFOo9cdTAwMWN92l5v75OrWqd0XCK+XFw0ySil1kArXHJTg5HATFxizaRcdTAwMWVWXG5TQ1x1MDAxMVx1MDAwNViHOcFYa4yHtHLNMCr69bpcdTAwMTOB9Y99x4tcdTAwMDat3DXnelx1MDAxY+xV2yxcdTAwMGa2wqjSbYOo0IglZilp8m41XHSb7of++3/ej+492pnjY9iNXHUwMDEzYSvp13FoXHUwMDE22Fb0XHUwMDE0zCNcdTAwMTCNUDpcdTAwMTbSMIQvl1qpXHUwMDA0KiZhWv4kLymmQXppYIUhX0GUUKF1lmdcdTAwMTCtXGZCMSyGXHUwMDE4QStTfECzOfJcZpFgVFx1MDAxZseUXHUwMDFhwi2CtITVJoV6r5Jfbd7W6N3mbadxb53csk7rlG6ffVre/Opi97zV3melg/2Al7c7pElYScxBLrko73zaqllcdTAwMDdqneGzunu06V1V5iB3QeZ9W2Jr4mKntdU4wd+aYbXW4lvo9rD8x7hLLfaRblx1MDAxZu/fsiNyXiu5XHUwMDFi9p44q1x1MDAxZOzOo0Bycllj1YOz/a/0+HHnq/ulxYLSi+ROKlx1MDAwZYw2UFwi9ueCm0PuuEB44cVcdTAwMDFC2fhlWyhcdTAwMTmTXGKS5J2Tlu18v1jWZZuSvGWbXHUwMDAyRZSvtGzzXHUwMDExy3YqZe4v20RLyqRIxvtcblx1MDAwNVx1MDAwMaZcdTAwMTTCXHUwMDFhP8MjR1x1MDAxN1x1MDAwNKYtXHUwMDAwXHUwMDE0f2bn7669uMEpf7yOK/+uX7kuXFx7o2tcdTAwMDOcZOT0U3/Xvs36/7MqXHUwMDAzXHUwMDEz2OhgZWCi5rPTbHDG8fc3XGLTXGLy0OnD9ZN9ZNPHc+fh6/bxSXX7xrNcdTAwMWXto19cdTAwMWKufGK0XHUwMDFhXHUwMDE4XHUwMDExXHUwMDAx2CgohFx1MDAwMYCTyEQrXHUwMDEz3Fx1MDAxMMCyIVx1MDAxOedap1x1MDAwYlhzXHUwMDBmVo2Gg1VcctdcdTAwMDY0xZhS9dr3ME7XnMvS/tq3b0FJ31xir3VPXHUwMDAzbP3h2PPi2Fx1MDAwYjLv21x1MDAxMrsojv22rOBeXHUwMDE2vfXO1n2FXHUwMDE3afXIosXjqIh+PyvojaK62S6yolx1MDAxMPbp2d5DZWurvbxWWFSqMXd1J2VcdTAwMWGjL5iI7b2bJ6/LpS/jMlxySphcdTAwMWE8nTBcdTAwMTeluKY0YbqTmEu+mZeTuYgsc8lmXHUwMDE5TKLX4i1qXHUwMDA0b1x1MDAxOXFPQ1NcdTAwMWRXilOT8n+YZGw0o8j33sXnnrj6deGrXHUwMDFkXlx1MDAxN94/fWqZgWN6XHUwMDEx0PewaVkwuvGZh8xcbp9T5jGBoVx1MDAwZmZcdTAwMWWzXHInN54npCNCjlx1MDAwYmrOXHUwMDE44/g5NzLXLiufxVWntLPjXHUwMDFjuvbnzm7n7mHpq1x1MDAwN4RcbkNThFx1MDAxNCVcXGlAuYG4hnxcdTAwMDTBeYw4U9BXysVcdTAwMDX2dFx0XHSDMVx0qVx1MDAxN1A7yKWK+uiIq8+HzZ3L5teaf2Fe3W3JP+nIvNKRXHUwMDA1mfeNiV1QOvK2rLCodOSNWWFB6cjbssL8b3wsSN1JWc7oXHUwMDBiJmJ775Ygy+Fjs1x1MDAxY6KB4Ovn3E7JN/OyXHUwMDEyXCKGc1x0XHUwMDExJDqvRYimzHRcdTAwMDRcdTAwMTJUKM5cdTAwMTdQoV3qTOfQXHUwMDFmkVx1MDAxOdiAXHUwMDAzwWunOVx1MDAxM5j/XHUwMDE0ac6kseRG89h9mlx1MDAwNOHxt0fBwbHkWj5j93QuXHUwMDFjL2s8XHUwMDEzajBcdLFEhGSMZm+3UKVcZsA0QZiIN/nSXHUwMDA1XHUwMDA2M8aGXHUwMDEwglx1MDAxMcYpJYAtbDi2IdeK94tCZFx1MDAwMdBcIonpUKgrgCNGOZmhqDH7Rs1cdTAwMTeE+pRcdTAwMWI1p95cdTAwMTOJXGaGYfhgXHUwMDAyKjhHhKhUp6dcdTAwMWSRxMBgZULiXHUwMDFlQilGSa/HM/dp5sd0RicuXHUwMDA0LFx1MDAxNTzeXHRMJUpcdTAwMTC6r1x1MDAxM8w90lJoXHT6IFx1MDAwMXP8tndpjvfl+Fx1MDAxOPLiRNxK+vXZaIY104Onkz2aSkpYtNn0ezTzS+jLWYMlYHkpkFx1MDAwNk+iUjCW2lx1MDAxM/lcdTAwMDRn2oh3XG5cdTAwMGLEqKZcdTAwMDIrNqDYPPFcZmBVXHUwMDExrVx1MDAxMEw2k0IlO1x1MDAxN1x1MDAxMjwjhlx1MDAwMColMFdKXCLGdKoq3Fx1MDAwMzREXHUwMDAwk1x1MDAxNVEzlHNmXHUwMDA3NPjHuUx86Vx1MDAxN1x1MDAwMtpcdTAwMWGgXHUwMDA3zKRgmDGAdSZIqtNcdTAwMTN4gJ0pWFlRXG44I7RmM248z6/FXHUwMDBl6Fx1MDAxNKuEgFx1MDAxZmDALZxQ/tXUvnNcIrrPXHUwMDFjUayUxupNXHUwMDAz2tp4b46PYT9+JqTlXHUwMDE2oWFix6NcdTAwMWFnXHUwMDA0ol1PX4Qu31x1MDAxY1/c3947l6XPx7e6yuT5oW3+WlSjk1BccixvSLA6XHUwMDAx6ytcdTAwMDbeNLwnRlx1MDAwYlx1MDAwZcu6XHUwMDEyWCP2omdp/mKWsG85Y8OQRkSCpklcdTAwMDE6wbVcdTAwMWVmcZhcdTAwMTNccvrMsOl8XHUwMDEyZPXdakTNorTl7IR8XHUwMDE37Z5cdTAwMWNcdTAwMWY+3lhHlebGWvHlJZaiPGqZdvPTvfmlXHUwMDFkXVx1MDAxY9HD8DDam0OJZe7qTiqxjL7glNo6n3evLm4qdrBn1qnliK90s/FtOiv8XHUwMDA0gDz6XGZ/iWstqHQj5djKXHJGgoLLajZ9qpc/fctcbiMyXHUwMDE3RjQ3WFx1MDAxYUZcdTAwMTaX7KWraMmDscPpXHUwMDFjQLtcdTAwMDI6J39B5Wam5+5SlVx1MDAxYoIyZ/uVm2QoPys3divWydizO+9qdufjdeHsujDmyVid+fLcnoydsCZcdTAwMGWWZ0YrPPtcdTAwMDKvUyvWYE1cdTAwMTWmgrDnZC3ty9ZR5+BTZJ5vr31ztrfq7EBtLntcclx1MDAwNlJcdTAwMTFDQqIo4u0jXFxcdTAwMTA+UIVBkDJcbowhhWNAhNGLXHUwMDAy8+VcdTAwMGI8dIOo1LM8r/6SXHUwMDA13lx1MDAxNlx1MDAxYid3ev2uuVtad+W3dus4OKsv71x1MDAwMr8gdecudlx1MDAxMm9cdTAwMTh9wd+HNyg9dkc+iZ+Fxlx1MDAxMLfT3/LJn76lhSeRXHUwMDBiT5RcdTAwMWJoXvA0XHUwMDE33iBjXHUwMDEywyVdwHOvf3hDwlx1MDAxYiastXPgXHJja52pZ6NcdTAwMDb3pXFcdTAwMDSrXHUwMDEzXHUwMDE208dkPkg9KybJq8UkV8JAkGjHv66VpfFcdTAwMWNcdTAwMTkqrktROX6TqS3iavDsgSjhXHUwMDFhXHUwMDA0UTzqXHUwMDA3brgwQC/IXHUwMDFmXHUwMDA0RfG2XzlcXNVkQmtMkX7d2zRwSZz6XHUwMDE1g/lXNfN59Gr6llxiXHUwMDAxnGRIi/hX0SBcdTAwMTVcdTAwMWLxc1x1MDAxYVx1MDAxOEBVXHUwMDAzmGI4YmuyXodnVjXzY3RAJ+B1Or6nT1x1MDAwNJNsSCNhXHUwMDAwwnVnlOk4W6ZDXHUwMDFhvama5rBcdTAwMGZ3T1x1MDAwZrtvXCJpJf36bFwiMb6MyblShKR/X2ZcdTAwMTJmSeteXHUwMDFkXFzV/eKRf3/X1vqiendcdTAwMTUuP2ZJXHUwMDAz+Fx1MDAwM9aKgWdjNlx1MDAwMFxcSFx1MDAxYZpJQYVWWDGxuC3yPFlcdTAwMWJcdTAwMTJcdTAwMTYxjFI0vlx1MDAxZFx0gfeqv8slXHUwMDA00UrNhFJTsIjhfSM3zZub9Fx1MDAxMp+mXHIq03vavSCR31x1MDAxOMdcdTAwMTgyo1x1MDAxOKRcdTAwMDc9TZ5ia6VcdTAwMTe3XHUwMDA1s9E4jcBCfYiDSXDKvWEm8lxuLcdub4zIdm+7Ryy1XHUwMDFir3Fk2PFcdTAwMTR8/7Hy419iJnwqIn0= App()Container( id=\"dialog\")Button( \"Yes\", variant=\"success\")Button( \"No\", variant=\"error\")events.Key(key=\"T\")events.Key(key=\"T\")bubble

As before, the event bubbles to its parent (the App class).

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXGtT4lhcdTAwMTP+7q+w2C/zVlxy2XO/bNXWW4p3XHUwMDFkXHUwMDFkXHUwMDA1dZzXLStAhFxmgWBcdTAwMTK8zNb+97dcdTAwMGYqXHS3XHUwMDEwlTgwO1QpkFx1MDAxYzqdPt1Pnu7Tyd8rq6uF6KHrXHUwMDE0/lgtOPc123PrgX1X+Gi23zpB6PpcdTAwMWTYRfrfQ79cdTAwMTfU+iObUdRccv/4/fe2XHUwMDFktJyo69k1x7p1w57thVGv7vpWzW//7kZOO/yv+X9ot50/u367XHUwMDFlXHUwMDA1VnyQolN3Iz94PJbjOW2nXHUwMDEzhSD9f/B9dfXv/v+EdnXXbvuden94f0esXHUwMDFl13p066Hf6auKmWBMak3jXHUwMDExbrhcdTAwMDFHi5w67L5cdTAwMDaNnXiP2VRAZVxc/LZ3vr7+5UpUrtTa3XkobuPDXrueV45cdTAwMWW8R0PYtWYvcOK9YVx1MDAxNPgt59ytR01z9JHtg9+FPtgg/lXg91x1MDAxYc2OXHUwMDEzhkO/8bt2zY1cdTAwMWXMNoRcdTAwMDZb7U6jLyPeclx1MDAwZt+KmEhLIaQ5ZZxJitRgd19cdTAwMDDllmJKI0a1YFxu61x1MDAxMcVKvlx1MDAwNzNcdTAwMDGK/Yb6r1i1ql1rNUC/Tj1cdTAwMWWjSE3jxEnfPZ+uolx1MDAxNmOUXHUwMDEwSVx0WFx1MDAxY1x1MDAxMT5cdTAwMTjSdNxGMzJjXGJcdTAwMDE9mVCSP1x1MDAxZSphI6c/J1hJolx05yreY1x1MDAxNOju1vve8deoTZt20H2yXSE0X1x1MDAxMspcdTAwMWK9N0ddK+leiXnvKSEu9q/0fveb+vL15vtR8M0hXHUwMDAzWUO+XHUwMDE4OfdRYbDjn4+/xP4kYodGf8x6wKzaXHUwMDFlRrf35Vx1MDAwYreoP92cVnfOTlx1MDAxZZwvXHUwMDA3k7W1g8C/S8h9+lx1MDAxNPt+r1u3XHUwMDFmIVxmXHUwMDBiQVx1MDAxOeJEUarkYL/ndlqws9PzvHibX2vFqLeSUHhcZmyHzj+JtEhMRVrEXHUwMDE4w0igOOhnIW369C0u0pI0pFXCklx1MDAxY1x1MDAxM1x1MDAwMFn2ZqSNXHUwMDAyu1x1MDAxM3btXHUwMDAw4GtcdTAwMDLaytloS8bRXHUwMDE1SSFcYvxcIlZsbug6T/eMvcDvRGX3e9/FhMWxYohcYkTgmo64XHUwMDFjXHUwMDFhtWW3Xe9haGL7flxmmq91u1x1MDAxZv6TNHXogFxufZl8aPCa5zaMo1x1MDAxN2pwUk4wXHUwMDE0XHUwMDAzkVx1MDAwYtRlMKDt1utewlx1MDAxZmuggVxyMoPdLCzCXHUwMDBm3Ibbsb3KkIKpMfmIXHRcdTAwMTOCXHUwMDEyI6KmRaVcIkozJjDOXHUwMDFllKkotahBSYVFXGJFhHCFNNA9OVx1MDAxNJREUPBcdTAwMWNcdTAwMDAnJoFYYIJZflFpSawxVlxcXHRkXFyejFx1MDAwNyUjXHUwMDE2XHUwMDE1lFJcYlxcRVx1MDAxNUpQ0+dcdTAwMTiVmFx1MDAxMcpwwjczx2hf1dfG6Fx1MDAxMJq9IEbDyFx1MDAwZaJ1t1N3O1xy2Fx1MDAxOV/7nnl9lpjoR3GtZ7QsXCJcdTAwMGJcIlxcMiVcdTAwMTUgLONcdTAwMWMmViXGNeyusaOFsWCYXHUwMDFhsFVgcfI0YHBcdTAwMTUuOJ36bK22909Pd13S+7z9cNw624nOTsrlL5O0XHUwMDAypVx1MDAwMDypQDA/iiNcdTAwMDPwMeZcdTAwMGW00lx1MDAxNlKUXHUwMDBiialWiFA5ppRnh1HJb7fdXGKs/9l3O9GolfvmXFwz4d507ProXjip5L5RXFzoXHUwMDFhicNkN/60XHUwMDFhx03/y+DzX1x1MDAxZieOLk51Z/NcdTAwMWFz5FjcSvJ9XHUwMDFholx1MDAwNU4teoznXHSoRijlo5ufUY0gpYngmtLMsJY+y4tcbmuYUEsyXG54QJnJ31hsXHUwMDEyI4FiZmkuIaWD2Vx1MDAxMZrlSDZETPxcdTAwMDZApmKweEIuXHKhylx1MDAxMCM5kIs0Zu2fXHUwMDE3T2+6p9WOOjhu3n1cdTAwMGbsi1wiZ29PL7pF50SSXHLt7jKt9nuHLba9s5eNsKfKPd87u707YFx1MDAxYp9cdTAwMGVcdTAwMDJe334gPcI2xFx1MDAxY+SS8/ruzlar9kmtMVxcaXtHm52vjTnIzcm8yyW2Jc53b7e6J/iqXHUwMDE3Nlu3fFx1MDAwYl1cdTAwMWbW/3XGfUNcdTAwMGX7XHUwMDEzWSHqXu00Nq+0s1x1MDAxOVRxsX5cdTAwMWKG3l4wXHUwMDA3KyCnsvnJ82XjqHm/s394sulT725cdTAwMTGtO6tOMvmAsdhnejCVjDLNgcfH5Dmnelx1MDAwNrDsqSRcdTAwMDNcdTAwMGItXHT8yewkI93OXHUwMDBiSzKwSiVcdTAwMTlcdTAwMTRZ7H1IXHUwMDA2n0AyXHUwMDEySf5cdTAwMTPJgFx1MDAxY85cdTAwMTQxWDwv71DBeHRI/Fx1MDAwMoecXFzByFqxKD2XXHUwMDEzPlxcdsxcdTAwMGW3/uelWVx1MDAwM/H8xmXhsjO5mMHJkJxBrcJzrofd/0WljFx1MDAxOdR5tJQxU/PUSE3NXHQopmRauCpIXyVjQmaO1kP3XHUwMDA2bzU66/dcdTAwMDeoev39SFKvVKr/2GjlM4OVMG0pXHUwMDAyXHUwMDE5L1wiWFxuotVwsDJcdTAwMDU5XHUwMDE505JJzjWwcZFfsGo0XHUwMDFlrEqMXHUwMDA2K1NcdTAwMDIyYczeOSNwTnmVVuAlO8Xt1s3DWsA3rn5lXHUwMDA088pcYnIy73KJzSsjWC4r5JVcdTAwMTEsl1x1MDAxNTy9XlLV7Vx1MDAxMitcdOGUK/v3ja2teTD3nNTNK4FZlkmblb9MPmAs9unTXHUwMDBmz18oYdNbXySmlFx1MDAwYqqzM6J0Oy8sI2LpjEi+XHUwMDE3I1JcdTAwMTNcdTAwMTiRXHUwMDFjY0RKaES1UD91+rLei1wiv/PBbHvMXHUwMDAyLlx1MDAwYlx1MDAxN054Wfj4+O3WXHUwMDBlXFy7XHUwMDEzQWJcdTAwMTD2ajU4u+k5jVx1MDAxY1x1MDAxNj6nnGZcdTAwMDb3XHUwMDFmzWledzqpIT0j0Vx1MDAxMVPjmmjNMKTj2fss1Oej3fXjXHUwMDBi965q22F5u/LlU/PsfvHLXHUwMDEy1IJ4RVxcYoI1XHUwMDExmqmRuMaWwtysWULSQ4XKL64zZjpCaa6QeOe2tWtSo7h3Ujyon8pcdTAwMTLueC1ug9Bfmc6cMp2czLtcXGLzynSWy1xueWU6y2WFvDKd5bJCXis1y2KFWVx01ORcdTAwMDPGYp8+LUBcdTAwMDLFU1x1MDAxMijJJWGSZb93IN3Oi8q01FxmoiXei2hlS6BcdTAwMTjTjCgt/m1cdNShPyHhcFx1MDAwMF2C986eZiRcdTAwMTRcdTAwMTmyp1nnklx1MDAxYcxTO2FcdMJpy7mYMcp59rwpXHUwMDFk5Fx1MDAxNzWaibRcdTAwMTinlEqJXHUwMDA0I5QmXG5ccv1wRtzChGAlsemkppLnXHUwMDE3z1x1MDAxOFtCgFx1MDAxMkZcdTAwMWZcdTAwMDJQy8bDW3CLa66V6ZXUSGI6XHUwMDFh7Vx1MDAwMExCICXly6P99b2wL7/6JPSY1lx1MDAwYlx1MDAxYveRMkw5XHUwMDAxX6RiSmsrsTCYjVx1MDAxMDNCKMVoou8yS/fq0+DZnbCxTmBlXHUwMDBlXHQsolxmfFx1MDAwMsWoO9BcdCZcdTAwMTNpKbRcdTAwMDR9kIBJw9N0mlxmXHUwMDBmYzotUyPsdFc2rzEnjsWtJN9f3tqvp9d3KTiQubsxO56lV/1cdTAwMTdcdTAwMTXPqDbBgFx1MDAxOdicXHUwMDEygHBcdTAwMWRfQ1x1MDAxZvFMWFRcdTAwMGJcYiNNNVx1MDAxNXnecIOpXHUwMDA1XGZIK1x1MDAwNJPNpFBcdTAwMTN6+1x1MDAwNbGEpkhcdTAwMDBfUlx1MDAxMoGyXHR0fWIvXFxcIlx1MDAwZVx1MDAwMPOKXHUwMDA18Vx1MDAwNcWzXCKAXHUwMDA3NVxyOTA5nEgmkmD1iFx1MDAxZGA4XG5mU2ZBQlxinWgxylx0z4xORiWzJoBcdTAwMDG2cNySXHUwMDFjXHUwMDAzXHUwMDFhtYhAYFx1MDAxZkSxUlx1MDAxYatpOk0uXHUwMDE2LzWeXHUwMDE1pzuzeY278Vx1MDAwYlx1MDAxMS21uq3o9LtcYvshTjDJjmqVXHUwMDA3t1k+uHPuSifOt/Jaw1x0i9/2fyyq0VmgRihcdTAwMWVes1x1MDAxYcm4jPmxJNLgXHUwMDE5SvY0veZ2bVZcdTAwMTPONWdsXHUwMDFj0UhcIpmLK9sxTDzfj4SVuWmKklx1MDAxY+5HXHUwMDFh+NWEqkXt+vz4QNb3mq1cdTAwMWLvXHUwMDBivjj8fmJvbb29XHUwMDE4UpJHt7bT27mxT++i8yN6XHUwMDE4XHUwMDFlRvuTxb6odpOTunNcdTAwMTc7q3Yz+YCx2GdcdTAwMDBIudpcdTAwMDDyJlx1MDAxNk1yqt1IOb10g1x1MDAwMdZcdTAwMTBcdTAwMDeyllx1MDAxOUbSzbyoMFwiUmBcdTAwMDRi1sJcdFx1MDAxOHlcdTAwMTOKpDIjwifgXGJcdTAwMTlL5bBkXFzBgX5A4eZV1CdRuCFoaOugcFx1MDAxM5/Kc+HGuTU6WfvOw4eW8/DnZaFyWZhy67FcdTAwMWX68dxuPZ5xQVx1MDAxY63OTFb49Vd3nUjzx8KSYE2xTjzaY1ZYejdl9nnz/qHZ3tpu672d43DXX1/wsFx1MDAwNPSxuGKacHPNXHUwMDE0hI801Fx1MDAxM2KZZmVI/Vx1MDAxOXBmJN90N/LbL+8ms9LoVVx1MDAwYtdvubpfXHR0UdtZP946p3vVs/2Tln/8eWNxr+45qbssYmeRhslcdTAwMDfMqG2vWj/fYdXKzV6RrYn2UbVabspsc5aBjEhNaO5PRlE65W5lSLA4ZSp7TpM+fYuKejxcdTAwMTX1OLbk3FBvXHUwMDFldISYslx1MDAwMEf8XZ+EXHUwMDAy7kg5euuTUJaJjsy4gudNR8wjiaZGJqZKcYNcdTAwMGaZI3N9jbZcdTAwMTTfaWzfn5XOfMlC7yxoLXpcclx1MDAxNZtcdTAwMWGpXHUwMDEwXHUwMDFjklx1MDAwMY20XHUwMDEymlxyhSbm3JJcXHJwTZO64eRcIsyPICRcXDNO2KtcdTAwMTZ430JIPt/qYD3c6V03v29ffN3arGzc3PdcdTAwMTaXkOSk7nKJ3f3aq5xViN495Vx1MDAxN2frd07P35XeXCJcdTAwMWF3XHUwMDE2f5p8wFx1MDAxZs+fKFVEXHUwMDEynHsxXHUwMDA3J59cdTAwMDQ5XG7TXHUwMDE0XHUwMDAwQSRJ3CyUTp++RUVpjFNRWnGLzFxypedSz4G0kSMk87i5c55cdTAwMGW57Fxmalx1MDAwNueYXHUwMDAzg0p5ttz09WdJXHRcdTAwMTOcveDRcqkw9aKoJO9cdTAwMTaV5vZcItPywKnCiGAx0lx1MDAxZEeRtFx1MDAwNMdSIVxykavV9NuLXHUwMDFjIeVbgtI8fkxRhDnlXGaSK8YxmfC4XHUwMDA0XHUwMDAxWVx1MDAxNoP9XHUwMDE0eC2nVNCx9jmTnUlcdTAwMDRcdTAwMTDznlx1MDAxZDVPUfuq/rmMT5dLzzNWh5/jXHUwMDA2zF9rJMCI2DzCLNHN8bw4zCxBuVx1MDAxNppIhVx1MDAxOWj+3NPxwqfLpcfu6tCKNaJEKTgq4qbpR3MxrpayXHUwMDAwdDVkrlxcKUSVYGNaLdUqdJpP91x1MDAwN4y7cyxzJfn+Yr6RuJyNXHUwMDAwXHUwMDFiU5pcIk1Zdrpx4GyfXpVON4vnVcq/ke1cdTAwMTLZrp0sPLBJYVx1MDAxMbP2jzhcdTAwMDZGQYZxjUjz5DlOzJ2KXG6ihOXXJpgowMRkY6xvxviAIFi9a7VcdTAwMDbYXHUwMDE45TSvxaPxrt9qr1pNXHUwMDEygSS5UEOjs3byRn53XHUwMDFhr1x1MDAxODqLUVx1MDAxMvGkyWNsrTxFcMHudstcdTAwMTFYaFx1MDAwMHgwXHRu/ek0Y3mFW9e5W59QXHUwMDFhuO6/jNR+vJrIcMxcdTAwMTT8/c/KP/9cdTAwMDeoXHUwMDAzMlx1MDAwNiJ9 App()Container( id=\"dialog\")Button( \"Yes\", variant=\"success\")Button( \"No\", variant=\"error\")events.Key(key=\"T\")events.Key(key=\"T\")events.Key(key=\"T\")bubble

The App class is always the root of the DOM, so there is nowhere for the event to bubble to.

"},{"location":"guide/events/#stopping-bubbling","title":"Stopping bubbling","text":"

Event handlers may stop this bubble behavior by calling the stop() method on the event or message. You might want to do this if a widget has responded to the event in an authoritative way. For instance when a text input widget responds to a key event it stops the bubbling so that the key doesn't also invoke a key binding.

"},{"location":"guide/events/#custom-messages","title":"Custom messages","text":"

You can create custom messages for your application that may be used in the same way as events (recall that events are simply messages reserved for use by Textual).

The most common reason to do this is if you are building a custom widget and you need to inform a parent widget about a state change.

Let's look at an example which defines a custom message. The following example creates color buttons which\u2014when clicked\u2014send a custom message.

custom01.pyOutput custom01.py
from textual.app import App, ComposeResult\nfrom textual.color import Color\nfrom textual.message import Message\nfrom textual.widgets import Static\nclass ColorButton(Static):\n\"\"\"A color button.\"\"\"\nclass Selected(Message):\n\"\"\"Color selected message.\"\"\"\ndef __init__(self, color: Color) -> None:\nself.color = color\nsuper().__init__()\ndef __init__(self, color: Color) -> None:\nself.color = color\nsuper().__init__()\ndef on_mount(self) -> None:\nself.styles.margin = (1, 2)\nself.styles.content_align = (\"center\", \"middle\")\nself.styles.background = Color.parse(\"#ffffff33\")\nself.styles.border = (\"tall\", self.color)\ndef on_click(self) -> None:\n# The post_message method sends an event to be handled in the DOM\nself.post_message(self.Selected(self.color))\ndef render(self) -> str:\nreturn str(self.color)\nclass ColorApp(App):\ndef compose(self) -> ComposeResult:\nyield ColorButton(Color.parse(\"#008080\"))\nyield ColorButton(Color.parse(\"#808000\"))\nyield ColorButton(Color.parse(\"#E9967A\"))\nyield ColorButton(Color.parse(\"#121212\"))\ndef on_color_button_selected(self, message: ColorButton.Selected) -> None:\nself.screen.styles.animate(\"background\", message.color, duration=0.5)\nif __name__ == \"__main__\":\napp = ColorApp()\napp.run()\n

ColorApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aColor(0,\u00a0128,\u00a0128)\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aColor(128,\u00a0128,\u00a00)\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aColor(233,\u00a0150,\u00a0122)\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aColor(18,\u00a018,\u00a018)\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

Note the custom message class which extends Message. The constructor stores a color object which handler methods will be able to inspect.

The message class is defined within the widget class itself. This is not strictly required but recommended, for these reasons:

  • It reduces the amount of imports. If you import ColorButton, you have access to the message class via ColorButton.Selected.
  • It creates a namespace for the handler. So rather than on_selected, the handler name becomes on_color_button_selected. This makes it less likely that your chosen name will clash with another message.
"},{"location":"guide/events/#sending-messages","title":"Sending messages","text":"

To send a message call the post_message() method. This will place a message on the widget's message queue and run any message handlers.

It is common for widgets to send messages to themselves, and allow them to bubble. This is so a base class has an opportunity to handle the message. We do this in the example above, which means a subclass could add a on_color_button_selected if it wanted to handle the message itself.

"},{"location":"guide/events/#preventing-messages","title":"Preventing messages","text":"

You can temporarily disable posting of messages of a particular type by calling prevent, which returns a context manager (used with Python's with keyword). This is typically used when updating data in a child widget and you don't want to receive notifications that something has changed.

The following example will play the terminal bell as you type. It does this by handling Input.Changed and calling bell(). There is a Clear button which sets the input's value to an empty string. This would normally also result in a Input.Changed event being sent (and the bell playing). Since we don't want the button to make a sound, the assignment to value is wrapped within a prevent context manager.

Tip

In reality, playing the terminal bell as you type would be very irritating -- we don't recommend it!

prevent.pyOutput prevent.py
from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.widgets import Button, Input\nclass PreventApp(App):\n\"\"\"Demonstrates `prevent` context manager.\"\"\"\ndef compose(self) -> ComposeResult:\nyield Input()\nyield Button(\"Clear\", id=\"clear\")\ndef on_button_pressed(self) -> None:\n\"\"\"Clear the text input.\"\"\"\ninput = self.query_one(Input)\nwith input.prevent(Input.Changed):  # (1)!\ninput.value = \"\"\ndef on_input_changed(self) -> None:\n\"\"\"Called as the user types.\"\"\"\nself.bell()  # (2)!\nif __name__ == \"__main__\":\napp = PreventApp()\napp.run()\n
  1. Clear the input without sending an Input.Changed event.
  2. Plays the terminal sound when typing.

PreventApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Clear\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

"},{"location":"guide/events/#message-handlers","title":"Message handlers","text":"

Most of the logic in a Textual app will be written in message handlers. Let's explore handlers in more detail.

"},{"location":"guide/events/#handler-naming","title":"Handler naming","text":"

Textual uses the following scheme to map messages classes on to a Python method.

  • Start with \"on_\".
  • Add the message's namespace (if any) converted from CamelCase to snake_case plus an underscore \"_\".
  • Add the name of the class converted from CamelCase to snake_case.
eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nNVaa0/byFx1MDAxYf7eX1x1MDAxMeV8OStcdTAwMTV37pdKq1x1MDAxNbC0hZSKXHUwMDE2SludripjT1x1MDAxMlx1MDAxN8c29oTLVvz3845cdLGdXHUwMDFiJFx1MDAwNJaNXHUwMDA0iWcm9ut3nud5L86vXHUwMDE3rVbbXmWm/brVNpeBXHUwMDFmR2HuX7RfuvFzk1x1MDAxN1GawFx1MDAxNCmPi3SYXHUwMDA35cq+tVnx+tWrgZ+fXHUwMDFhm8V+YLzzqFx1MDAxOPpxYYdhlHpBOnhcdTAwMTVZMyj+cP8/+Fx1MDAwM/N7llx1MDAwZUKbe9VFNkxcdTAwMTjZNL+5lonNwCS2gLP/XHUwMDBmjlutX+X/mnW5XHSsn/RiU36hnKpcZqSMT45+SJPSWEyEIEghQcYrouJPuJ41IUx3wWZTzbih9uHRWXbGf36Sxu/wY3HOWYfH1WW7UVx1MDAxY1x1MDAxZtqruDSrSOFuqrnC5ump+Vx1MDAxMoW27649MT7+VuhcdTAwMTd9U/tanlx1MDAwZXv9xFx1MDAxNO7+0Xg0zfwgslfuRKhcdTAwMWG9cUJ93WXpXHUwMDAx5mkqJEOSY0mVqlx1MDAxY+JOQFx1MDAxNfVcdTAwMTRiWlx1MDAxMy2xkpLwXHTTttNcdTAwMTj2XHUwMDAyTPtcdTAwMGYqX5VtJ35w2lx1MDAwM1x1MDAwM5OwWtP1OeHg2GrVxeiWufaI1FIqxqjQRHAxXtI3Ua9vYVxymMqphlx1MDAxZKlcdTAwMTlhyt2QREmMqVx1MDAxYY+7XHUwMDBiZ7thiYu/Jr3Z9/Ns5LR2aWDNaHe4U1x1MDAwM1X15WFcdTAwMTb6N3uPhaCMYiYwl3Q8XHUwMDFmR8kpTCbDOK7G0uC0gks5ev1yXHUwMDA1nHIh5uGUaoKpVETcXHUwMDFiprvKbH3K2LtT8/VL9mZjiFx1MDAwZnW8/8xhypDwsFx1MDAwMjcgXHUwMDAxb0hKOVx1MDAwMVPmwYZIqplmgmPxIJRicqKUmIVSgpCHXHUwMDA1lkpywrRAWC2DUsw1sEgyjNaP09FEXHUwMDA1rNqGZ2pPXHUwMDA1vv3z8it786M3/FZcdTAwMWO9XHUwMDFkRuNzNVDo53l60Vx1MDAxZc9cXI8+zWdcdTAwMDHilMHNPlx0XHUwMDBiXHUwMDE0V/NYoCRBVFx1MDAwYszuzYKtg86eke92XHUwMDBlXHUwMDA2W+Kk0z00XHUwMDE355vZM2eBQMqTXHUwMDFjIaEoXHUwMDA1KFwiPEVcdTAwMDLFXGIjXHUwMDFhlNyJwsNYwFx1MDAwMmG6fFx1MDAxNlx1MDAwYjDhXHUwMDFlgFx1MDAxOPM6xu+Bf65cdTAwMTQhgj6GTC+C//bnz2FHXuxcdTAwMDdcdTAwMWb3tnl+2T/svDtGa4M/k6qGulx1MDAwN8Lfmks7XHUwMDBi+Vx1MDAxOOm5XHUwMDAxXHUwMDAwXHUwMDEyXHUwMDE1XGKcXGYtgX1fXHUwMDFl51x1MDAwN/tZxFx1MDAwZtOf4uJIbnx7n+C1Yn/iW3Xo45WgT1x1MDAxMfVcdTAwMTjSWiHIWIRqyj+XIMtcdTAwMWMxqTlnXHUwMDFhXHUwMDA06bGAr+g03jmaxLnETElIY8TyOC/cwYo4XHUwMDE3Nk63szOqTj7GfPPqfNOc7H5dXHUwMDEzzlx0oYIqslx1MDAwNM4rNKWJPYz+NmX4bIy+8Vx1MDAwN1F81YBESVx1MDAwMDBw3z81Rcv2TWtgbD9ccr8nPnwqXG6/Z1p9P1x0Y5PXN7EwYJC7XHUwMDAyo41TbcZRz/GnXHUwMDFkm26TWDaCemI8bdOa01x1MDAwMzDNh9Plu+HkLaZ51ItcdTAwMTI/PlrCzJVcYi/lXFy+Q6AjIFx1MDAwN7yWRdzF9zefd97KLOb9y1x1MDAxZPb+7eCg8377p37efGeMe1x1MDAwNFx1MDAwM6khmDHBVDPUQcrhQSjhXHUwMDA0SSYg4j1apNOVqC4gPKFgiUtcdTAwMGKflvCPmddcdTAwMTFcdTAwMDI7oKpcdTAwMWJ6dMKPWJNAzV9cdTAwMDBOzPfkv+nQmrxcdTAwMTXEflH8tlx1MDAxNNtcdTAwMDPwXl0g1sf3u6xcXInsXHUwMDAyycnRMdlcdTAwMTXVUNjo+3M97n56XHUwMDFmn3X2985+fOufpztcdTAwMWaO9o7X24RYO9dcdTAwMDUlnlx1MDAwMFx1MDAxYbuyTihBmlxc54J6RFx1MDAxMq2g2Fx1MDAwNtFTXHUwMDBm60As4DqrrruA61hLXHJ/XHUwMDFjVcHwScj+mFksRHdcdTAwMDVC+2Rkd429Vtr9ntzGypI9z4Pic2xbSOxcdTAwMWKHz6pYiZ5cdTAwMWO9ZTaTpKyH7t9eXFyc3y3BbDKJ0Vx1MDAxNZkt70zahfa05sQ1ypieiOGcS09Csq6ohDBcdTAwMGbEnsvrUDOFuqtXq641XHUwMDA0nlaUKC2FnNFZxIR6XHUwMDFhMSpcdTAwMTVcdTAwMThcdTAwMDJcdTAwMDU0q1x1MDAwNPm2duVQ5nGxXHUwMDAy6VfvMN4k3ct0XHUwMDE4a3b4ud2KkjBKejBZ6cltx3z3XHUwMDFlhWBJ5GDorNxAXHUwMDFlRYxzjClcdTAwMTJQ84Jtsras52cjT3NcdTAwMDFcdTAwMWJcdTAwMGVBS1x1MDAxM4bxaMH12CyThHdcdTAwMWLV/Vx1MDAxNvTzqyP68biTXHUwMDExxTa2gy8mnmVcdTAwMTTyXHUwMDE0XHUwMDE4xFx1MDAxMFx1MDAxNH1cdTAwMTIkWVA9bVx1MDAxMvdcdTAwMTBsLFx1MDAxM6497HrYesomoLfdTlx1MDAwN4PIgu9cdTAwMGbSKLGTPi6duek43jf+lIDAPdXnJsUgc2dsqnv1qVVcdTAwMTGmPFx1MDAxOH/+6+XM1fOx7F5cdTAwMWLTMK5O+KL+vrSQQVx1MDAxMEeTw5WSXHTYdCnv339YnLg+RyXjWHuuv4ggzVx1MDAwN6+z6sJlOaKQx6FcIlx1MDAwM4oghvWChyQ80FxmhatKXHUwMDE5XHUwMDA1XHUwMDFiXHUwMDE4p5hcbsY10bXYUXXfqMcoXHUwMDE0RIpgxFx1MDAxMVx1MDAxMzVVXHUwMDFk5S+YKIFcdTAwMTnI8dNKXHUwMDE5g1x1MDAwZlUwXFy/lC2ucZtS5kpoTDjDXG70SkheY9FINzQkpJhqXHUwMDA0rlx1MDAwNDditpqSLX7S0rRcdEmCXHUwMDA1lLRKYoqRXHUwMDE0fMomXHUwMDA1abBEXHUwMDAyYVxydkGWPG3Uv0nKNuaCuZydwvHalIyrudVcdTAwMTZWIJ5Y1blxl5QtTsv/XHUwMDAxKVN3VltaeKrsW0OgRnDHXHUwMDEzWZlcdTAwMDAoUijFXHUwMDFj7sX8xlxubFs3kKsqXHUwMDE5cTVcdTAwMWRkN1x1MDAxYVx1MDAwNFWDINVcdTAwMTKuKinDwnNSi7iCilx1MDAwYik5lZRp95iB1DvfT5OVrVos3VPKXHUwMDE2l/CNXHUwMDA0XGLCsqZcXFNcbuVcdTAwMDSkZOCQXHUwMDFhjUa6IT1QXGbwn1x1MDAwMGAzwlx1MDAxOF1NzFx1MDAxNj8wa1olmGBaXHUwMDBipIWSkFxyztJXXHL7XHUwMDBmlTSDRIVCMf3v1rL5cC6np5G8pJrN61x1MDAxY1x1MDAxMTz3iSiBbFx1MDAwNMKJXFyidbQ48W5qWd9cdTAwMGb6w9zMU7N1NY/0nSUmV56mXHUwMDAwJii1QdpcdTAwMTVrylx1MDAxOVXSQ1gypLSm5IE/XGawuZ9cdTAwMTSZn1x1MDAwMyVm5GZcdTAwMTJcIlx1MDAxNilcdTAwMDFfnmlGbkaxJ1x1MDAxNSbAjNGrZs2oyoRbgJJ4lVx1MDAxZlxiPNcnR5hJcEu1tyv2loTHXHUwMDE0XHUwMDE0Nje+hZdsrFx1MDAxYfeamt1cImdwmvxcYtxcdTAwMDb+OFx1MDAxOVpcdTAwMGJcdTAwMDdcdTAwMDVQILCmkYOPu02Ez92hp3icNNfWXHUwMDE3t74u/dz2s+zQgpfHalxyMInCkauqK7TPI3OxNetnWOXLnbVcdTAwMTRcdTAwMWPHbONA8uv6xfX/XHUwMDAx2ibQXHUwMDAzIn0= Makes the methoda message handlerMessage namespace(outer class)Name ofmessage classon_color_button_selected

Messages have a namespace if they are defined as a child class of a Widget. The namespace is the name of the parent class. For instance, the builtin Input class defines it's Changed message as follow:

class Input(Widget):\n...\nclass Changed(Message):\n\"\"\"Posted when the value changes.\"\"\"\n...\n

Because Changed is a child class of Input, its namespace will be \"input\" (and the handler name will be on_input_changed). This allows you to have similarly named events, without clashing event handler names.

Tip

If you are ever in doubt about what the handler name should be for a given event, print the handler_name class variable for your event class.

Here's how you would check the handler name for the Input.Changed event:

>>> from textual.widgets import Input\n>>> Input.Changed.handler_name\n'on_input_changed'\n
"},{"location":"guide/events/#on-decorator","title":"On decorator","text":"

In addition to the naming convention, message handlers may be created with the on decorator, which turns a method into a handler for the given message or event.

For instance, the two methods declared below are equivalent:

@on(Button.Pressed)\ndef handle_button_pressed(self):\n...\ndef on_button_pressed(self):\n...\n

While this allows you to name your method handlers anything you want, the main advantage of the decorator approach over the naming convention is that you can specify which widget(s) you want to handle messages for.

Let's first explore where this can be useful. In the following example we have three buttons, each of which does something different; one plays the bell, one toggles dark mode, and the other quits the app.

on_decorator01.pyOutput on_decorator01.py
from textual import on\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Button\nclass OnDecoratorApp(App):\nCSS_PATH = \"on_decorator.tcss\"\ndef compose(self) -> ComposeResult:\n\"\"\"Three buttons.\"\"\"\nyield Button(\"Bell\", id=\"bell\")\nyield Button(\"Toggle dark\", classes=\"toggle dark\")\nyield Button(\"Quit\", id=\"quit\")\ndef on_button_pressed(self, event: Button.Pressed) -> None:  # (1)!\n\"\"\"Handle all button pressed events.\"\"\"\nif event.button.id == \"bell\":\nself.bell()\nelif event.button.has_class(\"toggle\", \"dark\"):\nself.dark = not self.dark\nelif event.button.id == \"quit\":\nself.exit()\nif __name__ == \"__main__\":\napp = OnDecoratorApp()\napp.run()\n
  1. The message handler is called when any button is pressed

OnDecoratorApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Bell\u00a0\u00a0Toggle\u00a0dark\u00a0\u00a0Quit\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

Note how the message handler has a chained if statement to match the action to the button. While this works just fine, it can be a little hard to follow when the number of buttons grows.

The on decorator takes a CSS selector in addition to the event type which will be used to select which controls the handler should work with. We can use this to write a handler per control rather than manage them all in a single handler.

The following example uses the decorator approach to write individual message handlers for each of the three buttons:

on_decorator02.pyOutput on_decorator02.py
from textual import on\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Button\nclass OnDecoratorApp(App):\nCSS_PATH = \"on_decorator.tcss\"\ndef compose(self) -> ComposeResult:\n\"\"\"Three buttons.\"\"\"\nyield Button(\"Bell\", id=\"bell\")\nyield Button(\"Toggle dark\", classes=\"toggle dark\")\nyield Button(\"Quit\", id=\"quit\")\n@on(Button.Pressed, \"#bell\")  # (1)!\ndef play_bell(self):\n\"\"\"Called when the bell button is pressed.\"\"\"\nself.bell()\n@on(Button.Pressed, \".toggle.dark\")  # (2)!\ndef toggle_dark(self):\n\"\"\"Called when the 'toggle dark' button is pressed.\"\"\"\nself.dark = not self.dark\n@on(Button.Pressed, \"#quit\")  # (3)!\ndef quit(self):\n\"\"\"Called when the quit button is pressed.\"\"\"\nself.exit()\nif __name__ == \"__main__\":\napp = OnDecoratorApp()\napp.run()\n
  1. Matches the button with an id of \"bell\" (note the # to match the id)
  2. Matches the button with class names \"toggle\" and \"dark\"
  3. Matches the button with an id of \"quit\"

OnDecoratorApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Bell\u00a0\u00a0Toggle\u00a0dark\u00a0\u00a0Quit\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

While there are a few more lines of code, it is clearer what will happen when you click any given button.

Note that the decorator requires that the message class has a control attribute which should be the widget associated with the message. Messages from builtin controls will have this attribute, but you may need to add control to any custom messages you write.

Note

If multiple decorated handlers match the message, then they will all be called in the order they are defined.

The naming convention handler will be called after any decorated handlers.

"},{"location":"guide/events/#applying-css-selectors-to-arbitrary-attributes","title":"Applying CSS selectors to arbitrary attributes","text":"

The on decorator also accepts selectors as keyword arguments that may be used to match other attributes in a Message, provided those attributes are in Message.ALLOW_SELECTOR_MATCH.

The snippet below shows how to match the message TabbedContent.TabActivated only when the tab with id home was activated:

@on(TabbedContent.TabActivated, tab=\"#home\")\ndef home_tab(self) -> None:\nself.log(\"Switched back to home tab.\")\n...\n
"},{"location":"guide/events/#handler-arguments","title":"Handler arguments","text":"

Message handler methods can be written with or without a positional argument. If you add a positional argument, Textual will call the handler with the event object. The following handler (taken from custom01.py above) contains a message parameter. The body of the code makes use of the message to set a preset color.

    def on_color_button_selected(self, message: ColorButton.Selected) -> None:\nself.screen.styles.animate(\"background\", message.color, duration=0.5)\n

A similar handler can be written using the decorator on:

    @on(ColorButton.Selected)\ndef animate_background_color(self, message: ColorButton.Selected) -> None:\nself.screen.styles.animate(\"background\", message.color, duration=0.5)\n

If the body of your handler doesn't require any information in the message you can omit it from the method signature. If we just want to play a bell noise when the button is clicked, we could write our handler like this:

    def on_color_button_selected(self) -> None:\nself.app.bell()\n

This pattern is a convenience that saves writing out a parameter that may not be used.

"},{"location":"guide/events/#async-handlers","title":"Async handlers","text":"

Message handlers may be coroutines. If you prefix your handlers with the async keyword, Textual will await them. This lets your handler use the await keyword for asynchronous APIs.

If your event handlers are coroutines it will allow multiple events to be processed concurrently, but bear in mind an individual widget (or app) will not be able to pick up a new message from its message queue until the handler has returned. This is rarely a problem in practice; as long as handlers return within a few milliseconds the UI will remain responsive. But slow handlers might make your app hard to use.

Info

To re-use the chef analogy, if an order comes in for beef wellington (which takes a while to cook), orders may start to pile up and customers may have to wait for their meal. The solution would be to have another chef work on the wellington while the first chef picks up new orders.

Network access is a common cause of slow handlers. If you try to retrieve a file from the internet, the message handler may take anything up to a few seconds to return, which would prevent the widget or app from updating during that time. The solution is to launch a new asyncio task to do the network task in the background.

Let's look at an example which looks up word definitions from an api as you type.

Note

You will need to install httpx with pip install httpx to run this example.

dictionary.pydictionary.tcssOutput dictionary.py
import asyncio\ntry:\nimport httpx\nexcept ImportError:\nraise ImportError(\"Please install httpx with 'pip install httpx' \")\nfrom rich.json import JSON\nfrom textual.app import App, ComposeResult\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Input, Static\nclass DictionaryApp(App):\n\"\"\"Searches a dictionary API as-you-type.\"\"\"\nCSS_PATH = \"dictionary.tcss\"\ndef compose(self) -> ComposeResult:\nyield Input(placeholder=\"Search for a word\")\nyield VerticalScroll(Static(id=\"results\"), id=\"results-container\")\nasync def on_input_changed(self, message: Input.Changed) -> None:\n\"\"\"A coroutine to handle a text changed message.\"\"\"\nif message.value:\n# Look up the word in the background\nasyncio.create_task(self.lookup_word(message.value))\nelse:\n# Clear the results\nself.query_one(\"#results\", Static).update()\nasync def lookup_word(self, word: str) -> None:\n\"\"\"Looks up a word.\"\"\"\nurl = f\"https://api.dictionaryapi.dev/api/v2/entries/en/{word}\"\nasync with httpx.AsyncClient() as client:\nresults = (await client.get(url)).text\nif word == self.query_one(Input).value:\nself.query_one(\"#results\", Static).update(JSON(results))\nif __name__ == \"__main__\":\napp = DictionaryApp()\napp.run()\n
dictionary.tcss
Screen {\nbackground: $panel;\n}\nInput {\ndock: top;\nwidth: 100%;\nheight: 1;\npadding: 0 1;\nmargin: 1 1 0 1;\n}\n#results {\nwidth: auto;\nmin-height: 100%;\n}\n#results-container {\nbackground: $background 50%;\noverflow: auto;\nmargin: 1 2;\nheight: 100%;\n}\n

DictionaryApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aSearch\u00a0for\u00a0a\u00a0word\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

Note the highlighted line in the above code which calls asyncio.create_task to run a coroutine in the background. Without this you would find typing in to the text box to be unresponsive.

"},{"location":"guide/input/","title":"Input","text":"

This chapter will discuss how to make your app respond to input in the form of key presses and mouse actions.

Quote

More Input!

\u2014 Johnny Five

"},{"location":"guide/input/#keyboard-input","title":"Keyboard input","text":"

The most fundamental way to receive input is via Key events which are sent to your app when the user presses a key. Let's write an app to show key events as you type.

key01.pyOutput key01.py
from textual import events\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import RichLog\nclass InputApp(App):\n\"\"\"App to display key events.\"\"\"\ndef compose(self) -> ComposeResult:\nyield RichLog()\ndef on_key(self, event: events.Key) -> None:\nself.query_one(RichLog).write(event)\nif __name__ == \"__main__\":\napp = InputApp()\napp.run()\n

InputApp Key(key='T',\u00a0character='T',\u00a0name='upper_t',\u00a0is_printable=True) Key(key='e',\u00a0character='e',\u00a0name='e',\u00a0is_printable=True) Key(key='x',\u00a0character='x',\u00a0name='x',\u00a0is_printable=True) Key(key='t',\u00a0character='t',\u00a0name='t',\u00a0is_printable=True) Key(key='u',\u00a0character='u',\u00a0name='u',\u00a0is_printable=True) Key(key='a',\u00a0character='a',\u00a0name='a',\u00a0is_printable=True) Key(key='l',\u00a0character='l',\u00a0name='l',\u00a0is_printable=True) Key( key='exclamation_mark', character='!', name='exclamation_mark', is_printable=True )

When you press a key, the app will receive the event and write it to a RichLog widget. Try pressing a few keys to see what happens.

Tip

For a more feature rich version of this example, run textual keys from the command line.

"},{"location":"guide/input/#key-event","title":"Key Event","text":"

The key event contains the following attributes which your app can use to know how to respond.

"},{"location":"guide/input/#key","title":"key","text":"

The key attribute is a string which identifies the key that was pressed. The value of key will be a single character for letters and numbers, or a longer identifier for other keys.

Some keys may be combined with the Shift key. In the case of letters, this will result in a capital letter as you might expect. For non-printable keys, the key attribute will be prefixed with shift+. For example, Shift+Home will produce an event with key=\"shift+home\".

Many keys can also be combined with Ctrl which will prefix the key with ctrl+. For instance, Ctrl+P will produce an event with key=\"ctrl+p\".

Warning

Not all keys combinations are supported in terminals and some keys may be intercepted by your OS. If in doubt, run textual keys from the command line.

"},{"location":"guide/input/#character","title":"character","text":"

If the key has an associated printable character, then character will contain a string with a single Unicode character. If there is no printable character for the key (such as for function keys) then character will be None.

For example the P key will produce character=\"p\" but F2 will produce character=None.

"},{"location":"guide/input/#name","title":"name","text":"

The name attribute is similar to key but, unlike key, is guaranteed to be valid within a Python function name. Textual derives name from the key attribute by lower casing it and replacing + with _. Upper case letters are prefixed with upper_ to distinguish them from lower case names.

For example, Ctrl+P produces name=\"ctrl_p\" and Shift+P produces name=\"upper_p\".

"},{"location":"guide/input/#is_printable","title":"is_printable","text":"

The is_printable attribute is a boolean which indicates if the key would typically result in something that could be used in an input widget. If is_printable is False then the key is a control code or function key that you wouldn't expect to produce anything in an input.

"},{"location":"guide/input/#aliases","title":"aliases","text":"

Some keys or combinations of keys can produce the same event. For instance, the Tab key is indistinguishable from Ctrl+I in the terminal. For such keys, Textual events will contain a list of the possible keys that may have produced this event. In the case of Tab, the aliases attribute will contain [\"tab\", \"ctrl+i\"]

"},{"location":"guide/input/#key-methods","title":"Key methods","text":"

Textual offers a convenient way of handling specific keys. If you create a method beginning with key_ followed by the key name (the event's name attribute), then that method will be called in response to the key press.

Let's add a key method to the example code.

key02.py
from textual import events\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import RichLog\nclass InputApp(App):\n\"\"\"App to display key events.\"\"\"\ndef compose(self) -> ComposeResult:\nyield RichLog()\ndef on_key(self, event: events.Key) -> None:\nself.query_one(RichLog).write(event)\ndef key_space(self) -> None:\nself.bell()\nif __name__ == \"__main__\":\napp = InputApp()\napp.run()\n

Note the addition of a key_space method which is called in response to the space key, and plays the terminal bell noise.

Note

Consider key methods to be a convenience for experimenting with Textual features. In nearly all cases, key bindings and actions are preferable.

"},{"location":"guide/input/#input-focus","title":"Input focus","text":"

Only a single widget may receive key events at a time. The widget which is actively receiving key events is said to have input focus.

The following example shows how focus works in practice.

key03.pykey03.tcssOutput key03.py
from textual import events\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import RichLog\nclass KeyLogger(RichLog):\ndef on_key(self, event: events.Key) -> None:\nself.write(event)\nclass InputApp(App):\n\"\"\"App to display key events.\"\"\"\nCSS_PATH = \"key03.tcss\"\ndef compose(self) -> ComposeResult:\nyield KeyLogger()\nyield KeyLogger()\nyield KeyLogger()\nyield KeyLogger()\nif __name__ == \"__main__\":\napp = InputApp()\napp.run()\n
key03.tcss
Screen {\nlayout: grid;\ngrid-size: 2 2;\ngrid-columns: 1fr;\n}\nKeyLogger {\nborder: blank;\n}\nKeyLogger:hover {\nborder: wide $secondary;\n}\nKeyLogger:focus {\nborder: wide $accent;\n}\n

InputApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 name='o',\u258echaracter='d',\u258a is_printable=True\u258ename='d',\u258a )\u258eis_printable=True\u258a Key(\u258e)\u258a key='tab',\u258eKey(\u258a character='\\t',\u258ekey='exclamation_mark',\u258a name='tab',\u258echaracter='!',\u258a is_printable=False,\u2586\u2586\u258ename='exclamation_mark',\u2587\u2587\u258a aliases=['tab',\u00a0'ctrl+i']\u258eis_printable=True\u258a )\u258e)\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

The app splits the screen in to quarters, with a RichLog widget in each quarter. If you click any of the text logs, you should see that it is highlighted to show that the widget has focus. Key events will be sent to the focused widget only.

Tip

the :focus CSS pseudo-selector can be used to apply a style to the focused widget.

You can move focus by pressing the Tab key to focus the next widget. Pressing Shift+Tab moves the focus in the opposite direction.

"},{"location":"guide/input/#controlling-focus","title":"Controlling focus","text":"

Textual will handle keyboard focus automatically, but you can tell Textual to focus a widget by calling the widget's focus() method.

"},{"location":"guide/input/#focus-events","title":"Focus events","text":"

When a widget receives focus, it is sent a Focus event. When a widget loses focus it is sent a Blur event.

"},{"location":"guide/input/#bindings","title":"Bindings","text":"

Keys may be associated with actions for a given widget. This association is known as a key binding.

To create bindings, add a BINDINGS class variable to your app or widget. This should be a list of tuples of three strings. The first value is the key, the second is the action, the third value is a short human readable description.

The following example binds the keys R, G, and B to an action which adds a bar widget to the screen.

binding01.pybinding01.tcssOutput binding01.py
from textual.app import App, ComposeResult\nfrom textual.color import Color\nfrom textual.widgets import Footer, Static\nclass Bar(Static):\npass\nclass BindingApp(App):\nCSS_PATH = \"binding01.tcss\"\nBINDINGS = [\n(\"r\", \"add_bar('red')\", \"Add Red\"),\n(\"g\", \"add_bar('green')\", \"Add Green\"),\n(\"b\", \"add_bar('blue')\", \"Add Blue\"),\n]\ndef compose(self) -> ComposeResult:\nyield Footer()\ndef action_add_bar(self, color: str) -> None:\nbar = Bar(color)\nbar.styles.background = Color.parse(color).with_alpha(0.5)\nself.mount(bar)\nself.call_after_refresh(self.screen.scroll_end, animate=False)\nif __name__ == \"__main__\":\napp = BindingApp()\napp.run()\n
binding01.tcss
Bar {\nheight: 5;\ncontent-align: center middle;\ntext-style: bold;\nmargin: 1 2;\ncolor: $text;\n}\n

BindingApp red\u2582\u2582 green blue blue \u00a0R\u00a0\u00a0Add\u00a0Red\u00a0\u00a0G\u00a0\u00a0Add\u00a0Green\u00a0\u00a0B\u00a0\u00a0Add\u00a0Blue\u00a0

Note how the footer displays bindings and makes them clickable.

Tip

Multiple keys can be bound to a single action by comma-separating them. For example, (\"r,t\", \"add_bar('red')\", \"Add Red\") means both R and T are bound to add_bar('red').

"},{"location":"guide/input/#binding-class","title":"Binding class","text":"

The tuple of three strings may be enough for simple bindings, but you can also replace the tuple with a Binding instance which exposes a few more options.

"},{"location":"guide/input/#priority-bindings","title":"Priority bindings","text":"

Individual bindings may be marked as a priority, which means they will be checked prior to the bindings of the focused widget. This feature is often used to create hot-keys on the app or screen. Such bindings can not be disabled by binding the same key on a widget.

You can create priority key bindings by setting priority=True on the Binding object. Textual uses this feature to add a default binding for Ctrl+C so there is always a way to exit the app. Here's the bindings from the App base class. Note the first binding is set as a priority:

    BINDINGS = [\nBinding(\"ctrl+c\", \"quit\", \"Quit\", show=False, priority=True),\nBinding(\"tab\", \"focus_next\", \"Focus Next\", show=False),\nBinding(\"shift+tab\", \"focus_previous\", \"Focus Previous\", show=False),\n]\n
"},{"location":"guide/input/#show-bindings","title":"Show bindings","text":"

The footer widget can inspect bindings to display available keys. If you don't want a binding to display in the footer you can set show=False. The default bindings on App do this so that the standard Ctrl+C, Tab and Shift+Tab bindings don't typically appear in the footer.

"},{"location":"guide/input/#mouse-input","title":"Mouse Input","text":"

Textual will send events in response to mouse movement and mouse clicks. These events contain the coordinates of the mouse cursor relative to the terminal or widget.

Information

The trackpad (and possibly other pointer devices) are treated the same as the mouse in terminals.

Terminal coordinates are given by a pair values named x and y. The X coordinate is an offset in characters, extending from the left to the right of the screen. The Y coordinate is an offset in lines, extending from the top of the screen to the bottom.

Coordinates may be relative to the screen, so (0, 0) would be the top left of the screen. Coordinates may also be relative to a widget, where (0, 0) would be the top left of the widget itself.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1ba0/bSFx1MDAxNP3eX1x1MDAxMaVfdqUynfej0mpcdTAwMDVcdTAwMDGaQHktXHUwMDAxXG6rVeUmTmxw7GA7XHUwMDA0qPjve+1A7DxcdKHJhlXzXHUwMDAxyIxcdTAwMWbXM+ecOfeO+fGuUCjGd227+KlQtG9rlufWQ6tb/JC039hh5Fx1MDAwNj500fR7XHUwMDE0dMJaeqRcdTAwMTPH7ejTx48tK7yy47Zn1Wx040ZcdTAwMWTLi+JO3VxyUC1ofXRju1x1MDAxNf2Z/Ny3WvZcdTAwMWbtoFWPQ5TdZM2uu3FcdTAwMTD27mV7dsv241xirv43fC9cdTAwMTR+pD9z0YV2Lbb8pmenJ6RdWYBcdTAwMDRcdTAwMTM63LxcdTAwMWb4abREcIO1xFxc9I9wo024YWzXobtcdTAwMDFB21lP0lRcXN/ddcqh3GlFe7dcdTAwMDd1p4nXP5Nqdt+G63nH8Z2XxlVcdTAwMGKDKFpzrLjmZEdEcVx1MDAxOFxc2WduPXaehi/X3j83XG5gKLKzwqDTdHw7SkaB9FuDtlVz47v0KXG/tTdcdTAwMTSfXG5Zyy18Y1xcIS1cdTAwMTVcdTAwMTFEsuShs0dOzqdKIK2l4ZphTnVuQHpxlVx1MDAwMlx1MDAwZuZcdTAwMDPieo/TT1x1MDAxNtl3q3bVhPD8ev+YOLT8qG2FMGvZcd3HJ1x1MDAxNpwjySjjXGZcdTAwMGJCSHYjx3abTpxEKjHSXHUwMDA0XHUwMDFiziVjgmgqs2DsdGKMVIJjzni/I4mgXamnIPlneExcdTAwMWQrbD+OXTFKvuSiT1x1MDAwMt/KISw7udOuWz1cdTAwMWNcdTAwMTApOVFKJSGpfr/n+lfQ6Xc8L2tcdTAwMGJqV1x1MDAxOXTS1odcdTAwMGZzgFZRPFx0s1RcdTAwMWKYKMrUzJC9pXW+tS3Pr02zVbnc2zrnNz6ZXHUwMDAw2SHY/ZdgXHUwMDE1XHUwMDE4XHUwMDBiLFx1MDAxODbKaDlcdTAwMDJWhinBXG5LSoyii0QrQ5hIQbVQRFx1MDAxOaVH4Uo1klx1MDAxMjNcdTAwMGU6QiVcdTAwMDNoj8BVSmNcYjVcdTAwMDS/Ybjanue2o7FglUJMXHUwMDAyq1x1MDAxMdhcYi3MzFg9XzPfhFPdPmp2tXdcdTAwMTFEe/5e4/M8WCXLw6owiFx1MDAxOclcdTAwMTnAQ1x1MDAxYWL0IFa1QEJcdTAwMDEoXHUwMDE4pVx1MDAxOGuB+Wuw+r5hXHQq6ChOXHRDXHUwMDFjUyqMpPCLa81HgUooXHUwMDEyXHUwMDAwXHJcdTAwMDOaiiljNFx1MDAwN45HoFxuiFx1MDAxNEiVQ/D/XG6ocKOJToBcdTAwMTgjYU0hbGaofnXx93XiXHUwMDFj8Z3908PWXHUwMDA2/evM7norXHUwMDBlVS1cdTAwMTGjilx1MDAxOSap1ppcdTAwMTA1hFWOXHUwMDAwXHUwMDFjSmiDXHUwMDA11yQnu/Nh9TtI+KKwSlx1MDAxOSxcZkyI/6moKskmYlx1MDAxNVx1MDAxNlx1MDAxYWpcdTAwMTjFs2M12rwpy3W3s3dcdTAwMWWas9qFiNbL5Hi1scpJXHUwMDAyRlx1MDAwNoNONFx1MDAwMJaTIagypDBcdTAwMDYzZFx1MDAxOKgue5VcdTAwMDN4T+h38FSLQiphXHSjXHUwMDE4e8tItcIw6I5Nr9jExZ8rpjCTuVx1MDAwNfE5mKqD+0atpiW/3qVhyexcdTAwMTFIXHUwMDAzK1x1MDAxM2DqWDWnXHUwMDEz2v89UJlUSEkh6WBGxVx1MDAxOEFcdTAwMTgyLblAd4pcdTAwMTFVXG4yKTUmi5JitPNcdJDwUMJoMUf6lFx1MDAwNjcnILlcdTAwMDBcdTAwMWL9XHUwMDAyQObisMJ4w/Xrrt9cdTAwMWM+xfbrWc9cdTAwMTNsXHUwMDBi/apBpecqO9s7+5ub3Vx1MDAxM6dy24lOon1cdTAwMTmfZrhKkFx1MDAxNdQ6UTqghFx1MDAxOUHBrFx1MDAwM+UlOFx1MDAwMpI7qGm1XHUwMDEzVCNB01F97HjIoreiuFx1MDAxNLRablxmz31cdTAwMTi4fjxcdTAwMWNs+iDrXHSVXHUwMDFj26qPeZR83zDn2slcdTAwMTWzKkjyyf4qZKBMv/T//ufD2KPXRqGTfHKgya7wLv/7xVx1MDAwMqHkcGM/k4XEXG6QSNTsXHUwMDAyXHUwMDEx3G59bVxcnljd06tSuXFz0vWv/7pYfYGAXGZcdTAwMTFWMTUkXHUwMDEw1CDJsVx1MDAwNpVkXHUwMDFhw2rOhlwi+olZrEGQepixOkFcdTAwMTDBZEC9nlRCc2a4WrZMXHUwMDE4pvNcdP0yZeLwRl+FR+t39dYhO7mp4jiu7tTHy1x1MDAwNCZcdTAwMTTUjIO6q0RLiaa5w3pCQTCSvZF900oxip3ks9aHzVx1MDAwYnVcIrZv43EykUPZkExcYkGYJHmj/5xKTJ/HXHUwMDE1VVx0zjT43Vx1MDAwMY6mKkFcdTAwMDTSSi/WR+Sy3qysNSpcYlx1MDAwMGdcIlx1MDAxOPD051x1MDAxYtk+in7kQDaT6Fx1MDAwZqCrR4R+z8NcdTAwMTMkp7lcdTAwMTLKSTZjL5CbRuDHx+59byVcdTAwMWJo3bZarnc3gIRcdTAwMTT2SdEgP0mRXHK3SzM6PXDguuc2XHUwMDEzTlx1MDAxND27MUiW2K1ZXr87XHUwMDBlckNag1x1MDAxYltwubAyXCJcdTAwMTdB6DZd3/Kq/SDmoqiavI+iXHLVIIM4O+LZQt9US7aiXHUwMDFjXHUwMDA1XHUwMDFkQorgXHUwMDExknKMXHUwMDExJKl60Gz/bJJmsUwjqUnqO1xc5EzVUkg6PXVcdTAwMWLA1zwknTd1mIukd6tA0rvpJJ26fcTYRNMtlZGJY8mW2+eYKr/qzeq5+ra5xaPTXHUwMDFhoSdl91x1MDAxMs/H1OVtIFx0IdBwRs45RYxyOuDE5yps1pVNOOejXHUwMDFjZYlcdTAwMTCMddnSoLEuW1HCtFFkuVuZWsFgyFx1MDAxN1x1MDAxMGoqXHUwMDE2J+Z+Uk0sXHUwMDBlJU5CQs7D5cxA1MI76HSFdqtB6aLEq3LvXHUwMDFiu1xc9SVcdTAwMDOEXHUwMDE4XHQ96uu4ZFxic2rkK8G4iPpQsrPKwOjllvNlZH5cdTAwMWFzpfBcdTAwMGJA+fMyv/tNZ9uznS1TvdhVpVp5o0qP4tzq9atA9PhZQIFIYjXc2lx1MDAxN1x0WMhcYoXRnl0kqvbpl8P7vc+4XHUwMDFhhpVyffdr4/42Wn2RMIhcdTAwMWI6Ulwi4onfZJpqsshXXHUwMDFj5ilcdTAwMGVRXGYzwzWZx2a+TYnoVlx1MDAwZU7Lbjlyb732l42Se3h3fXw1oTiEXHUwMDA12Fx1MDAwM5gzpmBhXHUwMDE3ODd7hV/VodzDzpx6MjptN5Qkb4O9IPecPpUrqlx1MDAxMZKDg1SDfqHnalx1MDAxNdJcdTAwMGKWiNnqQzrZc9RCLcDK9mE0JvOcrvhcdTAwMDPwennmXHSCkzexv8pDUzia26JcdTAwMWbmqOGgXHUwMDExlJDZs87pjmxFOSpcdTAwMTRF4JuH8k7BKFqF0pDSsCwl3nS5/JyetlxyQGvl+flcdTAwMTYqQ5P4SfFEfjLNXHUwMDA1y7+g8lx1MDAxYztcdTAwMWT7vGxFYSm4a1xcXHUwMDFjXHUwMDFkXHUwMDE4vGNcImfl92HpSFxyJt1gwVx1MDAwNqnXloSeMdizsFx1MDAxM1wiXHUwMDEz4PfZkl++1FxmgzYtjUC/4Vx1MDAwZlx1MDAwNfz7KrDoMZK5qKRy7zRcctdXXHUwMDE5JpqJXHUwMDE3uNHKmXW4WflyXHUwMDE4XHUwMDFknYlcbrlukMvd49aqc4lrgyC/XHUwMDE53JZM7SjTi99cbpmRUZxcdTAwMWLMtDHLffFOU54vr/9iVGFcdTAwMTbzOHFt4jR5I1x1MDAwNM9eXHUwMDAxKlx1MDAxMa5cdTAwMGVD3N611i/3jsXJie9/2171/VxuqSmSckx6J4hChL9293/KloVcdTAwMWPzTutcdTAwMTguKYqTfzpcdTAwMTJcdTAwMGLYVpzGJVx1MDAwM7dd3upcdTAwMDTz3rTjVeDSYyQ9Lr17tMBFq90+jmGEik9lKphcdTAwMDS3/viY2fWKN67d3Vx1MDAxOIeC9JNcXDXlZ8JcdTAwMDU7mYJcdTAwMWZcdTAwMGbvXHUwMDFl/lx1MDAwNeEmVVx1MDAxOCJ9 XyXy(0, 0)(0, 0)Widget"},{"location":"guide/input/#mouse-movements","title":"Mouse movements","text":"

When you move the mouse cursor over a widget it will receive MouseMove events which contain the coordinate of the mouse and information about what modifier keys (Ctrl, Shift etc) are held down.

The following example shows mouse movements being used to attach a widget to the mouse cursor.

mouse01.pymouse01.tcss mouse01.py
from textual import events\nfrom textual.app import App, ComposeResult\nfrom textual.containers import Container\nfrom textual.widgets import RichLog, Static\nclass PlayArea(Container):\ndef on_mount(self) -> None:\nself.capture_mouse()\ndef on_mouse_move(self, event: events.MouseMove) -> None:\nself.screen.query_one(RichLog).write(event)\nself.query_one(Ball).offset = event.offset - (8, 2)\nclass Ball(Static):\npass\nclass MouseApp(App):\nCSS_PATH = \"mouse01.tcss\"\ndef compose(self) -> ComposeResult:\nyield RichLog()\nyield PlayArea(Ball(\"Textual\"))\nif __name__ == \"__main__\":\napp = MouseApp()\napp.run()\n
mouse01.tcss
Screen {\nlayers: log ball;\n}\nTextLog {\nlayer: log;\n}\nPlayArea {\nopacity: 0%;\nlayer: ball;\n}\nBall {\nlayer: ball;\nwidth: auto;\nheight: 1;\nbackground: $secondary;\nborder: tall $secondary;\ncolor: $background;\nbox-sizing: content-box;\ntext-style: bold;\npadding: 0 4;\n}\n

If you run mouse01.py you should find that it logs the mouse move event, and keeps a widget pinned directly under the cursor.

The on_mouse_move handler sets the offset style of the ball (a rectangular one) to match the mouse coordinates.

"},{"location":"guide/input/#mouse-capture","title":"Mouse capture","text":"

In the mouse01.py example there was a call to capture_mouse() in the mount handler. Textual will send mouse move events to the widget directly under the cursor. You can tell Textual to send all mouse events to a widget regardless of the position of the mouse cursor by calling capture_mouse.

Call release_mouse to restore the default behavior.

Warning

If you capture the mouse, be aware you might get negative mouse coordinates if the cursor is to the left of the widget.

Textual will send a MouseCapture event when the mouse is captured, and a MouseRelease event when it is released.

"},{"location":"guide/input/#enter-and-leave-events","title":"Enter and Leave events","text":"

Textual will send a Enter event to a widget when the mouse cursor first moves over it, and a Leave event when the cursor moves off a widget.

"},{"location":"guide/input/#click-events","title":"Click events","text":"

There are three events associated with clicking a button on your mouse. When the button is initially pressed, Textual sends a MouseDown event, followed by MouseUp when the button is released. Textual then sends a final Click event.

If you want your app to respond to a mouse click you should prefer the Click event (and not MouseDown or MouseUp). This is because a future version of Textual may support other pointing devices which don't have up and down states.

"},{"location":"guide/input/#scroll-events","title":"Scroll events","text":"

Most mice have a scroll wheel which you can use to scroll the window underneath the cursor. Scrollable containers in Textual will handle these automatically, but you can handle MouseScrollDown and MouseScrollUp if you want build your own scrolling functionality.

Information

Terminal emulators will typically convert trackpad gestures in to scroll events.

"},{"location":"guide/layout/","title":"Layout","text":"

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.

"},{"location":"guide/layout/#vertical","title":"Vertical","text":"

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.

Outputvertical_layout.pyvertical_layout.tcss

VerticalLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Two\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Three\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass VerticalLayoutExample(App):\nCSS_PATH = \"vertical_layout.tcss\"\ndef compose(self) -> ComposeResult:\nyield Static(\"One\", classes=\"box\")\nyield Static(\"Two\", classes=\"box\")\nyield Static(\"Three\", classes=\"box\")\nif __name__ == \"__main__\":\napp = VerticalLayoutExample()\napp.run()\n
Screen {\nlayout: vertical;\n}\n.box {\nheight: 1fr;\nborder: solid green;\n}\n

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.tcss, 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\"\n

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 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2582\u2582 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Two\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502

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

"},{"location":"guide/layout/#horizontal","title":"Horizontal","text":"

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.

Outputhorizontal_layout.pyhorizontal_layout.tcss

HorizontalLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502\u2502Two\u2502\u2502Three\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass HorizontalLayoutExample(App):\nCSS_PATH = \"horizontal_layout.tcss\"\ndef compose(self) -> ComposeResult:\nyield Static(\"One\", classes=\"box\")\nyield Static(\"Two\", classes=\"box\")\nyield Static(\"Three\", classes=\"box\")\nif __name__ == \"__main__\":\napp = HorizontalLayoutExample()\napp.run()\n
Screen {\nlayout: horizontal;\n}\n.box {\nheight: 100%;\nwidth: 1fr;\nborder: solid green;\n}\n

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:

Outputhorizontal_layout_overflow.pyhorizontal_layout_overflow.tcss

HorizontalLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u258a

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass HorizontalLayoutExample(App):\nCSS_PATH = \"horizontal_layout_overflow.tcss\"\ndef compose(self) -> ComposeResult:\nyield Static(\"One\", classes=\"box\")\nyield Static(\"Two\", classes=\"box\")\nyield Static(\"Three\", classes=\"box\")\nif __name__ == \"__main__\":\napp = HorizontalLayoutExample()\napp.run()\n
Screen {\nlayout: horizontal;\noverflow-x: auto;\n}\n.box {\nheight: 100%;\nborder: solid green;\n}\n

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.

"},{"location":"guide/layout/#utility-containers","title":"Utility containers","text":"

Textual comes with several \"container\" widgets. Among them, we have 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.

Outpututility_containers.pyutility_containers.tcss

UtilityContainersExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502One\u2502\u2502Three\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502Two\u2502\u2502Four\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal, Vertical\nfrom textual.widgets import Static\nclass UtilityContainersExample(App):\nCSS_PATH = \"utility_containers.tcss\"\ndef compose(self) -> ComposeResult:\nyield Horizontal(\nVertical(\nStatic(\"One\"),\nStatic(\"Two\"),\nclasses=\"column\",\n),\nVertical(\nStatic(\"Three\"),\nStatic(\"Four\"),\nclasses=\"column\",\n),\n)\nif __name__ == \"__main__\":\napp = UtilityContainersExample()\napp.run()\n
Static {\ncontent-align: center middle;\nbackground: crimson;\nborder: solid darkred;\nheight: 1fr;\n}\n.column {\nwidth: 1fr;\n}\n

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

"},{"location":"guide/layout/#composing-with-context-managers","title":"Composing with context managers","text":"

In the previous section, we've shown how you add children to a container (such as Horizontal and Vertical) using positional arguments. It's fine to do it this way, but Textual offers a simplified syntax using context managers, which is generally easier to write and edit.

When composing a widget, you can introduce a container using Python's with statement. Any widgets yielded within that block are added as a child of the container.

Let's update the utility containers example to use the context manager approach.

utility_containers_using_with.pyutility_containers.pyutility_containers.tcssOutput

Note

This code uses context managers to compose widgets.

from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal, Vertical\nfrom textual.widgets import Static\nclass UtilityContainersExample(App):\nCSS_PATH = \"utility_containers.tcss\"\ndef compose(self) -> ComposeResult:\nwith Horizontal():\nwith Vertical(classes=\"column\"):\nyield Static(\"One\")\nyield Static(\"Two\")\nwith Vertical(classes=\"column\"):\nyield Static(\"Three\")\nyield Static(\"Four\")\nif __name__ == \"__main__\":\napp = UtilityContainersExample()\napp.run()\n

Note

This is the original code using positional arguments.

from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal, Vertical\nfrom textual.widgets import Static\nclass UtilityContainersExample(App):\nCSS_PATH = \"utility_containers.tcss\"\ndef compose(self) -> ComposeResult:\nyield Horizontal(\nVertical(\nStatic(\"One\"),\nStatic(\"Two\"),\nclasses=\"column\",\n),\nVertical(\nStatic(\"Three\"),\nStatic(\"Four\"),\nclasses=\"column\",\n),\n)\nif __name__ == \"__main__\":\napp = UtilityContainersExample()\napp.run()\n
Static {\ncontent-align: center middle;\nbackground: crimson;\nborder: solid darkred;\nheight: 1fr;\n}\n.column {\nwidth: 1fr;\n}\n

UtilityContainersExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502One\u2502\u2502Three\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502Two\u2502\u2502Four\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

Note how the end result is the same, but the code with context managers is a little easier to read. It is up to you which method you want to use, and you can mix context managers with positional arguments if you like!

"},{"location":"guide/layout/#grid","title":"Grid","text":"

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

Outputgrid_layout1.pygrid_layout1.tcss

GridLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502\u2502Two\u2502\u2502Three\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Four\u2502\u2502Five\u2502\u2502Six\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass GridLayoutExample(App):\nCSS_PATH = \"grid_layout1.tcss\"\ndef compose(self) -> ComposeResult:\nyield Static(\"One\", classes=\"box\")\nyield Static(\"Two\", classes=\"box\")\nyield Static(\"Three\", classes=\"box\")\nyield Static(\"Four\", classes=\"box\")\nyield Static(\"Five\", classes=\"box\")\nyield Static(\"Six\", classes=\"box\")\nif __name__ == \"__main__\":\napp = GridLayoutExample()\napp.run()\n
Screen {\nlayout: grid;\ngrid-size: 3 2;\n}\n.box {\nheight: 100%;\nborder: solid green;\n}\n

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:

Outputgrid_layout2.pygrid_layout2.tcss

GridLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502\u2502Two\u2502\u2502Three\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Four\u2502\u2502Five\u2502\u2502Six\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Seven\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass GridLayoutExample(App):\nCSS_PATH = \"grid_layout2.tcss\"\ndef compose(self) -> ComposeResult:\nyield Static(\"One\", classes=\"box\")\nyield Static(\"Two\", classes=\"box\")\nyield Static(\"Three\", classes=\"box\")\nyield Static(\"Four\", classes=\"box\")\nyield Static(\"Five\", classes=\"box\")\nyield Static(\"Six\", classes=\"box\")\nyield Static(\"Seven\", classes=\"box\")\nif __name__ == \"__main__\":\napp = GridLayoutExample()\napp.run()\n
Screen {\nlayout: grid;\ngrid-size: 3;\n}\n.box {\nheight: 100%;\nborder: solid green;\n}\n

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.

"},{"location":"guide/layout/#row-and-column-sizes","title":"Row and column sizes","text":"

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.

Outputgrid_layout3_row_col_adjust.pygrid_layout3_row_col_adjust.tcss

GridLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502\u2502Two\u2502\u2502Three\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Four\u2502\u2502Five\u2502\u2502Six\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass GridLayoutExample(App):\nCSS_PATH = \"grid_layout3_row_col_adjust.tcss\"\ndef compose(self) -> ComposeResult:\nyield Static(\"One\", classes=\"box\")\nyield Static(\"Two\", classes=\"box\")\nyield Static(\"Three\", classes=\"box\")\nyield Static(\"Four\", classes=\"box\")\nyield Static(\"Five\", classes=\"box\")\nyield Static(\"Six\", classes=\"box\")\nif __name__ == \"__main__\":\napp = GridLayoutExample()\napp.run()\n
Screen {\nlayout: grid;\ngrid-size: 3;\ngrid-columns: 2fr 1fr 1fr;\n}\n.box {\nheight: 100%;\nborder: solid green;\n}\n

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

Outputgrid_layout4_row_col_adjust.pygrid_layout4_row_col_adjust.tcss

GridLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502\u2502Two\u2502\u2502Three\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Four\u2502\u2502Five\u2502\u2502Six\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass GridLayoutExample(App):\nCSS_PATH = \"grid_layout4_row_col_adjust.tcss\"\ndef compose(self) -> ComposeResult:\nyield Static(\"One\", classes=\"box\")\nyield Static(\"Two\", classes=\"box\")\nyield Static(\"Three\", classes=\"box\")\nyield Static(\"Four\", classes=\"box\")\nyield Static(\"Five\", classes=\"box\")\nyield Static(\"Six\", classes=\"box\")\nif __name__ == \"__main__\":\napp = GridLayoutExample()\napp.run()\n
Screen {\nlayout: grid;\ngrid-size: 3;\ngrid-columns: 2fr 1fr 1fr;\ngrid-rows: 25% 75%;\n}\n.box {\nheight: 100%;\nborder: solid green;\n}\n

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

"},{"location":"guide/layout/#auto-rows-columns","title":"Auto rows / columns","text":"

The grid-columns and grid-rows rules can both accept a value of \"auto\" in place of any of the dimensions, which tells Textual to calculate an optimal size based on the content.

Let's modify the previous example to make the first column an auto column.

Outputgrid_layout_auto.pygrid_layout_auto.tcss

GridLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502First\u00a0column\u2502\u2502Two\u2502\u2502Three\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Four\u2502\u2502Five\u2502\u2502Six\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass GridLayoutExample(App):\nCSS_PATH = \"grid_layout_auto.tcss\"\ndef compose(self) -> ComposeResult:\nyield Static(\"First column\", classes=\"box\")\nyield Static(\"Two\", classes=\"box\")\nyield Static(\"Three\", classes=\"box\")\nyield Static(\"Four\", classes=\"box\")\nyield Static(\"Five\", classes=\"box\")\nyield Static(\"Six\", classes=\"box\")\nif __name__ == \"__main__\":\napp = GridLayoutExample()\napp.run()\n
Screen {\nlayout: grid;\ngrid-size: 3;\ngrid-columns: auto 1fr 1fr;\ngrid-rows: 25% 75%;\n}\n.box {\nheight: 100%;\nborder: solid green;\n}\n

Notice how the first column is just wide enough to fit the content of each cell. The layout will adjust accordingly if you update the content for any widget in that column.

"},{"location":"guide/layout/#cell-spans","title":"Cell spans","text":"

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.

Outputgrid_layout5_col_span.pygrid_layout5_col_span.tcss

GridLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502\u2502Two\u00a0(column-span:\u00a02)\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Three\u2502\u2502Four\u2502\u2502Five\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Six\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass GridLayoutExample(App):\nCSS_PATH = \"grid_layout5_col_span.tcss\"\ndef compose(self) -> ComposeResult:\nyield Static(\"One\", classes=\"box\")\nyield Static(\"Two [b](column-span: 2)\", classes=\"box\", id=\"two\")\nyield Static(\"Three\", classes=\"box\")\nyield Static(\"Four\", classes=\"box\")\nyield Static(\"Five\", classes=\"box\")\nyield Static(\"Six\", classes=\"box\")\nif __name__ == \"__main__\":\napp = GridLayoutExample()\napp.run()\n
Screen {\nlayout: grid;\ngrid-size: 3;\n}\n#two {\ncolumn-span: 2;\ntint: magenta 40%;\n}\n.box {\nheight: 100%;\nborder: solid green;\n}\n

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.

Outputgrid_layout6_row_span.pygrid_layout6_row_span.tcss

GridLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502\u2502Two\u00a0(column-span:\u00a02\u00a0and\u00a0row-span:\u00a02)\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2502\u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u2502\u2502 \u2502Three\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Four\u2502\u2502Five\u2502\u2502Six\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass GridLayoutExample(App):\nCSS_PATH = \"grid_layout6_row_span.tcss\"\ndef compose(self) -> ComposeResult:\nyield Static(\"One\", classes=\"box\")\nyield Static(\"Two [b](column-span: 2 and row-span: 2)\", classes=\"box\", id=\"two\")\nyield Static(\"Three\", classes=\"box\")\nyield Static(\"Four\", classes=\"box\")\nyield Static(\"Five\", classes=\"box\")\nyield Static(\"Six\", classes=\"box\")\napp = GridLayoutExample()\nif __name__ == \"__main__\":\napp.run()\n
Screen {\nlayout: grid;\ngrid-size: 3;\n}\n#two {\ncolumn-span: 2;\nrow-span: 2;\ntint: magenta 40%;\n}\n.box {\nheight: 100%;\nborder: solid green;\n}\n

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.

"},{"location":"guide/layout/#gutter","title":"Gutter","text":"

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.

Outputgrid_layout7_gutter.pygrid_layout7_gutter.tcss

GridLayoutExample OneTwoThree FourFiveSix

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass GridLayoutExample(App):\nCSS_PATH = \"grid_layout7_gutter.tcss\"\ndef compose(self) -> ComposeResult:\nyield Static(\"One\", classes=\"box\")\nyield Static(\"Two\", classes=\"box\")\nyield Static(\"Three\", classes=\"box\")\nyield Static(\"Four\", classes=\"box\")\nyield Static(\"Five\", classes=\"box\")\nyield Static(\"Six\", classes=\"box\")\nif __name__ == \"__main__\":\napp = GridLayoutExample()\napp.run()\n
Screen {\nlayout: grid;\ngrid-size: 3;\ngrid-gutter: 1;\nbackground: lightgreen;\n}\n.box {\nbackground: darkmagenta;\nheight: 100%;\n}\n

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.

"},{"location":"guide/layout/#docking","title":"Docking","text":"

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.

Outputdock_layout1_sidebar.pydock_layout1_sidebar.tcss

DockLayoutExample SidebarDocking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0 edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0making\u00a0them\u00a0ideal\u00a0 for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0\u2587\u2587 edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0making\u00a0them\u00a0ideal\u00a0 for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0 edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0making\u00a0them\u00a0ideal\u00a0 for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0 edges\u00a0of\u00a0a\u00a0container.

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nTEXT = \"\"\"\\\nDocking a widget removes it from the layout and fixes its position, aligned to either the top, right, bottom, or left edges of a container.\nDocked widgets will not scroll out of view, making them ideal for sticky headers, footers, and sidebars.\n\"\"\"\nclass DockLayoutExample(App):\nCSS_PATH = \"dock_layout1_sidebar.tcss\"\ndef compose(self) -> ComposeResult:\nyield Static(\"Sidebar\", id=\"sidebar\")\nyield Static(TEXT * 10, id=\"body\")\nif __name__ == \"__main__\":\napp = DockLayoutExample()\napp.run()\n
#sidebar {\ndock: left;\nwidth: 15;\nheight: 100%;\ncolor: #0f2b41;\nbackground: dodgerblue;\n}\n

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.

Outputdock_layout2_sidebar.pydock_layout2_sidebar.tcss

DockLayoutExample Sidebar1Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0 fixes\u00a0its\u00a0position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0 right,\u00a0bottom,\u00a0or\u00a0left\u00a0edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0 making\u00a0them\u00a0ideal\u00a0for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0 and\u00a0sidebars. \u2587\u2587 Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0 fixes\u00a0its\u00a0position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0 right,\u00a0bottom,\u00a0or\u00a0left\u00a0edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0 making\u00a0them\u00a0ideal\u00a0for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0 and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0 fixes\u00a0its\u00a0position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0 right,\u00a0bottom,\u00a0or\u00a0left\u00a0edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0 making\u00a0them\u00a0ideal\u00a0for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0 and\u00a0sidebars.

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nTEXT = \"\"\"\\\nDocking a widget removes it from the layout and fixes its position, aligned to either the top, right, bottom, or left edges of a container.\nDocked widgets will not scroll out of view, making them ideal for sticky headers, footers, and sidebars.\n\"\"\"\nclass DockLayoutExample(App):\nCSS_PATH = \"dock_layout2_sidebar.tcss\"\ndef compose(self) -> ComposeResult:\nyield Static(\"Sidebar2\", id=\"another-sidebar\")\nyield Static(\"Sidebar1\", id=\"sidebar\")\nyield Static(TEXT * 10, id=\"body\")\napp = DockLayoutExample()\nif __name__ == \"__main__\":\napp.run()\n
#another-sidebar {\ndock: left;\nwidth: 30;\nheight: 100%;\nbackground: deeppink;\n}\n#sidebar {\ndock: left;\nwidth: 15;\nheight: 100%;\ncolor: #0f2b41;\nbackground: dodgerblue;\n}\n

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.

Outputdock_layout3_sidebar_header.pydock_layout3_sidebar_header.tcss

DockLayoutExample Sidebar1DockLayoutExample Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0 edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0making\u00a0them\u00a0ideal\u00a0 for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0 edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0making\u00a0them\u00a0ideal\u00a0 for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0 edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0making\u00a0them\u00a0ideal\u00a0 for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0

from textual.app import App, ComposeResult\nfrom textual.widgets import Header, Static\nTEXT = \"\"\"\\\nDocking a widget removes it from the layout and fixes its position, aligned to either the top, right, bottom, or left edges of a container.\nDocked widgets will not scroll out of view, making them ideal for sticky headers, footers, and sidebars.\n\"\"\"\nclass DockLayoutExample(App):\nCSS_PATH = \"dock_layout3_sidebar_header.tcss\"\ndef compose(self) -> ComposeResult:\nyield Header(id=\"header\")\nyield Static(\"Sidebar1\", id=\"sidebar\")\nyield Static(TEXT * 10, id=\"body\")\nif __name__ == \"__main__\":\napp = DockLayoutExample()\napp.run()\n
#sidebar {\ndock: left;\nwidth: 15;\nheight: 100%;\ncolor: #0f2b41;\nbackground: dodgerblue;\n}\n

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.

"},{"location":"guide/layout/#layers","title":"Layers","text":"

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

Outputlayers.pylayers.tcss

LayersExample box1\u00a0(layer\u00a0=\u00a0above) box2\u00a0(layer\u00a0=\u00a0below)

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass LayersExample(App):\nCSS_PATH = \"layers.tcss\"\ndef compose(self) -> ComposeResult:\nyield Static(\"box1 (layer = above)\", id=\"box1\")\nyield Static(\"box2 (layer = below)\", id=\"box2\")\nif __name__ == \"__main__\":\napp = LayersExample()\napp.run()\n
Screen {\nalign: center middle;\nlayers: below above;\n}\nStatic {\nwidth: 28;\nheight: 8;\ncolor: auto;\ncontent-align: center middle;\n}\n#box1 {\nlayer: above;\nbackground: darkcyan;\n}\n#box2 {\nlayer: below;\nbackground: orange;\noffset: 12 6;\n}\n
"},{"location":"guide/layout/#offsets","title":"Offsets","text":"

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.
"},{"location":"guide/layout/#putting-it-all-together","title":"Putting it all together","text":"

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.

Outputcombining_layouts.pycombining_layouts.tcss

CombiningLayoutsExample \u2b58CombiningLayoutsExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502HorizontallyPositionedChildrenHere\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a00\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a01\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2585\u2585\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a02\u2502\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502\u2502\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502Thispanelis\u2502 \u2502\u2502\u2502\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a03\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502usinggrid\u00a0layout!\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a04\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

from textual.app import App, ComposeResult\nfrom textual.containers import Container, Horizontal, VerticalScroll\nfrom textual.widgets import Header, Static\nclass CombiningLayoutsExample(App):\nCSS_PATH = \"combining_layouts.tcss\"\ndef compose(self) -> ComposeResult:\nyield Header()\nwith Container(id=\"app-grid\"):\nwith VerticalScroll(id=\"left-pane\"):\nfor number in range(15):\nyield Static(f\"Vertical layout, child {number}\")\nwith Horizontal(id=\"top-right\"):\nyield Static(\"Horizontally\")\nyield Static(\"Positioned\")\nyield Static(\"Children\")\nyield Static(\"Here\")\nwith Container(id=\"bottom-right\"):\nyield Static(\"This\")\nyield Static(\"panel\")\nyield Static(\"is\")\nyield Static(\"using\")\nyield Static(\"grid layout!\", id=\"bottom-right-final\")\nif __name__ == \"__main__\":\napp = CombiningLayoutsExample()\napp.run()\n
#app-grid {\nlayout: grid;\ngrid-size: 2;  /* two columns */\ngrid-columns: 1fr;\ngrid-rows: 1fr;\n}\n#left-pane > Static {\nbackground: $boost;\ncolor: auto;\nmargin-bottom: 1;\npadding: 1;\n}\n#left-pane {\nwidth: 100%;\nheight: 100%;\nrow-span: 2;\nbackground: $panel;\nborder: dodgerblue;\n}\n#top-right {\nheight: 100%;\nbackground: $panel;\nborder: mediumvioletred;\n}\n#top-right > Static {\nwidth: auto;\nheight: 100%;\nmargin-right: 1;\nbackground: $boost;\n}\n#bottom-right {\nheight: 100%;\nlayout: grid;\ngrid-size: 3;\ngrid-columns: 1fr;\ngrid-rows: 1fr;\ngrid-gutter: 1;\nbackground: $panel;\nborder: greenyellow;\n}\n#bottom-right-final {\ncolumn-span: 2;\n}\n#bottom-right > Static {\nheight: 100%;\nbackground: $boost;\n}\n

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

"},{"location":"guide/queries/","title":"DOM Queries","text":"

In the previous chapter we introduced the DOM which is how Textual apps keep track of widgets. We saw how you can apply styles to the DOM with CSS selectors.

Selectors are a very useful idea and can do more that apply styles. We can also find widgets in Python code with selectors, and make updates to widgets in a simple expressive way. Let's look at how!

"},{"location":"guide/queries/#query-one","title":"Query one","text":"

The query_one method gets a single widget in an app or other widget. If you call it with a selector it will return the first matching widget.

Let's say we have a widget with an ID of send and we want to get a reference to it in our app. We could do this with the following:

send_button = self.query_one(\"#send\")\n

If there is no widget with an ID of send, Textual will raise a NoMatches exception. Otherwise it will return the matched widget.

You can also add a second parameter for the expected type.

send_button = self.query_one(\"#send\", Button)\n

If the matched widget is not a button (i.e. if isinstance(widget, Button) equals False), Textual will raise a WrongType exception.

Tip

The second parameter allows type-checkers like MyPy to know the exact return type. Without it, MyPy would only know the result of query_one is a Widget (the base class).

"},{"location":"guide/queries/#making-queries","title":"Making queries","text":"

Apps and widgets have a query method which finds (or queries) widgets. This method returns a DOMQuery object which is a list-like container of widgets.

If you call query with no arguments, you will get back a DOMQuery containing all widgets. This method is recursive, meaning it will also return child widgets (as many levels as required).

Here's how you might iterate over all the widgets in your app:

for widget in self.query():\nprint(widget)\n

Called on the app, this will retrieve all widgets in the app. If you call the same method on a widget, it will return the children of that widget.

Note

All the query and related methods work on both App and Widget sub-classes.

"},{"location":"guide/queries/#query-selectors","title":"Query selectors","text":"

You can call query with a CSS selector. Let's look a few examples:

If we want to find all the button widgets, we could do something like the following:

for button in self.query(\"Button\"):\nprint(button)\n

Any selector that works in CSS will work with the query method. For instance, if we want to find all the disabled buttons in a Dialog widget, we could do this:

for button in self.query(\"Dialog Button.disabled\"):\nprint(button)\n

Info

The selector Dialog Button.disabled says find all the Button with a CSS class of disabled that are a child of a Dialog widget.

"},{"location":"guide/queries/#results","title":"Results","text":"

Query objects have a results method which is an alternative way of iterating over widgets. If you supply a type (i.e. a Widget class) then this method will generate only objects of that type.

The following example queries for widgets with the disabled CSS class and iterates over just the Button objects.

for button in self.query(\".disabled\").results(Button):\nprint(button)\n

Tip

This method allows type-checkers like MyPy to know the exact type of the object in the loop. Without it, MyPy would only know that button is a Widget (the base class).

"},{"location":"guide/queries/#query-objects","title":"Query objects","text":"

We've seen that the query method returns a DOMQuery object you can iterate over in a for loop. Query objects behave like Python lists and support all of the same operations (such as query[0], len(query) ,reverse(query) etc). They also have a number of other methods to simplify retrieving and modifying widgets.

"},{"location":"guide/queries/#first-and-last","title":"First and last","text":"

The first and last methods return the first or last matching widget from the selector, respectively.

Here's how we might find the last button in an app:

last_button = self.query(\"Button\").last()\n

If there are no buttons, Textual will raise a NoMatches exception. Otherwise it will return a button widget.

Both first() and last() accept an expect_type argument that should be the class of the widget you are expecting. Let's say we want to get the last widget with class .disabled, and we want to check it really is a button. We could do this:

disabled_button = self.query(\".disabled\").last(Button)\n

The query selects all widgets with a disabled CSS class. The last method gets the last disabled widget and checks it is a Button and not any other kind of widget.

If the last widget is not a button, Textual will raise a WrongType exception.

Tip

Specifying the expected type allows type-checkers like MyPy to know the exact return type.

"},{"location":"guide/queries/#filter","title":"Filter","text":"

Query objects have a filter method which further refines a query. This method will return a new query object with widgets that match both the original query and the new selector

Let's say we have a query which gets all the buttons in an app, and we want a new query object with just the disabled buttons. We could write something like this:

# Get all the Buttons\nbuttons_query = self.query(\"Button\")\n# Buttons with 'disabled' CSS class\ndisabled_buttons = buttons_query.filter(\".disabled\")\n

Iterating over disabled_buttons will give us all the disabled buttons.

"},{"location":"guide/queries/#exclude","title":"Exclude","text":"

Query objects have an exclude method which is the logical opposite of filter. The exclude method removes any widgets from the query object which match a selector.

Here's how we could get all the buttons which don't have the disabled class set.

# Get all the Buttons\nbuttons_query = self.query(\"Button\")\n# Remove all the Buttons with the 'disabled' CSS class\nenabled_buttons = buttons_query.exclude(\".disabled\")\n
"},{"location":"guide/queries/#loop-free-operations","title":"Loop-free operations","text":"

Once you have a query object, you can loop over it to call methods on the matched widgets. Query objects also support a number of methods which make an update to every matched widget without an explicit loop.

For instance, let's say we want to disable all buttons in an app. We could do this by calling add_class() on a query object.

self.query(\"Button\").add_class(\"disabled\")\n

This single line is equivalent to the following:

for widget in self.query(\"Button\"):\nwidget.add_class(\"disabled\")\n

Here are the other loop-free methods on query objects:

  • set_class Sets a CSS class (or classes) on matched widgets.
  • add_class Adds a CSS class (or classes) to matched widgets.
  • remove_class Removes a CSS class (or classes) from matched widgets.
  • toggle_class Sets a CSS class (or classes) if it is not set, or removes the class (or classes) if they are set on the matched widgets.
  • remove Removes matched widgets from the DOM.
  • refresh Refreshes matched widgets.
"},{"location":"guide/reactivity/","title":"Reactivity","text":"

Textual's reactive attributes are attributes with superpowers. In this chapter we will look at how reactive attributes can simplify your apps.

Quote

With great power comes great responsibility.

\u2014 Uncle Ben

"},{"location":"guide/reactivity/#reactive-attributes","title":"Reactive attributes","text":"

Textual provides an alternative way of adding attributes to your widget or App, which doesn't require adding them to your class constructor (__init__). To create these attributes import reactive from textual.reactive, and assign them in the class scope.

The following code illustrates how to create reactive attributes:

from textual.reactive import reactive\nfrom textual.widget import Widget\nclass Reactive(Widget):\nname = reactive(\"Paul\")  # (1)!\ncount = reactive(0) # (2)!\nis_cool = reactive(True)  # (3)!\n
  1. Create a string attribute with a default of \"Paul\"
  2. Creates an integer attribute with a default of 0.
  3. Creates a boolean attribute with a default of True.

The reactive constructor accepts a default value as the first positional argument.

Information

Textual uses Python's descriptor protocol to create reactive attributes, which is the same protocol used by the builtin property decorator.

You can get and set these attributes in the same way as if you had assigned them in an __init__ method. For instance self.name = \"Jessica\", self.count += 1, or print(self.is_cool).

"},{"location":"guide/reactivity/#dynamic-defaults","title":"Dynamic defaults","text":"

You can also set the default to a function (or other callable). Textual will call this function to get the default value. The following code illustrates a reactive value which will be automatically assigned the current time when the widget is created:

from time import time\nfrom textual.reactive import reactive\nfrom textual.widget import Widget\nclass Timer(Widget):\nstart_time = reactive(time)  # (1)!\n
  1. The time function returns the current time in seconds.
"},{"location":"guide/reactivity/#typing-reactive-attributes","title":"Typing reactive attributes","text":"

There is no need to specify a type hint if a reactive attribute has a default value, as type checkers will assume the attribute is the same type as the default.

You may want to add explicit type hints if the attribute type is a superset of the default type. For instance if you want to make an attribute optional. Here's how you would create a reactive string attribute which may be None:

    name: reactive[str | None] = reactive(\"Paul\")\n
"},{"location":"guide/reactivity/#smart-refresh","title":"Smart refresh","text":"

The first superpower we will look at is \"smart refresh\". When you modify a reactive attribute, Textual will make note of the fact that it has changed and refresh automatically.

Information

If you modify multiple reactive attributes, Textual will only do a single refresh to minimize updates.

Let's look at an example which illustrates this. In the following app, the value of an input is used to update a \"Hello, World!\" type greeting.

refresh01.pyrefresh01.tcssOutput
from textual.app import App, ComposeResult\nfrom textual.reactive import reactive\nfrom textual.widget import Widget\nfrom textual.widgets import Input\nclass Name(Widget):\n\"\"\"Generates a greeting.\"\"\"\nwho = reactive(\"name\")\ndef render(self) -> str:\nreturn f\"Hello, {self.who}!\"\nclass WatchApp(App):\nCSS_PATH = \"refresh01.tcss\"\ndef compose(self) -> ComposeResult:\nyield Input(placeholder=\"Enter your name\")\nyield Name()\ndef on_input_changed(self, event: Input.Changed) -> None:\nself.query_one(Name).who = event.value\nif __name__ == \"__main__\":\napp = WatchApp()\napp.run()\n
Input {\ndock: top;\nmargin-top: 1;\n}\nName {\nheight: 100%;\ncontent-align: center middle;\n}\n

WatchApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aTextual\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e Hello,\u00a0Textual!

The Name widget has a reactive who attribute. When the app modifies that attribute, a refresh happens automatically.

Information

Textual will check if a value has really changed, so assigning the same value wont prompt an unnecessary refresh.

"},{"location":"guide/reactivity/#disabling-refresh","title":"Disabling refresh","text":"

If you don't want an attribute to prompt a refresh or layout but you still want other reactive superpowers, you can use var to create an attribute. You can import var from textual.reactive.

The following code illustrates how you create non-refreshing reactive attributes.

class MyWidget(Widget):\ncount = var(0)  # (1)!\n
  1. Changing self.count wont cause a refresh or layout.
"},{"location":"guide/reactivity/#layout","title":"Layout","text":"

The smart refresh feature will update the content area of a widget, but will not change its size. If modifying an attribute should change the size of the widget, you can set layout=True on the reactive attribute. This ensures that your CSS layout will update accordingly.

The following example modifies \"refresh01.py\" so that the greeting has an automatic width.

refresh02.pyrefresh02.tcssOutput
from textual.app import App, ComposeResult\nfrom textual.reactive import reactive\nfrom textual.widget import Widget\nfrom textual.widgets import Input\nclass Name(Widget):\n\"\"\"Generates a greeting.\"\"\"\nwho = reactive(\"name\", layout=True)  # (1)!\ndef render(self) -> str:\nreturn f\"Hello, {self.who}!\"\nclass WatchApp(App):\nCSS_PATH = \"refresh02.tcss\"\ndef compose(self) -> ComposeResult:\nyield Input(placeholder=\"Enter your name\")\nyield Name()\ndef on_input_changed(self, event: Input.Changed) -> None:\nself.query_one(Name).who = event.value\nif __name__ == \"__main__\":\napp = WatchApp()\napp.run()\n
  1. This attribute will update the layout when changed.
Input {\ndock: top;\nmargin-top: 1;\n}\nName {\nwidth: auto;\nheight: auto;\nborder: heavy $secondary;\n}\n

WatchApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aname\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Hello,\u00a0name!\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

If you type in to the input now, the greeting will expand to fit the content. If you were to set layout=False on the reactive attribute, you should see that the box remains the same size when you type.

"},{"location":"guide/reactivity/#validation","title":"Validation","text":"

The next superpower we will look at is validation, which can check and potentially modify a value you assign to a reactive attribute.

If you add a method that begins with validate_ followed by the name of your attribute, it will be called when you assign a value to that attribute. This method should accept the incoming value as a positional argument, and return the value to set (which may be the same or a different value).

A common use for this is to restrict numbers to a given range. The following example keeps a count. There is a button to increase the count, and a button to decrease it. The validation ensures that the count will never go above 10 or below zero.

validate01.pyvalidate01.tcssOutput
from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.reactive import reactive\nfrom textual.widgets import Button, RichLog\nclass ValidateApp(App):\nCSS_PATH = \"validate01.tcss\"\ncount = reactive(0)\ndef validate_count(self, count: int) -> int:\n\"\"\"Validate value.\"\"\"\nif count < 0:\ncount = 0\nelif count > 10:\ncount = 10\nreturn count\ndef compose(self) -> ComposeResult:\nyield Horizontal(\nButton(\"+1\", id=\"plus\", variant=\"success\"),\nButton(\"-1\", id=\"minus\", variant=\"error\"),\nid=\"buttons\",\n)\nyield RichLog(highlight=True)\ndef on_button_pressed(self, event: Button.Pressed) -> None:\nif event.button.id == \"plus\":\nself.count += 1\nelse:\nself.count -= 1\nself.query_one(RichLog).write(f\"{self.count=}\")\nif __name__ == \"__main__\":\napp = ValidateApp()\napp.run()\n
#buttons {\ndock: top;\nheight: auto;\n}\n

ValidateApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0+1\u00a0\u00a0-1\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

If you click the buttons in the above example it will show the current count. When self.count is modified in the button handler, Textual runs validate_count which performs the validation to limit the value of count.

"},{"location":"guide/reactivity/#watch-methods","title":"Watch methods","text":"

Watch methods are another superpower. Textual will call watch methods when reactive attributes are modified. Watch method names begin with watch_ followed by the name of the attribute, and should accept one or two arguments. If the method accepts a single argument, it will be called with the new assigned value. If the method accepts two positional arguments, it will be called with both the old value and the new value.

The following app will display any color you type in to the input. Try it with a valid color in Textual CSS. For example \"darkorchid\" or \"#52de44\".

watch01.pywatch01.tcssOutput
from textual.app import App, ComposeResult\nfrom textual.color import Color, ColorParseError\nfrom textual.containers import Grid\nfrom textual.reactive import reactive\nfrom textual.widgets import Input, Static\nclass WatchApp(App):\nCSS_PATH = \"watch01.tcss\"\ncolor = reactive(Color.parse(\"transparent\"))  # (1)!\ndef compose(self) -> ComposeResult:\nyield Input(placeholder=\"Enter a color\")\nyield Grid(Static(id=\"old\"), Static(id=\"new\"), id=\"colors\")\ndef watch_color(self, old_color: Color, new_color: Color) -> None:  # (2)!\nself.query_one(\"#old\").styles.background = old_color\nself.query_one(\"#new\").styles.background = new_color\ndef on_input_submitted(self, event: Input.Submitted) -> None:\ntry:\ninput_color = Color.parse(event.value)\nexcept ColorParseError:\npass\nelse:\nself.query_one(Input).value = \"\"\nself.color = input_color  # (3)!\nif __name__ == \"__main__\":\napp = WatchApp()\napp.run()\n
  1. Creates a reactive color attribute.
  2. Called when self.color is changed.
  3. New color is assigned here.
Input {\ndock: top;\nmargin-top: 1;\n}\n#colors {\ngrid-size: 2 1;\ngrid-gutter: 2 4;\ngrid-columns: 1fr;\nmargin: 0 1;\n}\n#old {\nheight: 100%;\nborder: wide $secondary;\n}\n#new {\nheight: 100%;\nborder: wide $secondary;\n}\n

WatchApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258adarkorchid\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

The color is parsed in on_input_submitted and assigned to self.color. Because color is reactive, Textual also calls watch_color with the old and new values.

"},{"location":"guide/reactivity/#when-are-watch-methods-called","title":"When are watch methods called?","text":"

Textual only calls watch methods if the value of a reactive attribute changes. If the newly assigned value is the same as the previous value, the watch method is not called. You can override this behaviour by passing always_update=True to reactive.

"},{"location":"guide/reactivity/#compute-methods","title":"Compute methods","text":"

Compute methods are the final superpower offered by the reactive descriptor. Textual runs compute methods to calculate the value of a reactive attribute. Compute methods begin with compute_ followed by the name of the reactive value.

You could be forgiven in thinking this sounds a lot like Python's property decorator. The difference is that Textual will cache the value of compute methods, and update them when any other reactive attribute changes.

The following example uses a computed attribute. It displays three inputs for each color component (red, green, and blue). If you enter numbers in to these inputs, the background color of another widget changes.

computed01.pycomputed01.tcssOutput
from textual.app import App, ComposeResult\nfrom textual.color import Color\nfrom textual.containers import Horizontal\nfrom textual.reactive import reactive\nfrom textual.widgets import Input, Static\nclass ComputedApp(App):\nCSS_PATH = \"computed01.tcss\"\nred = reactive(0)\ngreen = reactive(0)\nblue = reactive(0)\ncolor = reactive(Color.parse(\"transparent\"))\ndef compose(self) -> ComposeResult:\nyield Horizontal(\nInput(\"0\", placeholder=\"Enter red 0-255\", id=\"red\"),\nInput(\"0\", placeholder=\"Enter green 0-255\", id=\"green\"),\nInput(\"0\", placeholder=\"Enter blue 0-255\", id=\"blue\"),\nid=\"color-inputs\",\n)\nyield Static(id=\"color\")\ndef compute_color(self) -> Color:  # (1)!\nreturn Color(self.red, self.green, self.blue).clamped\ndef watch_color(self, color: Color) -> None:  # (2)\nself.query_one(\"#color\").styles.background = color\ndef on_input_changed(self, event: Input.Changed) -> None:\ntry:\ncomponent = int(event.value)\nexcept ValueError:\nself.bell()\nelse:\nif event.input.id == \"red\":\nself.red = component\nelif event.input.id == \"green\":\nself.green = component\nelse:\nself.blue = component\nif __name__ == \"__main__\":\napp = ComputedApp()\napp.run()\n
  1. Combines color components in to a Color object.
  2. The watch method is called when the result of compute_color changes.
#color-inputs {\ndock: top;\nheight: auto;\n}\nInput {\nwidth: 1fr;\n}\n#color {\nheight: 100%;\nborder: tall $secondary;\n}\n

ComputedApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a0\u258e\u258a0\u258e\u258a0\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

Note the compute_color method which combines the color components into a Color object. It will be recalculated when any of the red , green, or blue attributes are modified.

When the result of compute_color changes, Textual will also call watch_color since color still has the watch method superpower.

Note

Textual will first attempt to call the compute method for a reactive attribute, followed by the validate method, and finally the watch method.

Note

It is best to avoid doing anything slow or CPU-intensive in a compute method. Textual calls compute methods on an object when any reactive attribute changes.

"},{"location":"guide/screens/","title":"Screens","text":"

This chapter covers Textual's screen API. We will discuss how to create screens and switch between them.

"},{"location":"guide/screens/#what-is-a-screen","title":"What is a screen?","text":"

Screens are containers for widgets that occupy the dimensions of your terminal. There can be many screens in a given app, but only one screen is active at a time.

Textual requires that there be at least one screen object and will create one implicitly in the App class. If you don't change the screen, any widgets you mount or compose will be added to this default screen.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nM1Ya0/jOFx1MDAxNP3Or6i6X3YlXGKOY8fxSKtcdTAwMTXPpSywo1x1MDAwMVxyj9VcYrmJaT3Na1x1MDAxMpfHIP77XqdMXHUwMDFlbVxiZVx1MDAxN0ZEUZv4OtfX1+fc4+R+pdfr67tU9j/0+vLWXHUwMDE3oVxuMnHTXzXt1zLLVVx1MDAxMoNcdFx1MDAxN/d5Ms38oudY6zT/sL5cdTAwMWWJbFwidVx1MDAxYVxuX1rXKp+KMNfTQCWWn0TrSsso/8P8XHUwMDFliUj+niZRoDOrXHUwMDFhZE1cdTAwMDZKJ9lsLFx1MDAxOcpIxjpcdTAwMDfv/8B9r3df/NaiXHUwMDBilIiSOCi6XHUwMDE3hlp4njvfepTERaiUIVx1MDAwN3PqkbKDyrdhMC1cdTAwMDOwXkHAsrKYpn56sbN1pD45eiNcdTAwMWKyfNP5Nvy6d1WNeqXC8FjfhbM8XGJ/PM1kZc11lkzkqVxu9Fx1MDAxOOz2XFx7+VxcnkBcbqqnsmQ6XHUwMDFhxzLPXHUwMDFizySp8JW+M21cYpWtXCJcdTAwMWVcdTAwMTU+qpZbk1x1MDAwMeJamHmO5zpcdTAwMGV1XHUwMDEwqc23cECY5VLsXHUwMDEwh9G5mLaSXHUwMDEw1lx1MDAwMGL6XHUwMDA1XHUwMDE1R1x1MDAxNdVQ+JNcdTAwMTGEXHUwMDE2XHUwMDA3VVx1MDAxZlx1MDAwZvvcrs335sdMa1x1MDAwM46lXHUwMDFhjbVpxNjyXHUwMDEwcT1GZ75r+ZBF/m3P5pRcdTAwMTKMcWkxI6aDoFx1MDAwMMKX+fyNRZY+5qmfm5tatCbQnXlcdTAwMTTVkVRbY+dcIkv5vlx1MDAxYVxmvk7GfyX88CxcdTAwMWRcdTAwMGZcdTAwMGVLX1xy2Gl5q/ul4WG1y+2Ze1x1MDAxMm1cdTAwMGUv7evp9v6BPls7+8jRfrtbkWXJzfN+XHUwMDFiUawuO5HK7eNVlchpXHUwMDFhiFx1MDAxOfZt10XE5sjjXHUwMDBl4aU9VPFcdTAwMDSM8TRcZqu2xJ9UdFmpxbtA0kacdYba5CmG2thQXHUwMDE0XHUwMDEw4i1N0e7le69cdTAwMTSldidFObeAXG6GLP+HoTpcdTAwMTNxnopcZljQwlLWxlK+wErmeraDXFxcdTAwMWK9Piu7kMihOr1cdTAwMDSJ1YInsT5W31x1MDAwYjS5XHUwMDE2hWKEsIsw41x1MDAxY1HW6LUrXCJcdTAwMTXeNdawgCxEvnMrojSUXHUwMDFiafrrb/VcdTAwMTTnXHUwMDEyXCIpXFyTxjNcdTAwMWKhXHUwMDFhXHUwMDE5aPd9mJvMXHUwMDFhqNdcbkSu7Fx1MDAxMKkgXGJrXGL0IVx1MDAxMFx1MDAwMT6zwTKCk2RqpGJcdTAwMTGetMXZScZM+nqGxVx1MDAxNkZS+qRmYlx1MDAwNCDkUJXdpVx1MDAxOXn+PdGXXyfDk+PRwblzQsefkvPLd89IXHUwMDE3W8hlhHheXHUwMDFiI1x1MDAxZNuxXHUwMDEwI9h+U0pSukhJj0GlmFx1MDAxM+tHalx1MDAwMqRcdTAwMTHFXHUwMDFlcV+fml3KXHUwMDE27MfnQ0rOXHUwMDBmtlx1MDAwMrw33tldu9zDn9+jYM78nu5/vr45INuHXHUwMDA3XHUwMDE5XHL+vMNTTLbdV/CLT4PB3u7EP/Q2iH1cdTAwMTKFf+/EXHUwMDE3ozdcdTAwMTX49sS/QOCZkVZe7a/eSOBcdPXmW3+UXHUwMDEzwinUYUKX34J3o+3dVlx1MDAxM9ZZTVxisZhdaNzbXHUwMDE1XHUwMDEz0lJMsDNfREBcdTAwMWFhXHUwMDE3wp2fKu8vx2GbvGPUaO2Q82M/kzJ+SspZo/+rSfkzMjgv5WWMnZSbVZJcdTAwMTbOMfxcdTAwMTTlQCZAv+FcXF7Bu0vxO+Wc43BcdTAwMGJe7lx1MDAxMXNaOYdcdTAwMTm1XFzOjYJcdTAwMTNujjdjXHUwMDFlslxid5vkLlx06Fx1MDAxMIsz7FJcdTAwMTcvyLlcdTAwMDebXuDGf9loXHUwMDE3wf1sJuZaZHpTxYGKR2CslFxm2OhPzbhryEKO7VLCoVx1MDAxNlKOXHTyylmb6YnU7D0tXHUwMDAyckBcdTAwMWPYg1x1MDAxYYxWr5+98kNQ19b4sXMpqX1cdTAwMTlcdTAwMDfPXHUwMDA2hThUX8Tg1Vx1MDAwME7KmLdcdTAwMTBcdTAwMTW24LWh2HVcdTAwMTXfKmyHPVx1MDAxNVY7zVx1MDAxN8JcbkWut5IoUlx1MDAxYdL/MVGxnk9zkc9ccsPvsVx1MDAxNMG8XHUwMDE1plW3zVx1MDAxN4LUeGzu3KqrXsWU4qa8/rLa2nttXHUwMDExweaoYbfysFL/NzuQwmdfpOmxXHUwMDA2pJVrXHUwMDAwYFbBY+GuJta/VvJms+Xb0lVxmDRcdTAwMTYpNCVHmundP6w8/Fx1MDAwYlxiYlx1MDAxObwifQ== ExampleApp()Screen()"},{"location":"guide/screens/#creating-a-screen","title":"Creating a screen","text":"

You can create a screen by extending the Screen class which you can import from textual.screen. The screen may be styled in the same way as other widgets, with the exception that you can't modify the screen's dimensions (as these will always be the size of your terminal).

Let's look at a simple example of writing a screen class to simulate Window's blue screen of death.

screen01.pyscreen01.tcssOutput screen01.py
from textual.app import App, ComposeResult\nfrom textual.screen import Screen\nfrom textual.widgets import Static\nERROR_TEXT = \"\"\"\nAn error has occurred. To continue:\nPress Enter to return to Windows, or\nPress CTRL+ALT+DEL to restart your computer. If you do this,\nyou will lose any unsaved information in all open applications.\nError: 0E : 016F : BFF9B3D4\n\"\"\"\nclass BSOD(Screen):\nBINDINGS = [(\"escape\", \"app.pop_screen\", \"Pop screen\")]\ndef compose(self) -> ComposeResult:\nyield Static(\" Windows \", id=\"title\")\nyield Static(ERROR_TEXT)\nyield Static(\"Press any key to continue [blink]_[/]\", id=\"any-key\")\nclass BSODApp(App):\nCSS_PATH = \"screen01.tcss\"\nSCREENS = {\"bsod\": BSOD()}\nBINDINGS = [(\"b\", \"push_screen('bsod')\", \"BSOD\")]\nif __name__ == \"__main__\":\napp = BSODApp()\napp.run()\n
screen01.tcss
BSOD {\nalign: center middle;\nbackground: blue;\ncolor: white;\n}\nBSOD>Static {\nwidth: 70;\n}\n#title {\ncontent-align-horizontal: center;\ntext-style: reverse;\n}\n#any-key {\ncontent-align-horizontal: center;\n}\n

BSODApp \u00a0Windows\u00a0 An\u00a0error\u00a0has\u00a0occurred.\u00a0To\u00a0continue: Press\u00a0Enter\u00a0to\u00a0return\u00a0to\u00a0Windows,\u00a0or Press\u00a0CTRL+ALT+DEL\u00a0to\u00a0restart\u00a0your\u00a0computer.\u00a0If\u00a0you\u00a0do\u00a0this, you\u00a0will\u00a0lose\u00a0any\u00a0unsaved\u00a0information\u00a0in\u00a0all\u00a0open\u00a0applications. Error:\u00a00E\u00a0:\u00a0016F\u00a0:\u00a0BFF9B3D4 Press\u00a0any\u00a0key\u00a0to\u00a0continue\u00a0_

If you run this you will see an empty screen. Hit the B key to show a blue screen of death. Hit Esc to return to the default screen.

The BSOD class above defines a screen with a key binding and compose method. These should be familiar as they work in the same way as apps.

The app class has a new SCREENS class variable. Textual uses this class variable to associate a name with screen object (the name is used to reference screens in the screen API). Also in the app is a key binding associated with the action \"push_screen('bsod')\". The screen class has a similar action \"pop_screen\" bound to the Esc key. We will cover these actions below.

"},{"location":"guide/screens/#named-screens","title":"Named screens","text":"

You can associate a screen with a name by defining a SCREENS class variable in your app, which should be a dict that maps names on to Screen objects. The name of the screen may be used interchangeably with screen objects in much of the screen API.

You can also install new named screens dynamically with the install_screen method. The following example installs the BSOD screen in a mount handler rather than from the SCREENS variable.

screen02.pyscreen02.tcssOutput screen02.py
from textual.app import App, ComposeResult\nfrom textual.screen import Screen\nfrom textual.widgets import Static\nERROR_TEXT = \"\"\"\nAn error has occurred. To continue:\nPress Enter to return to Windows, or\nPress CTRL+ALT+DEL to restart your computer. If you do this,\nyou will lose any unsaved information in all open applications.\nError: 0E : 016F : BFF9B3D4\n\"\"\"\nclass BSOD(Screen):\nBINDINGS = [(\"escape\", \"app.pop_screen\", \"Pop screen\")]\ndef compose(self) -> ComposeResult:\nyield Static(\" Windows \", id=\"title\")\nyield Static(ERROR_TEXT)\nyield Static(\"Press any key to continue [blink]_[/]\", id=\"any-key\")\nclass BSODApp(App):\nCSS_PATH = \"screen02.tcss\"\nBINDINGS = [(\"b\", \"push_screen('bsod')\", \"BSOD\")]\ndef on_mount(self) -> None:\nself.install_screen(BSOD(), name=\"bsod\")\nif __name__ == \"__main__\":\napp = BSODApp()\napp.run()\n
screen02.tcss
BSOD {\nalign: center middle;\nbackground: blue;\ncolor: white;\n}\nBSOD>Static {\nwidth: 70;\n}\n#title {\ncontent-align-horizontal: center;\ntext-style: reverse;\n}\n#any-key {\ncontent-align-horizontal: center;\n}\n

BSODApp \u00a0Windows\u00a0 An\u00a0error\u00a0has\u00a0occurred.\u00a0To\u00a0continue: Press\u00a0Enter\u00a0to\u00a0return\u00a0to\u00a0Windows,\u00a0or Press\u00a0CTRL+ALT+DEL\u00a0to\u00a0restart\u00a0your\u00a0computer.\u00a0If\u00a0you\u00a0do\u00a0this, you\u00a0will\u00a0lose\u00a0any\u00a0unsaved\u00a0information\u00a0in\u00a0all\u00a0open\u00a0applications. Error:\u00a00E\u00a0:\u00a0016F\u00a0:\u00a0BFF9B3D4 Press\u00a0any\u00a0key\u00a0to\u00a0continue\u00a0_

Although both do the same thing, we recommend SCREENS for screens that exist for the lifetime of your app.

"},{"location":"guide/screens/#uninstalling-screens","title":"Uninstalling screens","text":"

Screens defined in SCREENS or added with install_screen are installed screens. Textual will keep these screens in memory for the lifetime of your app.

If you have installed a screen, but you later want it to be removed and cleaned up, you can call uninstall_screen.

"},{"location":"guide/screens/#screen-stack","title":"Screen stack","text":"

Textual apps keep a stack of screens. You can think of this screen stack as a stack of paper, where only the very top sheet is visible. If you remove the top sheet, the paper underneath becomes visible. Screens work in a similar way.

Note

You can also make parts of the top screen translucent, so that deeper screens show through. See Screen opacity.

The active screen (top of the stack) will render the screen and receive input events. The following API methods on the App class can manipulate this stack, and let you decide which screen the user can interact with.

"},{"location":"guide/screens/#push-screen","title":"Push screen","text":"

The push_screen method puts a screen on top of the stack and makes that screen active. You can call this method with the name of an installed screen, or a screen object.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nOVcXG1z2khcdTAwMTL+nl/h8n3Zq1xu2nnr6Zmturpcbk5cdTAwMWN7XHUwMDEzx/Fb/HK35ZJBgNaAMFx1MDAxMrbxVv779chcdTAwMGVcdTAwMTIvXCJgMEtyVGyMRkitmaeffrpnJn+92tjYTPqdYPO3jc3gvuI3w2rXv9t87Y7fXHUwMDA23TiM2tQk0s9x1OtW0jNcdTAwMWJJ0ol/+/XXlt+9XHUwMDBlkk7Tr1x1MDAwNN5tXHUwMDE49/xmnPSqYeRVotavYVx1MDAxMrTif7vfn/xW8K9O1KomXS+7SSmohknUfbxX0FxmWkE7ienq/6HPXHUwMDFiXHUwMDFif6W/c9ZVQ79cdTAwMTW1q+npaUPOPLSjRz9F7dRUY5hgWqFcdTAwMWGcXHUwMDEwxm/pZklQpdZcdTAwMWFcdTAwMTlcdTAwMWNkLe7QJjQv9MPZWalafuj/WTqqmebJXHUwMDE3nt21XHUwMDE2NptHSb+Z2lx1MDAxNEf0KFlbnHSj6+A0rCZccmrlI8eLvtWNevVGO4jjoe9EXHUwMDFkv1x1MDAxMiZ9d4yxwVG/XU+vkVx1MDAxZLmnT4prjzFccmgtN1x1MDAwMCx7WPd9oYzHuFVWgFx1MDAxMlpxgSOGbUVNXHUwMDFhXHUwMDA2MuxcdTAwMWYsfWWmXfmV6zrZ165m53Dw/aua1tlZd09cdTAwMGasLHjKKIkwaGpcdTAwMDRhvZE4I4z2XGZyZvOtcZCOXHUwMDAyV1ZbhsCzXHUwMDE2d9PObjWFw1x1MDAxZqP92PC7naf+2ozdh5zBztZ3o1jK4yk30sFu9fz86Fg97Fx1MDAxZVx1MDAxZX5IToKqvdy6XHUwMDFmXFxrXGJ8frdcdTAwMWLdbVx1MDAwZVq+Pv2VmdbrVP1HTHGtlWVcdTAwMTJcZkeW9XQzbF9TY7vXbGbHosp1XHUwMDA2w/To19dzg19cdFVcdTAwMDR+Qo7Qmis5O/rvoHFwvV+PXHUwMDBmPnROXHK7XHUwMDEwd1dvP/TWXHUwMDFj/YJ5wjKu6FGNNiiH4c9Re1xujVx1MDAwNcGNsoh8MfjX/CvGYIng18BcdTAwMDRYo/lqwVx1MDAxZuvz8Kh8X98qb5v7crdzqe3t4VLAb1xmXHUwMDFhLkGwZYE/XHTuk0nIXHUwMDA3XHKFyDdGXHUwMDEx+Fx0XHUwMDBmMyNfto9iXHUwMDFktMzno3q3vHuO8mRfrDnvXHUwMDFiXHSe0VJoXHUwMDAzipxcdTAwMWPsXHUwMDEw8ClcdTAwMTB4TCgujaBcdTAwMWZccrBcdTAwMTDu0Vx1MDAwMquJcdxzZsZcdTAwMDGPfFxm5lx1MDAxNH6YlIrJn4fjhbZKSJhcdTAwMDPmXHUwMDE5mqJ2clx1MDAxND5cdTAwMDQpOVxmXHUwMDFk3fZbYbM/XHUwMDA0iVx1MDAxNP9k4FGlXHUwMDFiXHUwMDA07Vxy/t/2L42wWlxy2v/MXHUwMDBmWVx1MDAxY9D93Vx1MDAwNfXwN980w7rzls1mUFx1MDAxYnajJCQpNmhOolxcXHUwMDFmV8hcdTAwMTKfLtfdrY4+UdRccuth229cdTAwMWVcdTAwMTdb9Sxv1kZcdTAwMTR7M1xigUTgXHUwMDE5/L/nzVx1MDAxNyfbd35n//D+Zlx1MDAwN45bV+clXHUwMDE5Xa+5Nyu0XHUwMDFlSoZWUawg+squ4r5P4cVcdTAwMTDgkFx0ozSSWHpcdTAwMTFvXHUwMDE2MiPMgTfnjj15s2aS5FxymiyU/uAxy1x0Nupblj3pipxZbPxCiVN41VxmJjuzgKFvrsiZ81ZNdebHbp7gzVxc5eLCqDtTTFKkj0Xm8N9z5+kjP4c7i1Fsvpg7g9RcdTAwMWVcdTAwMThtKTZLhZSaXHKrUqU8y8iVXHUwMDA1Q2lAczli2HL8XHUwMDE50JOcXiR/XHUwMDE13Vx1MDAwYsxcdTAwMDT3RuY5cVxmkqhF0082bt9it6A2inXyXHUwMDE5+Vlq51x1MDAxNHf/nkPOk0Hl7PC7STlsV8N2nVx1MDAxYTMm+VZm2J0hRqQuXFzpOSuZp4FzSYNI6Vx1MDAwNVx09ixRdV3hd5zN0mOGRFx1MDAwZadMg34o6Xo64+vAqqBd/b5N0/OvIZtcdTAwMTgpXFxOXHRccpL2s1xcXHQ7Zlx1MDAxNNmkZZrzcCYkmT5mU9OPk62o1VxuXHUwMDEz6vrPUdhORrs47cs3zs1cdTAwMWKBP8ZcdTAwMWb0TPm2UT7ouCtcdTAwMGXTevbXRuYw6YfB33+8nnh2MZbda1xmxdnlXuXf52Yyul0hkZGsZsRcdTAwMDSQucz3iGy6XHUwMDFlXUtcIjPUtYpcdTAwMGLrKkdEMyp72DTLkOhRzoeugEN5iFUjdi2HxzR6hmv6p4GowfKMTFx1MDAwNzRmXGJcdTAwMDCaXHUwMDEyaVdH4qBzicZcdTAwMTONSUtcdTAwMDJcdTAwMTJgxSSGXCLPqMsnselp61x1MDAxMGFI5ViCXHUwMDEzYl1HWZM76YnEhCdcdTAwMTEoWilwWog/l8Sml1BzNpXIKGlpXFy4pZtcdTAwMDGiVuNGecagdIqBMkgk++GHZrFSIZTT1jFcdTAwMTTPSWNTXG6F0rDRo1x1MDAwM1wi05K7bFx1MDAxNmZPsPzfXHUwMDBm3vfffLx4+yG86PcvL9tcdTAwMGbNXHUwMDAztt5cdFx1MDAxNiFcdTAwMWY8XHUwMDA1oDSjIE3cnYmhtE5cdTAwMGXCQ8s1UlJPRJZcdTAwMTNsa1ImJ7BokCBeoExeXFzLI6fjdq5cIsdz8Vx0qjBjUOT3zpjZq3kx6NPLclx1MDAxZuO9m/2y6uzU7vq4t/bwtJ6SXHUwMDAyXFzh0lx1MDAwMojhQMuN8Fx1MDAxOKN2XHUwMDEyQYyyXG47qlx1MDAwMP7eOjblXGJcdTAwMDBMw1x1MDAwYpSxp1TgrFwiPWJXXHUwMDAwTi1kXHUwMDExOEFKrTnD2UXgVrVcdTAwMWQlW5c7+zbeerhcdTAwMGXOeVJcdTAwMGab61x1MDAwZU4hPFLXRlJ4XHUwMDAyru1wdaqktYdcdTAwMTTTSGExJPDgYjLQiIrlwVx1MDAxMqkzzcCZYSueYTRBsNP7+PDQvLrQlzWZhLJ/wXJCaCz/XHUwMDE4tHx9Pe26+/H+obi77iad29Ln2t71l/v34tNcdTAwMTKuW8Wbm+B3/Vx0opCfRNt7N3uldydLuO5N+fN+VZ2etu7Ptt9WoFx1MDAwYu/fXHUwMDA0/WVV4Y1GsHJZXHUwMDFjUFSelpJcdTAwMTdcdTAwMTFcdTAwMDBSVkHaWsqZXHTg0n93/mf/rnZcdTAwMTi/3T/dub1Oro72dtY7XHUwMDBipDOMR1x1MDAxOZ5kLolcdTAwMTJsZJa1RKmFJ4hcYkmju0lYu2BcIkhZ01UwQTxBbrZ74PpqzOGVm/cmmbdSqYRcYozPXHUwMDE1jbJcdTAwMTHPSsiUT3NDbKtJoXKj7dA5g4JyhrVvXHUwMDA1Zb/T8Tq9uHFcdTAwMTmnNdxfXHUwMDFl3+TksnKupL+KsnKhbVNdsbAkI6coRZe1XHUwMDAym10oTqe8Zbhi1Y9cdTAwMWLBsoOx9ijUSlxutIYh48PB2IBcdTAwMDecXHUwMDE5amTkhsYsNvFb5IncXHUwMDEzzsW04kZxd5dcdI7JuSRSsKBpRCwl72JsJom7iVx1MDAwMiZI187vqc8vy5BuJLvnWaCQs2Omssx0ibeRL8tYo5hQSlCqRfwhclXNp1xuXGJ4Wlxu5UaUXHUwMDEz/VEnPp1QUJVcdTAwMTl+ilx1MDAxZqg0UoyotHVcZkvZ9V7l3+emXHUwMDEzZVxuQztHkETtYlx1MDAwZXE/XeusKaFcdTAwMDBcdTAwMDePUkpcdEaCXCKVn027pISiSFkjXG5cdTAwMDDNgVx1MDAxOGexuapcIkJcdTAwMTFcdTAwMWVQnm+UIHfgUptcdFx1MDAxYZ9z7qFby6WsK7vz3ErHgdJHXHUwMDFhM3jWcqpF+IS6jWfG/I18UlwiQrGCuVVBjjGktpg765FQKHhcYkB3krbaMGV/UkIpRJR7jWNpWXxC2UAhnyhcdTAwMDZMXG4+R6F1eq63pnxcIjR6qJTRmlx1MDAxMiPQw3RcIoT1pFx1MDAxNVppXHUwMDFhXHUwMDE4o5larJJVLFAsY5xuT9lcbpOcTZj6pifxSEXRXHUwMDE1LKUsbv52lE9QXHUwMDExMii1y1x1MDAxYVZCJ0ipy0vOXHUwMDFhzSFPSJxJKy2iXHUwMDA2olxuXHUwMDFh1DE6sVx1MDAxZadcdTAwMGUkkyl40Kjit6nXn41OXG5cdTAwMDGVNo5BaU46mbbEu3ihK7FcdTAwMWKSaJqjMv7pTInTqFxcOjg9tlx1MDAwZqdYPWbnX2prXnwkXHTmKTBGK0U9j7lcdTAwMTn5lE8096hcdTAwMDeMJFVohFx1MDAxNHKx0sNLbHBAXHUwMDEy+EOZ2ErKj0dcdTAwMTf7b5p7R8FJr1x1MDAwM+XD6qfT3avP0bLKbujkRcbcL1h6l4XhVEmwXHUwMDFhQc++kKxxU2p3XHUwMDBmto7fn15Hl5dxq9xTbz+sO/qBMlx1MDAxZlx1MDAwNsg5PaxQo/tcdTAwMWIs86RbdUSpXHUwMDEx0zQq6zUvxLlgXHUwMDE0RC0+I4IuMDGElph4XHUwMDFlQf5cXHSiLlxctmyF0qTF5+Dmz7+fd9iX3bPem/P6Wb9S7d10+s8qRq1cdTAwMTSdgjRcdTAwMDJ1ttRcdTAwMWElz3XH41x1MDAwNZRcdTAwMDdcXKBcdTAwMTKkXHUwMDExXHUwMDAw0C6WOy59Yki4WXVjV7wrYYF5oe9cdTAwMTKzJI3CzNKIuWg+RPDiJMcyRVx1MDAwMlxi7ezbzirJznZ8dVx1MDAxYpr7sniItpvxzVFcdTAwMTfWPckxaDw3XHUwMDExQsLEas1gWJWUXHUwMDA0+Vx1MDAwNaBUaaqthF5s31lcdTAwMTHy81x1MDAxYlCmrNjnXGYkokSxYqSrOHp7v7d3wpPjg6D8caeCeLK/XHUwMDE0pFPmzjVH8ayqy1wiS/blWi7Zl1x1MDAwYi/ZXHUwMDA3LC6DOpaXgs1cdTAwMTHKplx1MDAwZvx6TnFazTyBXHUwMDAyJMFcbpRcdTAwMWTZRq09bi0pXHUwMDE5Zlx1MDAwNXk7vkwgk5LuopVA6TJcdTAwMWGYtFx1MDAxZMcqz1xiSomsUW5cdTAwMTNcdTAwMGVcdTAwMWL3dVx1MDAwMaBcZua3XHUwMDFmr2Kp69xxJ2fHTEWL6UFiI1+0oFx1MDAwNF1cdTAwMGLLSSy7moTiuZNcdTAwMDZLXVx1MDAwNWjDnO3I3L7EpzN+tqJFqVx1MDAxMFHuNYal7HKv8u9zq1x1MDAwM1moio3klKHjXHUwMDFje/mOXHUwMDBm3vdKeHi6/eXjx/pd/fL0T41FS01cdTAwMWJ+pdHrXHUwMDA266CL0Vx1MDAxNYRAWMNcdTAwMTQoPZK1XHUwMDAxqWYrgCSElJbU68uIg9zE1jRtXHUwMDAwlMhz58Gr1Vx1MDAwNrUusWjnXGaO3zW2XHUwMDBmv1x1MDAxY4ja4bugM5s2eD3tui9a9rDWTZCtTHM8bqldXHUwMDA3nfFkyfO0XHUwMDA1isL/ocWme+D4XHUwMDFjM6zTcTNcdTAwMTdcdTAwMWasUFxcaItcdTAwMWVBR9Cjulx1MDAxZPt2ZFx1MDAxYo1cdTAwMTJcdTAwMWVoXHUwMDA13M2aUFx1MDAxZbtYXHUwMDExpzBZXHUwMDAwj9PFXHLnkrJxJifJXHUwMDBiro1cdTAwMDeIRksrXHUwMDA1XHUwMDEy6Mc20qCbu2GYXHUwMDFisZXMiTzb8WaUXHUwMDE308PMxvCuXHUwMDE1t0NDS+pAt0JcdTAwMDAmLNrgdJLmMt1qwMBcdTAwMDD8rFx1MDAwMqNcdTAwMThT7lVcdTAwMWGH05xcdTAwMTKjkFM0K+RcdTAwMTTQbjGylLNrjOkxY105XHUwMDA1085329qY0E5VjXCKJVx1MDAwNWKYNcYoXHUwMDE0L7Qm2yjPpYaoLaOsg7p9nFLIXHUwMDBlt1KNJCjRXHUwMDFiSW4xXoejhEpxQlHWef9/nFwiaFx1MDAxOK2rXHUwMDE4XHUwMDE56lx1MDAwYs3HOUW6rXCIlIQq1EaQfpzOKUVWTZ9cdTAwMDJcdTAwMWOxijFlJLOKI1GZlVx1MDAxM5jOc5UwTckxxVx1MDAwNUvZxI+9Qa9cdTAwMTDP7lVcdTAwMWGHclx1MDAxMZ29erqDW/x6lFx1MDAxMO5cdTAwMDZcdTAwMDNC0Fx1MDAwZatPSjB7zM3bMLgrT5qRSV9OeKVcdTAwMWTquChwXHUwMDBm+9fXV1//XHUwMDA3mNhRliJ9 Screen 1(hidden)Screen 2 (visible)app.push_screen(screen3)Screen 3 (visible)hidden"},{"location":"guide/screens/#action","title":"Action","text":"

You can also push screens with the \"app.push_screen\" action, which requires the name of an installed screen.

"},{"location":"guide/screens/#pop-screen","title":"Pop screen","text":"

The pop_screen method removes the top-most screen from the stack, and makes the new top screen active.

Note

The screen stack must always have at least one screen. If you attempt to remove the last screen, Textual will raise a ScreenStackError exception.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN1cXOtT20hcdTAwMTL/nr+C4r7sVcWz0z3vrbq6XHUwMDAyXHUwMDEyXHUwMDEyQnhsyObB3VZK2MLW4ddaMsZs5X+/XHUwMDFlhSDJQorBxnHiXHUwMDBmXHUwMDE4a+RRa+bX3b9+yH8/2djYTKbDcPO3jc3wqlx1MDAxOXSj1iiYbD71xy/DUVx1MDAxY1xy+jSE6ed4MFx1MDAxZTXTMztJMox/+/XXXjC6XGKTYTdohuwyisdBN07GrWjAmoPer1FcdTAwMTL24n/7v4dBL/zXcNBrJSOWXaRcdTAwMTG2omQw+nKtsFx1MDAxYvbCflx1MDAxMtPs/6HPXHUwMDFiXHUwMDFif6d/c9K1oqA36LfS09OBnHhazFx1MDAxZT1cdTAwMWP0U1FBXHUwMDBiLZW28vaEKH5GXHUwMDE3S8JcdTAwMTaNnpPAYTbiXHUwMDBmbcrRNNFcdTAwMDae897Fx119cmhcdTAwMGZOxyfZVc+jbvckmXZTmeJcdTAwMDHdSjZcdTAwMTYno8FF+D5qJVx1MDAxZH/pmeNV31x1MDAxYVxyxu1OP4zjwndcdTAwMDbDoFx1MDAxOSVTOqb47cGg306nyI5cXNGnXHUwMDA2cs6M0Vx1MDAxNqTiXHUwMDEy6G7V7fiXXHRcdTAwMDQz1lx1MDAxOFx1MDAwNUJcdTAwMWEhXHUwMDA1qFx1MDAxOcl2XHUwMDA2XdpcdTAwMDeS7Fx1MDAxZjx9ZbKdXHUwMDA1zYs2XHTYb2XngFxugrPz7JzJzf1Kp5i0Uphs+k5cdTAwMTi1O4nfIauZNcBdfjRcdTAwMGXTTXCgpJNaZlvkrzjca6Vg+HN2XHUwMDE1O8FoeLNam7H/kJPWXHUwMDBi+nxcdTAwMTZJeTTl9lm8grPdXHUwMDEwYKe1v/3X85NcdTAwMDP5+2CrfztXXHUwMDAxesFoNJhs3o58vvkvXHUwMDEzbTxsXHUwMDA1X1x1MDAxMFx1MDAwNVpLa43TXHUwMDEyTVx1MDAwNspu1L+gwf64282OXHKaXHUwMDE3XHUwMDE5XGLTo5+f3lx1MDAxYvp0mSroo+OO0KD03NBcdTAwMGbHU3ux39vnfHz+ctLeiyb6hfue0Fx1MDAwN/5N7IPTTFx1MDAxOSNRc1x1MDAwZVx1MDAwMoyyXHUwMDA17EuBXGalQYKeddo4vlx1MDAxOPbPgzPO1Vx1MDAxMrGPQipwlq9cdTAwMTb7vd45n2zx5NlhNFxmwz9eXHUwMDFlbb86iJeEfVx1MDAwYlxccG6Whf0kvEruXHUwMDAyvkVdXHUwMDA1fFx1MDAxMNZx5NLh3Mh/d941l1fDy5fT3taHwfjj8PiF2F1v5CMqprRBXHUwMDA0dEZ6XHUwMDBiWlx1MDAwML7lwMhcdTAwMDRJclxi1iHkrMBDcG+c4udYxj1wW1x1MDAwNryBWZhrgdL7pp/IxDtcdTAwMGJK2PvAPEPToJ+cRNepjbaFo7tBL+pOXHUwMDBikEjxT1x1MDAwMp40R2HY34D/9n/pRK1W2P9nfsfikK7vJ9TFb251o7bXls1ueF5UoyRcIlx1MDAxZXY7nFxmcmvcJElcdTAwMDKabrTXmr2jwShqR/2g+7Zaqlpt/rLMd6gzUVx1MDAxM5w9nNNnIHojxPz6XFy/8/fQZ5zF5uPps3HMSFx1MDAwMGm4pXdbVGfjJFx1MDAwM81cdTAwMWRcdTAwMWGU6EjjXHUwMDFmRZ1cdTAwMWQyrogwS2MsR9TuXHUwMDBl5XZMIzk6KVFcdTAwMDHXOlx1MDAwM/BXlyaFv4VcdTAwMDeoeipkjao/ijLGSTBKtqN+K+q3aTCzXCJfQ5K9OVx1MDAxY0Sqvs2xl5IzhVxcS8FpI5UgL1x1MDAwNLmT2sHQLyFcdTAwMDMgTqLJZKNVTtibXHUwMDEzPt9cblx1MDAxNfZb31x1MDAxNqk+UMmJ1OBcZml5nPV7pjhqilx1MDAxM0pCSaZcdTAwMDVIXHUwMDA3nITiTlmnSlJ1gzjZXHUwMDE59HpRQmt/PIj6yexcdTAwMWGni7nldbxcdTAwMTNcdTAwMDYl40F3lVx1MDAxZps1XHUwMDA2Qz9j0aZn/21k2pJ+uP3/z6d3nt2ohHI6WkJxNt+T/PtcdTAwMDNcdTAwMTi5sJWGXGYtqVx1MDAwNlx1MDAxMZdM8b/JyM9G2NpcdTAwMGKuw/1nW1x1MDAwN89f9LWNIVhvXlwinGOOTFx1MDAxNVxiNN5y68xcdTAwMTJ8XHRGLeNcdTAwMDLAOeK+ZCaEmJHsIcGo1suj5ESVkKJcYs5cdTAwMWaBrNRYMECFuIqIkYLtSkeLZLhcdTAwMDVwNb+jPdzpXHUwMDFjXGZfXHUwMDFjvdtcdTAwMWRcdTAwMWNfj9xhcjQ+XHUwMDFmivVcdTAwMDao5IJcdJ9cdFGWdFWqYrJEXG7NuOPGXHUwMDEyQqVcdTAwMDZcXFxmnsuOXHUwMDE3SVx1MDAxZaE4mTS7fHDWMenXZ8rufYyvOsPx+MNWdPomuVx1MDAxOMllXHUwMDA1jIQ3yHG7x4O+XHUwMDE1qlxu+k6AUkS65jfN76eHb9o9t3+0r/76NH32abtzKI+WivxWXHUwMDEwd8IlQ98xolx1MDAxZYJcdTAwMTONXHUwMDA0YpGuXHUwMDAwfaGQXHUwMDExKZFcdTAwMDY5cDBcXC9GMi02XHUwMDFkhGqZ6CdcdTAwMTNpSTS+4nSJXHLDl+PX19fds1P96VxcJJGYnvL50P+0bt5Y7U+v4+3jyejgdO/Vofj0x9SeLWHe8+H7ydvG5Ni+v+7Fp1x1MDAxN83wI3ZcdTAwMGaWMC9cdTAwMWab//VO3mLyprl9XHUwMDE1NT/uvlx1MDAxMc1oWfE0J+S5pTnAqrSRrktcdTAwMWKRWjiKvOzcNuAsnuKOONpcdTAwMDXV3u6Mmq33h+r6dL3DTGlcdTAwMWSzSkvNOZleNZMuXHUwMDA1QaNOcrKERJ3JXHUwMDE3zlxudj9cdTAwMTNA6npcdTAwMTbewc1ErqZxq/myrO9CWqIrhj+Ct6tBolx1MDAwMan1fZCYbXiW2Vx1MDAxMVx1MDAxNOWlgYfnwDZcdTAwMTdJXHUwMDE38jzZVb7meYLhkFxyXHUwMDA3w09xmln55e4sj9CF7z12lqckU63qVeZ4dHVkROxXXHUwMDEz71Zyft2rN57L0L1H8L8gmERUXHUwMDE0ZiOZOzFTq7CGIVx1MDAxN9pcYqPJXHUwMDE3w2Ip2yrd40RvXHUwMDAxkVgwXHUwMDAx06E1QpZ1XHUwMDExuPGCmpRvXHUwMDAyXHUwMDE5XHUwMDA0V9ZNQjUpTC7JvopEXHUwMDBmcFqcx0z01NO6jUJWxVEsXHUwMDBmjmyoRE2rmUsx3CRVXHUwMDE0U1x1MDAxNFx1MDAwMyuOdFx1MDAwMtpvZnqKt/EjZVtqQJWOl/GUTfkk/35vo2JyRYVZo0JwXHUwMDAx8iD3yFx1MDAxYtczp/U0Ko5LZi1ZXGZDsaySZtaoXGJmXHUwMDA0V4pWnlxmXHUwMDBizEZcdTAwMWLLMipoOWghfSmfLpIrRuVsimWSglx1MDAwZetcdTAwMWNcdTAwMTiCXG6WykSgjZXOc5PV2lx1MDAxNKCYXCJbte9oU4DRXHUwMDBl0PIopykqJlx1MDAxZVJOXHUwMDFlXHUwMDAzZ8QmrNTkQoThXFyan9SmVEPKv1x1MDAxYWU0LcuiXHUwMDE08l+zXHUwMDE5XFyurZS+nDm3SalPnaynSdHKMCO55lx1MDAxMiVIk+tk8d/XXHUwMDAyXHUwMDE4+X3iMd6x4YItXHUwMDE1VSaFdMFcdTAwMTkjyWhJWnXSiez+b02KU1xmpXbKcsFccqrcrtxYXHUwMDE0dPRdhfpcdTAwMDFcdTAwMDHEQiSFK5fJ8nCDMqt8P4FaN6r3NVx1MDAxZC5t6T3Vuq5XSldTXHUwMDA1Tv5cdTAwMTM06vmpgn6m8FX0cnLZfvfhQrevT49+j79rn+C31ZrIMzBcbj6QTCatr5lpXHUwMDE5UUAkTSluXHUwMDFkUSaXL8ivR2VGeTOvVptcZlhdJ59cdTAwMTbVXlx1MDAwN4Rwvk1s/ui4cbU/tn+1wsvOycdLXHUwMDExTl87PNhZe3Rq5uNcdTAwMDdB2DMobLFwSG6XkS9cIuRcbq2EQbdcdTAwMTA6l16YcUIgUTfzgHB4kdR0I57s7CB2I9lcdTAwMTZcdTAwMWZa4z+O48nFslKyVlpuga9cdTAwMDD7NuegS1xy3IAkjJyfcMX93dFe7+L5azHF8L1otTvDg9Z6Q79cdTAwMDHWMrCKmKXTzmlcdTAwMGVcdTAwMDXoXHUwMDBiKYlcZiuL3KLv6l1cYvlfyjLLLElcdTAwMTJHXHUwMDA0XHUwMDEy7CcpypxtPT9/XHUwMDFmbPHD9rv9t1x1MDAwN83rQZDE46X1xqK0uDSNqlxmYWpcbp2gfHVbajN/W3h92Wd9I1x1MDAxOFxupYVcdTAwMDRSXHUwMDFiXHUwMDAxekafiOiQWfHxi6b9WKzOWVx1MDAxZMBcdTAwMThFXHUwMDExlKPolYiLJZ9VVizincxnZZRcIq/mfLmjpF5cdTAwMTTqy2Jss5pcdTAwMTCG1P1BNZBl50Q448AlxXBKXHUwMDE5jeA4N3d2r/neNk3RKsWlZJBuTvjZklwijWpQfVx1MDAxOS7hKZvxSf79vnVTqWD26C071cjR3ec5k6vXoVx1MDAxY530XHUwMDFiz/u6od1+6/CV6thcboPSXHSanfEoXFxcdTAwMDNcdTAwMWZN+GJcdTAwMDZ89yTB0T/jUEy0XHUwMDFhJ1x1MDAxOHF0X1x1MDAwZVx1MDAxMMpavlD1Jlx1MDAxOVx1MDAwNf14XHUwMDE4jEhd7jAsucxpTdO9XHUwMDE0qJw1YsWM9DGfLfHZXHUwMDAy41bedI9r2XSPizfdc1fdXHJBtsOSf7zHk5P1O38vtV5dP0SDXHUwMDAySlx1MDAwNspYIynYXHUwMDE3zuqZxnuLjLyhptPQmVxccWXpWlxyilx0XHUwMDAwKYiekW1x7q5cdTAwMTJcblx1MDAwMpM+XHLCLYVMoPJcdTAwMWRaN3TBu1x1MDAwN4fwkCzJXCJ0gTyzclx1MDAwZtHLOelCvcvYKDa7k+8z5CONTOvo5bIscEaLJITl3JdR9Ndi5D1cdTAwMWLw61x1MDAxZpcsUFx1MDAxOFx1MDAxMCCNpZ3zXHUwMDE5XHUwMDAyJY0uyWRcdTAwMTjSgCR648jGXHUwMDE5xJJMP1x1MDAxMk+pXHUwMDA2s381yjheXHUwMDE2TVx1MDAxMZWJXHUwMDA0XHUwMDA0n2+mUHV+nlwiPlxcyNa1fHn54tnhm9Z04sK+qirdrFx1MDAwZk9cdTAwMDHBmSFoI7FD32ePRYOGqH1cdTAwMDXRKCAuY6VSXHUwMDBipVx1MDAxM75BVO5o8ypcdTAwMTNcdTAwMTWyXHUwMDFjUlx0IdWPw1Se1s37mFx1MDAxOVx1MDAwNLK0Wq+eXHUwMDAxXHTiXHUwMDFhl1FcdTAwMWOddcN1okBcdTAwMDWxXHUwMDFlxoFcdTAwMTRWXHUwMDA2NuAsoZK4/vxN4fVbv65cdTAwMTTIP/IglFTeLKBRYqYtXHUwMDFjkFx0Ylx1MDAxNVx1MDAxNoUherTYXHUwMDAzO7X2XHUwMDAyXHUwMDFkOUokj01Ow3IpsyvdWlx1MDAwZu3du/TJXHUwMDAwXHS0MVjOl/jSXHUwMDAxRaSr7iF5sFrOSYDqfVGRXHUwMDAwgdCeXHUwMDA3Ulx1MDAxNFxuXHUwMDE2yeHlclx1MDAwNF9cdTAwMTmQZNJvJaBcdTAwMTJcdTAwMWOMeegziPW59qJUXFxcbmPJISljtNEu94NcdTAwMWa3YllmNHFcdTAwMDPaWue4MkKWpPqRSFAlnP2rXHUwMDA05CUxIFGdqCFhUEtcdTAwMGbXue3ZyfXlKzWNj6ZHL+LB5OPWZft479O6MyCK1ohcdTAwMDGh4MSCjK9ZXHUwMDE0XHUwMDEzNUJcdTAwMThGXHUwMDExgnRElFxibvlfUPlOmVx1MDAxYSBcdTAwMWHmIFx1MDAxN1x1MDAxNfzotUPHXHUwMDA1t8LlbmiFmZo15Cm4OE8xrlqvwVx1MDAxN1xyrTXzN6/Ub/2a8lx1MDAxNFxuKsH/mFx1MDAwZjk3w41cdTAwMTC5eCFtXHUwMDEwXHUwMDAwzdAnckxcdTAwMWHai8VcdTAwMWXdrNVr7dtotPDP9/tcdTAwMDdcdTAwMDfhXHUwMDBlLdeGOeu7XHUwMDE2NddWlVx1MDAxYdP8U2zKoV3l7yQspJVz0pR6h7FRKOs4RS9OXHUwMDFiXHUwMDA1WphcXGfyRpZcdTAwMTORXGLCao1cdTAwMWH8XHUwMDEzYuWfJJiLpNT3wszIRFxcSCvyyEJq8lx1MDAxZqIkXHUwMDEzkmfx7Wv+4XY0d7X0/0hcdTAwMTSlXHUwMDEyyOlgXHUwMDExwlVcdTAwMDTlyc3s/jGhk4TwdrtcdTAwMTVcdTAwMDTpqHVjyrNb3LyMwsn2XT056cvbx3QxvVx1MDAxNVxu/Y3+/fnJ5/9cdTAwMDPV4pXXIn0= Screen 1(hidden)app.pop_screen()Screen 2(hidden)Screen 3(visible)Screen 2(visible)

When you pop a screen it will be removed and deleted unless it has been installed or there is another copy of the screen on the stack.

"},{"location":"guide/screens/#action_1","title":"Action","text":"

You can also pop screens with the \"app.pop_screen\" action.

"},{"location":"guide/screens/#switch-screen","title":"Switch screen","text":"

The switch_screen method replaces the top of the stack with a new screen.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN1cXG1T20hcdTAwMTL+nl9BcV/2qsLsTPe8btXVXHUwMDE1XHUwMDAxNlx1MDAwMVx1MDAxMpNccuH17opcdTAwMTK2bGsxsrFlXGZs5b9fjyCWLL9gg3DIKlXY1sjj1szTTz/dM8pfb1ZWVpPbTrj628pqeFNcclpRrVx1MDAxYlxmVt/689dht1x1MDAxN7VjaoL0c6/d71bTK5tJ0un99uuvl0H3XCJMOq2gXHUwMDFhsuuo11x1MDAwZlq9pF+L2qzavvw1SsLL3r/930pwXHUwMDE5/qvTvqwlXZb9yFpYi5J29/63wlZ4XHUwMDE5xkmPev9cdTAwMGZ9Xln5K/2bs65cdTAwMTZcdTAwMDWX7biWXp425MwzUDxbacepqWiElFxiKrsg6m3SjyVhjVrrZHCYtfhTq7fHrZPex6uT643+2ra4qrq7KKlmv1qPWq395LaV2tRr061kbb2k275cYo+iWtKkVlE4P+1b3Xa/0YzDXm/kO+1OUI2SWzqn+PBkXHUwMDEwN9IusjM39GlccpVgxmgrpOJSKG3VsN13IDUyY41RXHUwMDAypUGJQlx1MDAxNSzbaLdoXHUwMDFlyLJ/8PTIbDtcdTAwMGaqXHUwMDE3XHIyMK5l11xiXHUwMDE1XHUwMDA05/XsmsHD/UqnmLRcdTAwMTJN1n0zjFx1MDAxYc3Ez5DVzFx1MDAxYcFdvrVcdTAwMTemk1x1MDAwMCA5OpR22OB/sbNdS8Hwv+IoNoNu52G0Vnv+Q85ab+hWXHUwMDExSXk05eY5TCpR2N89QLt7uub0detcZuKPw75GoFx1MDAxN3S77cHqsOXbw7vMtH6nXHUwMDE23CNKaC2tdU5cIupsMltRfEGNcb/Vys61q1x1MDAxN1x1MDAxOVxi07Pf3i5cZn2Jalx1MDAxYfSFXHUwMDExllx1MDAxYm5cdTAwMTTOjf0/wi+wKW/OKp9cdTAwMGU+XHUwMDFjXHLuTreCne0vP1x1MDAxMvuCP1x1MDAwZX5pmDJGguZcXKAwelx1MDAwNPvogKHlXHUwMDFjXGJfTlx1MDAxYsefh/16cM65Klx1MDAxMftkmHBOw5LB/2Vf7Z00vsRna/31w62d/atccn5cdTAwMTmUXHUwMDAyfsdRcO2kKVx1MDAwYvxJeJNMQr6yelx1MDAxYfKNdMid4PNcdTAwMDM/6H95t3n2Z3vzc3x4dHP8YaNcIlx1MDAwZVx1MDAwZl478J1hloNBXHUwMDAx3FhrR4FvhGJIzi+ERUW+XHUwMDAxz8K9cYrXYVx1MDAxY/eC23HAXHUwMDFiUYS5UVxuLU1cdCxcdTAwMTflL0fxXHUwMDFl5dKAxlx1MDAwNVCeoalcdTAwMWQn+9FdmHLDyNnfg8uodTtcdTAwMDKJXHUwMDE0/mTgfrVcdTAwMWKG8Yr4b/xLM6rVwvif+Vx1MDAxOeuF9Pu+Qz36zfVW1PDOstpcbuujXpREpMOGzUk7N8ZVsiSg7rrbteJcdTAwMWS1u1EjioPW1+lWPc2ZXHUwMDAxi2eHYVxmOEVUoXF+bz4wx1f609ZBv9M53ZKbXHUwMDFmq+8/71x1MDAxZL52b7aOkXJDq1BobnKKNlxyYyiYoJNaodNKKlMwrFx1MDAxY29cdTAwMDbMOGTozblzXHUwMDBm3uxcZqCRSmZ38DeIWUpQzFi2N8PKL5Q2ReetcLI3g1x1MDAxYfnmkrw5b9VMb75cdTAwMWbmXHTuLCjgTPVnXHJcdTAwMTSXtMuJocf8efbML+DPxSD4gv5cZsYxdEpLZVx1MDAwNXJNideoQ2vrm41cdTAwMTDg6FJcdTAwMTC6YFo5XHUwMDFlrYk2hFBcdTAwMTKApsQ6nvHG0L9cdTAwMWRnXHUwMDE20GpDjsCdmaRRSVxccKKdJ0Tv1MxcdTAwMTn+PsMjlbJOLKJcInN2XHUwMDA03eRdXHUwMDE016K4QY1cdTAwMTmVfK8ybM9cdTAwMTElUlx1MDAxZq72vZWcXHTllPSJsybcWpVcdEs/XHUwMDE2QcdcdTAwMWLNQFhJk01aXmr/7+GKb0Ozwrj2uFGzM7CcUWucgeHcijTrp+R/kk2KQqexUjjpaO6tXHUwMDE5s6lcdTAwMTX0ko325WWU0Nh/bkdxUlx1MDAxY+N0MNe9ozfDYIxB6J7ybUVG6PhcdTAwMWVHiT17t5K5TPph+P5/bydePVx1MDAxNcv+XHUwMDE4Q3HW25v868JUZkFOT7BcdTAwMTUnW3CBPGO2XCJ9pUxmgUnUymjDSX7wQnXJSMtIXHUwMDBmWFx1MDAxNFx1MDAwMlx1MDAxMVxi/C9DZI5ZckJjuOFWTiQy5Vx1MDAxOGiN5H9cdTAwMDLISclZx5iMfIE6cFnDMojs6YnCnEQ2O3lcdTAwMWQhMs0taUhKXHRBccvR5i6651xmw1BrcjJCtsy50YIsNruGOmJcdTAwMTGXxPIkJo1cdTAwMTREU1x1MDAxM0js5+asabD1x9o4Ylx1MDAxN2StXHUwMDE5hUG0tnj2O29cdTAwMDHXXHUwMDE0yEDLjNlcdTAwMWXjrePT885e5Th+31x1MDAxZlxcRWu1TrR+dNF83Vx1MDAxOVx1MDAxNfi6oPWFXHUwMDExMFx1MDAwMimBzO72vihuXHUwMDE5+aZwzlx1MDAxMYNbmUsvn15cdTAwMTTXurzSoCNgSJJemdmlZVmPXHUwMDE1rrPg8nKFa6LGafjUToLBXFx6+Vx1MDAxODrXb+tcdTAwMDfBzqf1+LyyTTnCIN6CratS0VlcdTAwMGJ6zbBcXHgqYL5cXG2c4Vx1MDAwZYBcdTAwMTdcdTAwMTN+R0GXlI5cdTAwMDalwaJ2ZdStVYmVayFcdTAwMDWCUy+CzyFcdTAwMGJOqFx1MDAwMtTtYSVuXHUwMDFjbFx1MDAxY21tXZ9WKlx1MDAxZiuN3km/nCqAplx1MDAxMETiQC1cdTAwMDH9gtT4dFnprFEqP+WP0vOHs697h5+PzuqdXHUwMDAzPKq15Hn7Knnl9KzSlFx0lNdcdTAwMWFcblRu+S/twCGzaDSBTNNYQK75Kfi3UHVcIixz3caBXCI5vOxFS1x1MDAxYoZcdTAwMWb6XHUwMDFm7+5a56f6rI5JhLenfD70v53V77vbzul54/Bi1+3dbVx1MDAwZrqbUbR7Wi+h3+NrXHUwMDFi697B7mDQaZze1bu1qLr+R1x0/fbE+sGOXHUwMDFl3Fx1MDAwZWrrXCLYUdHazWkvLIdcdTAwMDW8KJDKubJYYFrJXHUwMDFi3dRcdTAwMDDo9Tl3Ml9Ee4xcdTAwMDFcdTAwMGX1p+jaXHUwMDFkh1eV3ubmn1j5uvm+rZ7CXHUwMDAwy0ssXHUwMDAxNVOCXCJcdTAwMWNcdTAwMTjKLa1Uo1x1MDAwYliCI6OMUyNpOGVcdTAwMTQ+s+ZccmDPw1x08kzpXHSppJxcdTAwMTDuaEpcdTAwMTRlTi9Q9J4lx5RxsFxiXHUwMDE0s1x1MDAxOc/K0mhcdTAwMTjZXHKgvVxmttqNXFwzLFJn+vd7kTrodFhvQJlb86yXVoZ/uX/BycXq3ELBMorVM6yb6Y/Ti9ZcdTAwMWPMNI90vvRHOkzM7ZCzma9cZod8XHRNqli6sMNcdH7kl06MeCRKzVxmSLBcXFhDI1JcdTAwMTRcdTAwMGLlOCQw37lUoFxyWIE4qdRjXHUwMDA1Q4da+1VlXHUwMDE0+dTtwVvRSikt8ieE5+dUeoBTQvlcdTAwMTRvnbPSM1vnreTrKs761X+Lylx1MDAxOcmlyVx1MDAxNVx1MDAxZVx1MDAxZVxuK4o5wf1FSFOluJFcdTAwMGZcdTAwMTdMKfWM3sVPVIKZjid/XHUwMDE0kZT19ib/+oRcdTAwMTWwnHuMhXdcdTAwMGUkLFx1MDAwNZ9f4M/WO6+TTYw0zO+70ki3i4iFXHUwMDA1MFx0TKVcdTAwMDOhTH7TVplUwpklia5IO5BzgsZcdFwi3zpcdTAwMDagUFJYXHUwMDE0xlx1MDAxYZlcdTAwMWKfXHUwMDA3Klx1MDAxMZo0XGKiWm7R2C836SetR5dNJWuCUTwgMVwiOWViXG5cdTAwMTByXHUwMDE33VOJZKTPNEpBI619reJvSiVrU1x1MDAwMeWPMSiVxiXcTS9cdTAwMTZcdTAwMTC1SePdaG4umZ3rvU4ukY5cdTAwMDaXK2JVXHUwMDFmzLRcdTAwMWRcdTAwMTUmNOSMa1x1MDAwZVx1MDAwNECjKVuXXHUwMDA1w8piXHUwMDEzpL7RXGItXHUwMDE1IV2bSUVcdTAwMDPHmfa0RmlcdTAwMDVcdTAwMDClNFx1MDAxM6SJcSgo21nyajrkPffHSlx1MDAxM6JcdTAwMTMgceaXRSjqar9vfTKlOO5cZo20X71cdTAwMWVfuv57UMpcZlD5Y1xmTlx1MDAwYnLKrJ3jZvqWO0cj75TUXHUwMDE57TxKKr/38fxz/Wj75FCfSLu373ai3dddgbSWM+VcdTAwMDR4tuZo5Gj5gSQzI6bn1ilcdTAwMDSH8nn7Z8tfXHUwMDFlks44Q1x1MDAxZbLkesSyloesnFx1MDAxZfJoSEg1gpj/mZ6dXHJcdTAwMGW7h4Ov8CFpVFx1MDAwNvWrvT+7W+9fOzolo2nXVlx1MDAwYidcdTAwMDGwXHUwMDEw8biXXCJCXHUwMDFiR6qMW8WfXHUwMDE38souj5NmJlx1MDAwNlEvUSxcdTAwMWJcdTAwMTLgXHUwMDEyq+NcdTAwMWLHXHUwMDFmZaRO63+Yg9v1rb3Eid8/XZRSbS7dpaZusJ7+iJyg8FwiXHSQXHUwMDBiXHUwMDE0t1x1MDAwME+qX9Q+bm13XHUwMDE0XHUwMDFjvW+a5ufaa5eQVvpcctbKbzJBa/Lrb2k6KvzOXHUwMDE3J8ibKFx1MDAxZlXuZfxcdCaloONcdTAwMWKshd/jrVx1MDAxNVx1MDAxN0t+XuLlcO6LY2ilWPrzXHUwMDEy+Cp3WOPzd1hLmPr4k/D79Ei32Pn3Jc6e+Ve5fOS0ZqBImVHex7WDwv5cdK2Ypmb/JIWw+efryvRnbVx1MDAxOGl30ipSS+B20sNQ1jFOXHUwMDEzXHUwMDAyXHUwMDAwXHUwMDBl1Eih68HXwVxuxV1+rW8ppeone+Oc+eDsXHUwMDEwMZJcdTAwMGZcbktJMeU3KFx1MDAxZFx0WpywXHUwMDA3XHUwMDEwaKa11ztaXHUwMDFhTjP+PVxyWnBb4mxcdTAwMTm4MrK5WlC+ZZVcdTAwMTbaot+GkD1dN7Rcblx1MDAxOKWupHGs81dKbse3fP9MmehULPujiOKsszf510VVicht8iqSXHUwMDE4aVx1MDAxMrA0vvPnoFti33aSRudT+6ZDaDpcYmrx2bspJNZcZqrNfjf88TpfkPDgQlxiIDCholxmf1ToXHUwMDFigcwh+ke+QPuc7zk8lnSDuNdcdLrkXHUwMDEwXHUwMDEztEkuxc20ybi058I/s/xcIlx1MDAwYuGztMmL7vuS4DRmkm9pT391w8v2dV7m/nBlktn0NF2SX+YperT0iSwp7Pk9evakL+TRS9zXXCItU9zHdK7AXHUwMDA3hsLzXHUwMDEyfl+LzzMoeZbGuGetos/0aFx1MDAwMaSB/Fx1MDAxNlNOXHUwMDFhSXOZ+48gsno1MFx1MDAwNc5vMjHoXHUwMDFmRCu6u0/wpXJmmdXq5/jjnOpkdqgoKFx1MDAwMUupo6LwbqSQOlx1MDAxYqJMnnCGXHUwMDE0XHUwMDE40Vx1MDAxMI+npamnqZPZu5hHXHUwMDE0XHUwMDEzd1xuaL6Ms1r6xYRxmyhqkDiRaK2jTFx1MDAwMIT6uZ/9mlx1MDAwZWV/rFx1MDAxNVE8TZ68efhcdTAwMDG/eWg/IcxccqeDYFx1MDAxZNVcdTAwMWWIPLvL1esoXHUwMDFjvJu0nzo9PEem4+mZKPT3+te3N9/+XHUwMDBmJe3LnyJ9 Screen 1(hidden)Screen 2 (visible)app.switch_screen(screen3)Screen 3 (visible)Screen 2 removed

Like pop_screen, if the screen being replaced is not installed it will be removed and deleted.

"},{"location":"guide/screens/#action_2","title":"Action","text":"

You can also switch screens with the \"app.switch_screen\" action which accepts the name of the screen to switch to.

"},{"location":"guide/screens/#screen-opacity","title":"Screen opacity","text":"

If a screen has a background color with an alpha component, then the background color will be blended with the screen beneath it. For example, if the top-most screen has a background set to rgba(0,0,255,0.5) then anywhere in the screen not occupied with a widget will display the second screen from the top, tinted with 50% blue.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nNVZ2VLjRlx1MDAxNH3nK1x1MDAxY+dlUlx1MDAwNZrel0mlUixhwizMXHUwMDAyXHUwMDE0Q5KplLDatsay5EhtMDPFv+dKXHUwMDE4tySvgJmFXHUwMDA3Y/dtt666zzn3XFz5y0aj0bRXXHUwMDAz03zWaJpRy4/CIPUvm5v5+IVJszCJIUSKz1kyTFvFzK61g+zZ06d9P+1cdTAwMTk7iPyW8S7CbOhHmVx1MDAxZFx1MDAwNmHitZL+09CafvZ7/nro981vg6RcdTAwMWbY1HNcdTAwMTfZMkFok/TmWiYyfVx1MDAxM9tcZlb/XHUwMDFiPjdcdTAwMWFfitdSdkHo95M4KKZcdTAwMTeBUnqK1kdcdTAwMGaTuEiVIEJcdTAwMTTmXHUwMDEyqcmMMNuDq1lcdTAwMTNAuFxyXHUwMDE5XHUwMDFiXHUwMDE3yYeaXHUwMDA3e6fPj7blaKsjRm/7r+WLbo+G7rLtMIqO7FVUJJUlcC8ultk06ZnTMLBdiOLa+Lxvpcmw041NllW+k1xm/FZor2CMo8mgXHUwMDFmd4ol3MhcYj5RwT2EhMKaXG4tKGVuO/LvXHUwMDBirDzKidKEUU0x4bW8dpNcYo5cdTAwMDHy+lx1MDAxOVx1MDAxNX8us3O/1etAenHg5mByrpRwcy7Hd8s095hiVLrluybsdG1xQMJTXHUwMDEyI12OZqY4XHUwMDAyrFx1MDAwNOFcYiniTii/5OAgKMDwsbxNcTDepnhcdTAwMThFLss88EdcdTAwMWRAZVx1MDAxMJVO99S+XHUwMDFh7n/OaHv44cxcdTAwMGbRXHQjvc7u5G4qiPPTNLlsTlwi1+N3LqPhIPBvcISF1Eoohlx1MDAxNEVu86Mw7tWTjZJWz0GvXHUwMDE4vd6cjXhrRnZcdTAwMTbctVx1MDAxNvPgzpXmikuyMti3tzvmg1x1MDAxY52/39k7XGbOXrB9/+jlm29cdHa1XGbskjOPYlx1MDAwZZCRhGvEZVx1MDAwNetKY48qXHUwMDA0jGBYYYJcdTAwMWaGdak5apNprONcdTAwMTIlJyDnuFx1MDAwZW2CqdSSUcF+cGhrJCnnWsg7QNthKIntUfj5Ro0ro/t+P4yuKkAoMFx1MDAwZlx07viZaWSt1Jj4n/jJwE9t6Edccqgx4XlkfimfWmYgl3zxkurlq2xHYSdnSzMy7SqNbFxiJWhcdTAwMTK2ycBFW5CVXHUwMDBmy6VcdTAwMDdB/e6SNOyEsVx1MDAxZlx1MDAxZK+W4UJm32z/XGZqY4llffiW21hJhDSidHV2L0bEXHUwMDFk2E1q4/dlN0ZL6S2FR6FqM62k4sLxt2C3VFDoXHUwMDE051goweRcdTAwMDMr2Vxcdlx1MDAwYk8rXHUwMDAypUpxRFx1MDAxMSup6YTrjIDKQFx1MDAwZZAoQpxxR/Ax9TlVipOy7VjOfEfpW6CQ8cj1fEFYUI1cdTAwMDT8x+w+lM0swHknjIMw7lRcdTAwMTNcdTAwMWL7tINcdTAwMTWKR0Hy1jDPclx1MDAwYnmMXHUwMDExTjCWXHUwMDFja02k5LQ0reNcdTAwMGbyrKlHJYg2lVx1MDAxNFx1MDAwYqZcdTAwMTGmU3dv4mB5Vov9Wy0rSYhcdTAwMDY0gUeUXHUwMDA03rNZWYGMc5goXHUwMDE54pzK6TOJ/MzuJv1+aGH73yZhbOvbXFzs53ZO+q7xp5RcdTAwMDXuqlx1MDAxY6urwyBfsSr+7l3D0af4MHn/cXPm7K254C6iU7h2622U/89cdTAwMTO2XHUwMDA1Jl2XqmDdpGMmKNdwXGYrK9v5q+10p3+xS+K93f1Pg97OSetcXH/nJl16YFx1MDAwZlx1MDAxNVx1MDAxMYJcdTAwMDFcdTAwMTFwzaRzykDaKEVcdTAwMWPioHO0ltfdpC2f0W6vz6QrTYG1uFR6XHUwMDFl08hkx1x1MDAwN0efgyx5d3T4qtc+XHUwMDE57tn/9vl6jFxmQUJAidGP7dFcdTAwMDWei3aMXHUwMDE1dDtSs9XR3rvAXHUwMDA3Q7/bZfzy9fut5+jw5cuT/lx1MDAxY7R3/VZ3mJrHxrtehnfCpKdcdTAwMTDCQlMmudBYVfDOoJTDXHUwMDE0qMFaXHUwMDAx9Vx1MDAxMWNcdTAwMGZcdTAwMDG8Tf04XHUwMDAzXHUwMDBmXHUwMDA26JpcdTAwMDY90WpcdTAwMWHt025cdTAwMWS6XHUwMDA1XHKtk6Lix1x1MDAwNzlXuX5/Nbd+nFxmtvpJZid+2Fx1MDAxZNGzRto595+gTbRJON9EXHUwMDFl/+XX78G+3zXl+/l5XVLMulxmXHUwMDEwiolAtESkZTKwXHUwMDE4MneSga9n6Fx1MDAxOZdcdTAwMWU4p7yJXHUwMDE0WKmyaS/qXHUwMDFlo56kXGaD+6JKXHUwMDEzVU9sfTJQ6Fx1MDAxMaRcdTAwMDCSxEB+S1x1MDAwZlx1MDAwZZytp55ASjFcZonkRdopwa1GIIrh4PSdXG7hWn19QW7KXHUwMDFm1dcvLjdVXHUwMDA3jYnWmGHwiphLrkt28tZAM3BcdTAwMWOC5s9cdTAwMWQpwlJSdT9bv9jxVZNC4HBcdTAwMTTO5VxcXG4o9nQ6K+VJXHUwMDAxglx1MDAwZolcdTAwMDMuOViCXHUwMDFm2tXPhXZcdTAwMTGsg/qOnr7Q51nahuY+hsR5h1x1MDAwMVxyk1hd20YjLsy74+fxv+/SreNR/81W/Fx1MDAxN/qWhn65slx0rj2Ru3VcZrZcdTAwMWSUy1GyeOqOmCeIgK6RsvyZ7MNcZn17lpsnTHlcdTAwMWFLaNhcdTAwMDDo4M/pXGafXHUwMDAzzPTAYyEmQHkxNFx1MDAxN072bjVccu6UcSRLT15cdTAwMWbqe5Y9XCJfh3jVyTYnsmZcdTAwMWFXYuvuzFn+qIFyUYNT/lx1MDAwN4XHQ0RcdTAwMTM4KyrhMMXS5SSUXZDbvJ/UnGJV0YRpVCxbTzFcdTAwMGauyzFcdTAwMDHTLlx1MDAxONOV5Th4JFx1MDAwNEJKsZZMwuJs2XLz9mYlRZrbdPFcdTAwMDVuXHUwMDBieMpcdTAwMDCBqyuSPbeH5oCEn9jhi1x1MDAxM3N0XHUwMDE2nqG389zWN1Mk7oHOglx1MDAwMHFcIjlUV+S+lytcdTAwMTRcdTAwMTfYg6BWgFx1MDAxYlx1MDAwNWfmXHUwMDA0u1AoXHUwMDAxVVx1MDAxMLxcZlVcdTAwMWO8XHUwMDE3zF2/QuGST3GS5LrvsVx1MDAwNFx0XHUwMDAyXaBcdTAwMDJ3+OhcbpT/kiFcdFbuXu/XXHUwMDFilaxhpTeqNjH5xvxpoihpnCZpXHUwMDE0/DSz8Sn9RvU1XHUwMDFhn0o+NzTbXHUwMDE407TpXHUwMDBmXHUwMDA2R1x1MDAxNnZrYsPgXHUwMDFjwmB8y27V5kVoLndmYyCHwcaYujlHTOGArzeu/1x1MDAwN0PH3J0ifQ== Base screen(partial visible)Top-most screenbackground: rgba(0,0,255,0.5);Hello World!

Note

Although parts of other screens may be made visible with background alpha, only the top-most is active (can respond to mouse and keyboard).

One use of background alpha is to style modal dialogs (see below).

"},{"location":"guide/screens/#modal-screens","title":"Modal screens","text":"

Screens may be used to create modal dialogs, where the main interface is temporarily disabled (but still visible) while the user is entering information.

The following example pushes a screen when you hit the Q key to ask you if you really want to quit. From the quit screen you can click either Quit to exit the app immediately, or Cancel to dismiss the screen and return to the main screen.

OutputOutput (after pressing Q)modal01.pymodal01.tcss

ModalApp \u2b58ModalApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.I\u00a0must\u00a0not\u00a0f Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2585\u2585 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.I\u00a0must\u00a0not\u00a0f Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.I\u00a0must\u00a0not\u00a0f Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. \u00a0Q\u00a0\u00a0Quit\u00a0

ModalApp \u2588\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2588 \u2588\u2588 \u2588\u2588 \u2588Are\u00a0you\u00a0sure\u00a0you\u00a0want\u00a0to\u00a0quit?\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2588 \u2588\u00a0Quit\u00a0\u00a0Cancel\u00a0\u2588 \u2588\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2588 \u2588\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2588

modal01.py
from textual.app import App, ComposeResult\nfrom textual.containers import Grid\nfrom textual.screen import Screen\nfrom textual.widgets import Button, Footer, Header, Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass QuitScreen(Screen):\n\"\"\"Screen with a dialog to quit.\"\"\"\ndef compose(self) -> ComposeResult:\nyield Grid(\nLabel(\"Are you sure you want to quit?\", id=\"question\"),\nButton(\"Quit\", variant=\"error\", id=\"quit\"),\nButton(\"Cancel\", variant=\"primary\", id=\"cancel\"),\nid=\"dialog\",\n)\ndef on_button_pressed(self, event: Button.Pressed) -> None:\nif event.button.id == \"quit\":\nself.app.exit()\nelse:\nself.app.pop_screen()\nclass ModalApp(App):\n\"\"\"An app with a modal dialog.\"\"\"\nCSS_PATH = \"modal01.tcss\"\nBINDINGS = [(\"q\", \"request_quit\", \"Quit\")]\ndef compose(self) -> ComposeResult:\nyield Header()\nyield Label(TEXT * 8)\nyield Footer()\ndef action_request_quit(self) -> None:\nself.push_screen(QuitScreen())\nif __name__ == \"__main__\":\napp = ModalApp()\napp.run()\n
modal01.tcss
QuitScreen {\nalign: center middle;\n}\n#dialog {\ngrid-size: 2;\ngrid-gutter: 1 2;\ngrid-rows: 1fr 3;\npadding: 0 1;\nwidth: 60;\nheight: 11;\nborder: thick $background 80%;\nbackground: $surface;\n}\n#question {\ncolumn-span: 2;\nheight: 1fr;\nwidth: 1fr;\ncontent-align: center middle;\n}\nButton {\nwidth: 100%;\n}\n

Note the request_quit action in the app which pushes a new instance of QuitScreen. This makes the quit screen active. If you click Cancel, the quit screen calls pop_screen to return the default screen. This also removes and deletes the QuitScreen object.

There are two flaws with this modal screen, which we can fix in the same way.

The first flaw is that the app adds a new quit screen every time you press Q, even when the quit screen is still visible. Consequently if you press Q three times, you will have to click Cancel three times to get back to the main screen. This is because bindings defined on App are always checked, and we call push_screen for every press of Q.

The second flaw is that the modal dialog doesn't look modal. There is no indication that the main interface is still there, waiting to become active again.

We can solve both those issues by replacing our use of Screen with ModalScreen. This screen sub-class will prevent key bindings on the app from being processed. It also sets a background with a little alpha to allow the previous screen to show through.

Let's see what happens when we use ModalScreen.

OutputOutput (after pressing Q)modal02.pymodal01.tcss

ModalApp \u2b58ModalApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.I\u00a0must\u00a0not\u00a0f Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2585\u2585 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.I\u00a0must\u00a0not\u00a0f Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.I\u00a0must\u00a0not\u00a0f Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. \u00a0Q\u00a0\u00a0Quit\u00a0

ModalApp \u2b58ModalApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0i\u2588\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2588 Where\u00a0the\u00a0\u2588\u2588st\u00a0not\u00a0f Fear\u00a0is\u00a0th\u2588\u2588 Fear\u00a0is\u00a0th\u2588Are\u00a0you\u00a0sure\u00a0you\u00a0want\u00a0to\u00a0quit?\u2588 I\u00a0will\u00a0fac\u2588\u2588 I\u00a0will\u00a0per\u2588\u2588\u2585\u2585 And\u00a0when\u00a0i\u2588\u2588 Where\u00a0the\u00a0\u2588\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2588st\u00a0not\u00a0f Fear\u00a0is\u00a0th\u2588\u00a0Quit\u00a0\u00a0Cancel\u00a0\u2588 Fear\u00a0is\u00a0th\u2588\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2588 I\u00a0will\u00a0fac\u2588\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2588 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.I\u00a0must\u00a0not\u00a0f Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. \u00a0Q\u00a0\u00a0Quit\u00a0

modal02.py
from textual.app import App, ComposeResult\nfrom textual.containers import Grid\nfrom textual.screen import ModalScreen\nfrom textual.widgets import Button, Footer, Header, Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass QuitScreen(ModalScreen):\n\"\"\"Screen with a dialog to quit.\"\"\"\ndef compose(self) -> ComposeResult:\nyield Grid(\nLabel(\"Are you sure you want to quit?\", id=\"question\"),\nButton(\"Quit\", variant=\"error\", id=\"quit\"),\nButton(\"Cancel\", variant=\"primary\", id=\"cancel\"),\nid=\"dialog\",\n)\ndef on_button_pressed(self, event: Button.Pressed) -> None:\nif event.button.id == \"quit\":\nself.app.exit()\nelse:\nself.app.pop_screen()\nclass ModalApp(App):\n\"\"\"An app with a modal dialog.\"\"\"\nCSS_PATH = \"modal01.tcss\"\nBINDINGS = [(\"q\", \"request_quit\", \"Quit\")]\ndef compose(self) -> ComposeResult:\nyield Header()\nyield Label(TEXT * 8)\nyield Footer()\ndef action_request_quit(self) -> None:\n\"\"\"Action to display the quit dialog.\"\"\"\nself.push_screen(QuitScreen())\nif __name__ == \"__main__\":\napp = ModalApp()\napp.run()\n
modal01.tcss
QuitScreen {\nalign: center middle;\n}\n#dialog {\ngrid-size: 2;\ngrid-gutter: 1 2;\ngrid-rows: 1fr 3;\npadding: 0 1;\nwidth: 60;\nheight: 11;\nborder: thick $background 80%;\nbackground: $surface;\n}\n#question {\ncolumn-span: 2;\nheight: 1fr;\nwidth: 1fr;\ncontent-align: center middle;\n}\nButton {\nwidth: 100%;\n}\n

Now when we press Q, the dialog is displayed over the main screen. The main screen is darkened to indicate to the user that it is not active, and only the dialog will respond to input.

"},{"location":"guide/screens/#returning-data-from-screens","title":"Returning data from screens","text":"

It is a common requirement for screens to be able to return data. For instance, you may want a screen to show a dialog and have the result of that dialog processed after the screen has been popped.

To return data from a screen, call dismiss() on the screen with the data you wish to return. This will pop the screen and invoke a callback set when the screen was pushed (with push_screen).

Let's modify the previous example to use dismiss rather than an explicit pop_screen.

modal03.pymodal01.tcss modal03.py
from textual.app import App, ComposeResult\nfrom textual.containers import Grid\nfrom textual.screen import ModalScreen\nfrom textual.widgets import Button, Footer, Header, Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass QuitScreen(ModalScreen[bool]):  # (1)!\n\"\"\"Screen with a dialog to quit.\"\"\"\ndef compose(self) -> ComposeResult:\nyield Grid(\nLabel(\"Are you sure you want to quit?\", id=\"question\"),\nButton(\"Quit\", variant=\"error\", id=\"quit\"),\nButton(\"Cancel\", variant=\"primary\", id=\"cancel\"),\nid=\"dialog\",\n)\ndef on_button_pressed(self, event: Button.Pressed) -> None:\nif event.button.id == \"quit\":\nself.dismiss(True)\nelse:\nself.dismiss(False)\nclass ModalApp(App):\n\"\"\"An app with a modal dialog.\"\"\"\nCSS_PATH = \"modal01.tcss\"\nBINDINGS = [(\"q\", \"request_quit\", \"Quit\")]\ndef compose(self) -> ComposeResult:\nyield Header()\nyield Label(TEXT * 8)\nyield Footer()\ndef action_request_quit(self) -> None:\n\"\"\"Action to display the quit dialog.\"\"\"\ndef check_quit(quit: bool) -> None:\n\"\"\"Called when QuitScreen is dismissed.\"\"\"\nif quit:\nself.exit()\nself.push_screen(QuitScreen(), check_quit)\nif __name__ == \"__main__\":\napp = ModalApp()\napp.run()\n
  1. See below for an explanation of the [bool]
modal01.tcss
QuitScreen {\nalign: center middle;\n}\n#dialog {\ngrid-size: 2;\ngrid-gutter: 1 2;\ngrid-rows: 1fr 3;\npadding: 0 1;\nwidth: 60;\nheight: 11;\nborder: thick $background 80%;\nbackground: $surface;\n}\n#question {\ncolumn-span: 2;\nheight: 1fr;\nwidth: 1fr;\ncontent-align: center middle;\n}\nButton {\nwidth: 100%;\n}\n

In the on_button_pressed message handler we call dismiss with a boolean that indicates if the user has chosen to quit the app. This boolean is passed to the check_quit function we provided when QuitScreen was pushed.

Although this example behaves the same as the previous code, it is more flexible because it has removed responsibility for exiting from the modal screen to the caller. This makes it easier for the app to perform any cleanup actions prior to exiting, for example.

Returning data in this way can help keep your code manageable by making it easy to re-use your Screen classes in other contexts.

"},{"location":"guide/screens/#typing-screen-results","title":"Typing screen results","text":"

You may have noticed in the previous example that we changed the base class to ModalScreen[bool]. The addition of [bool] adds typing information that tells the type checker to expect a boolean in the call to dismiss, and that any callback set in push_screen should also expect the same type. As always, typing is optional in Textual, but this may help you catch bugs.

"},{"location":"guide/screens/#modes","title":"Modes","text":"

Some apps may benefit from having multiple screen stacks, rather than just one. Consider an app with a dashboard screen, a settings screen, and a help screen. These are independent in the sense that we don't want to prevent the user from switching between them, even if there are one or more modal screens on the screen stack. But we may still want each individual screen to have a navigation stack where we can push and pop screens.

In Textual we can manage this with modes. A mode is simply a named screen stack, which we can switch between as required. When we switch modes, the topmost screen in the new mode becomes the active visible screen.

The following diagram illustrates such an app with modes. On startup the app switches to the \"dashboard\" mode which makes the top of the stack visible.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO2aW0/bSFx1MDAxNMff+ylQ9mVXKu7cL5VWK6ClXHUwMDA1UmhJuZRtVTn2JPHGsY3tJEDFd99jw8ZcdTAwMTdcYiSkXHUwMDA0Km1cdTAwMWWCPWfsOTPz+885M+HHi5WVRnpcdTAwMWWZxuuVhjlzbN9zY3vceJmVj0yceGFcdTAwMDAmkt8n4TB28pq9NI2S169eXHLsuG/SyLdcdTAwMWRjjbxkaPtJOnS90HLCwSsvNYPkr+x711x1MDAxZZg/o3DgprFVNLJqXFwvXHLjq7aMb1x1MDAwNiZIXHUwMDEzePvfcL+y8iP/LnnnevYgXGbcvHpuKLmnab10N1xmclcx5YpcdTAwMTOJNZrU8JI30FpqXFwwd8BjU1iyosaRjYZ7zeh78+JTW9uO2t48+/ChaLbj+X4rPfdzp5JcdTAwMTD6UtiSNFx1MDAwZfvmyHPTXtZ2rXzaU3E47PZcdTAwMDKTJJVnwsh2vPRcdTAwMWPKeOG7XHUwMDFkdPNXXHUwMDE0JWdwtyotjVx00pxcdTAwMTFOlGKMTMxXz1OLXHUwMDEwwlx1MDAwNYyT0lxuXHUwMDA2pObYRujDPIBjv6H8U7jWtp1+XHUwMDE3/Fx1MDAwYtyiXHUwMDBl5rbd7lx1MDAxNHXG190lQlpMM1x1MDAwMe1ThbVWk1x1MDAxYT3jdXtp1jvOLCWxkFxiXHUwMDBiTqUo/DD5dGioXHUwMDAwb2BsYshcdTAwMWGPttyci2/lXHUwMDExXHUwMDBi3OtcdTAwMTFcdTAwMGKGvl/4m1x1MDAxOd6WWCqeXHUwMDE5Rq59NelYwDhQxbGkolx1MDAxOCnfXHUwMDBi+vXX+aHTLzjJSy9fzo0n43wqnoogISijbGY8t9+hQ9U8+dTc2Wz7zY3Drffn0f5T4onRvXxyXHUwMDBi5lRrwpiQmFAlK3zChFtcdTAwMDIhSiSlSmjJXHUwMDE2wrNjt1x1MDAxMeKPgyehjGOt0Fx1MDAxMvBkQiMk+Vx1MDAxMvBUpaGo4amFXCLgzVx1MDAxY3SK3ogo01x1MDAxMu6J8665ftzdx1x1MDAwM3fzmdOJLYyBTC44V1xmRlx1MDAxZtEqnlhCXHUwMDA1ilx1MDAxOPArXHUwMDA1XHUwMDExdLHlU1x1MDAxMUdj8yh8YkJcdTAwMTiGJYUsXHUwMDAxUIZcdTAwMTFF0NxcdTAwMTJcdTAwMDClkkxcdTAwMDM0XHUwMDBiaowjiWdcdTAwMDd0dEC3u1EoRu/eXHUwMDFlOKnauaCO/5SA0vv4lDhcdTAwMDOUQ1dcdTAwMDVDWlx1MDAxMF6hkyNkMWCTaSGZYrTu1nxwtlxya7vtXz22g4gxXHUwMDEzVC6DzVIwuFx1MDAxMduZZJrCnM1cZufnL/2NcNPZ+4L39b67ZtZcdTAwMDej95+fNZxUUEtcbqmk1Fx1MDAwMsM3rcGpLSWIJCBhzLhaiM2OK1xyZv+zOTObnN3BJkJKc0g8Z2bT/b56vLcrm+GgXHUwMDFk+73xXHUwMDA2e+tvnjw3Ni3IXCJplkUykucuvFx1MDAwNiu3lIZcdTAwMDSTMYlcdTAwMTFcdTAwMTdcdTAwMTVWmURgXHUwMDE08Fx1MDAxNIxccqRcdTAwMDJ0IViZI0znl89CfzasqTlLbyNcdTAwMTWXwlaNVMVhaYG1Q85cZqp/SFtvdk9OTtaJcS76slx1MDAxZnL1aVxuqD3b6VxyY/P0SShcdTAwMTOWXHUwMDEwkFxcMlx1MDAwNmkoIZKzXG6cXHUwMDEySVx1MDAwYlZRyHUgxSOYL5aCTovymCuLaq6xlkAmYvImnOXk91xuR4Ipw1x1MDAxY5V20o+II+TmisyTc1x1MDAxNtNcdTAwMWVcdTAwMDZpy7vIk0ZVKd20XHUwMDA3nn9embmcU1x1MDAxOKmvXHLXTnrt0I7dr41Gxbzme90gx810qkynnmP7XHUwMDEzc1x1MDAxYUaF1YHmbC8w8ZZbdzuMva5cdTAwMTfY/ue7m4ZcdTAwMWWb95OVwiolg207MZk1z4pcdTAwMWakQqBrarzAXGa2o4ry2eOFQVx0+uLt75zzXvfirFx1MDAxZq7udFx1MDAwZbaeVobsPlx1MDAxNSpMsr0ggTVcdTAwMDfyadiD10SoLYIo5lx1MDAxYWFcYjNqsXOKaVwiXHUwMDE0ylx1MDAxMopqXGJRXGJyJlXKSZ6NXGKh92iek4lFRdgzfrR8/dVbfVTpTVx1MDAwZoDZgVx1MDAxOFwipdbuXHUwMDEz3sH2If9cdTAwMThcdTAwMWSGp7J1vrHzXHUwMDBmYmunb5vPXFx4jHKLYlx1MDAwNblcdTAwMWJHnGOlqvuIXFx5XHUwMDA0Q1qnRCa/R1xuf0RZsIvWRCDCqS6r6dlIXHUwMDBmKTlXOrao9Fx1MDAxMpOmXtBNli+/21p+TFx0lnO0mzt5yLnpPLsl0Vx1MDAxMuHx+MBcdTAwMWZ8XGLGXFydXlx1MDAxY1x1MDAwZtvjvYeJkNTKXHUwMDFmL1x0JcRCXHUwMDAy4p5cdTAwMDagQVx1MDAwNJRU41x1MDAxZtHEXHUwMDAyocLOKtNcdTAwMDfEpukqVLLD21x1MDAwZlShVlkrXHUwMDEwZJDimpaOwO9cdTAwMTChUOAz4z9vS3RtKPgpze1ol1x1MDAxY21/POqf4vFWa20zSnbtuNjqVWCz4zhcdTAwMWM3JpbL66s7XHUwMDE0rjQofJ5fpVx1MDAxNlP4mpN6I7Py+8hLvLZv/liuyqe3/jOUfjX4t0mdsHrpROqwXHUwMDA3k0rL2bebd9PwJEqX91x0PYt0QnPKXHUwMDE51Vx1MDAxNFx1MDAxM1ngklx1MDAxZqxAMOZIXHSuXGLnTEl2x1HIXHUwMDAyQkdcdTAwMTZWXHUwMDFjgStMZb9OXHUwMDEzSW45XGZhzJKIXHUwMDExgYXQWGB9U/lCQFx1MDAwZlx1MDAxOCqWqvulX2j6P1bIdcnlg6LywzWbpHacrnuBXHUwMDBika7q2PU/RGzNXHUwMDEwTXKVO8PMy1VkIcmFJJpjXGJZTFx1MDAxNucm2cjYUbbJgSqUXG5CKMKSiJtdN4FbuFTthZ2kXHUwMDFi4WDgpdD/j6FcdTAwMTek9Vx1MDAxYXmH1jLh9Yx9Q//w5rKtrtAoe2N1+S2uVlxuhvObyfW3l7fWXr2Dr+xzg6zihS/Kf7M1O2+iYUdRK4WZn0xcdTAwMTSg5rnXS27Rz8bIM+P121x1MDAwZbDzT1x1MDAxNlxy8rHOllx1MDAwNpPjePni8l9cdTAwMDSHyVx1MDAxMCJ9 \"dashboard\"\"help\"\"settings\"Active (visible)

If we later change the mode to \"settings\", the top of that mode's screen stack becomes visible.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN2a2VLbSFx1MDAxNIbv81x1MDAxNJTnNii9L6mammJfMmxcdTAwMDFcdTAwMDJkkkrJUttWLEtCkrFJinef01xuYy1gNrNlfGHsPrL6tPr7z1wi8fPN3FxcKz9PTOv9XFzLjD03XGb81Fx1MDAxZLXe2vEzk2ZBXHUwMDFjgYlcdTAwMTTfs3iYesWRvTxPsvfv3lxy3LRv8iR0PeOcXHUwMDA12dBccrN86Fx1MDAwN7HjxYN3QW5cdTAwMDbZX/Z921x1MDAxZJg/k3jg56lTTjJv/CCP019zmdBcZkyUZ3D2f+D73NzP4r3inVx1MDAxZriDOPKLw1x1MDAwYkPpXHUwMDFlJbw5ulx1MDAxZEeFq5xcdIlcdTAwMTlcIlx1MDAxM3uQLcNcXLnxwdhcdTAwMDF/TWmxQ62j3Z01s7iXu9m8TpLTlW/JsEPLSTtBXHUwMDE47ufnYeFSXHUwMDE2w0pKW5ancd9cdTAwMWNcdTAwMDV+3lx1MDAwMytujE/7VVx1MDAxYVx1MDAwZru9yGRZ7Tdx4npBfm5cdTAwMTeHJoNu1C1OUY6M7Y80dbDWmGtcIpggQtOJ2f6eKe4wXCK1XHUwMDA2XHUwMDEzZVxcNdxaikPYXHUwMDAzcOtcdTAwMGZUvErH2q7X74J3kV9cdTAwMWWDueu2O+Uxo8vFXHUwMDEyIVx1MDAxZKaZUIxRXHUwMDA1zpSz9EzQ7eXWTc5cdTAwMWMlsZBcYlx1MDAwYk6lKP0wxWbAXHUwMDAy7Fx1MDAxOVx1MDAxOJtcdTAwMTjs5MmGXzDxtXq9XCL/8npFwzAs/bWGlSZHVZYq27y/sX7a3/Hzle1w72Rk0sWTxW97k3XVwHPTNFx1MDAxZbUmlovLT6VHw8R3f1x1MDAwMYXh4iuGXHUwMDA1oVKWSIZB1G86XHUwMDFixl6/ZLBcdTAwMTi9eHtv8JlcdTAwMTLTwFx1MDAwNyooJVx1MDAwMle24jb0+3LpQ1x1MDAxYeJvx6j9PTzoxt3R9+HWK0dcdTAwMWbYXHUwMDE2XHUwMDAwNiGMXHUwMDEy1Fx1MDAwMJ9cdEchLlx1MDAwNVx1MDAxMoLCzrCZyO+4bYT405BPQJewUejRyH9ccmxqoqexSTVBmHBy96jMR8nR1tjfpOM12U+Xu0ujT/HglaOpXHUwMDFkiLlSaURcdTAwMTmSVNbYpGClWDFFuCRIK8ZnglNcdTAwMTFPY/MkcGKQXHUwMDE2xoqQ/1x1MDAxN520kiWbJYOWmFx1MDAxM87Znek88Vx1MDAwZdaOt7zOp7PNY7W1tDFYWF7cfkk62W10akxcdTAwMWNCXHUwMDA0x5hSpKSqwVx0VDpcdTAwMWHUXHTVhKRcdTAwMTji60xstlxya/vt36RkuFx1MDAxMU0hlHpcdTAwMTY01TQ0MVKwYCwqaf82NlPlkf7ybrhcdTAwMWLujE829lx1MDAwZvn6SftF61mMboOTXHUwMDBiu+1cdTAwMWFTXHUwMDBiqNSkXHUwMDFlOpkmXHUwMDBlXHUwMDEyXG5ziVx1MDAxONdcdTAwMTQ1/bpnWvelwez3p1x1MDAxM1xuXHUwMDFjhckz0Mn59F6LXG6GIKKgO8OZeWp1NzvrbVxmP6ydjr6vXHUwMDA0KplfeXVwOohcdTAwMTIoppVgRFx1MDAwYlx1MDAwNVA2aJVcdTAwMGXCSENcdTAwMDcmXHUwMDEwpFx1MDAwZVanlUNzhlxixlx1MDAwMlxuXHUwMDFlTORsRSjzhOn8XHUwMDBmitDHpTU34/w6VGGSqYGUI4FcdTAwMTnn8u7dUZet7+xcdTAwMWRcdTAwMWStRHtYXHUwMDFk8uHpeEzaXHUwMDBiU1jtuV5vmJpcdTAwMTdP84QpXHUwMDA3NpxcdMKVRErX2Vx1MDAxNLZcYoXuiCPIKZg+UZ7HXFw5VHONtVx1MDAwNDBcdTAwMTGTV9mkvEkjwZTZPVwiz0GjJlCG03vQWG56XHUwMDFj5fvBXHUwMDBme+GJqo2uuoMgPK/tW4EpXFypLy3fzXrt2E39L61WzbxcdTAwMTBcdTAwMDZdS24rNJ060nngueHEnMdJafVgOjeITLrhN92O06BcdTAwMWJEbnhw89SwYrM+XHRcdTAwMTRO5W5a282MtdpcdTAwMDXyXHUwMDA3ibBcdTAwMWHymlwiXHUwMDE0UHpcIoird+9cdTAwMDPnPZxcdTAwMWZcdTAwMWZ8Xo/ib5tcdTAwMWZHW6uHy8qLXrlcYjFEXFyHKi6FgFpcdTAwMDZcdTAwMTFdr2ckkkWG4IgwTrSa7Vx1MDAwNt00XHUwMDE1XG7lXGJFNWPgXHUwMDAwZqpSlLxcdTAwMWFcdTAwMTVCI4Lv0/rNqsKeXHST51x1MDAxN2Bz1ifVnsDN0Yn2JKOSQ69992Ltx+r4cIVcdTAwMDdq6cTdPmDR2s5cdTAwMGZ/8PFltXd7LyEocygniDJbc6Ayxf2SXHUwMDFlNLpSI6lcdTAwMTDngqjZWompXHSQKEdcdTAwMGLoY1x1MDAwNII0o6tyejXaI5yx+9Rjs2ovM3lcdTAwMWVE3ez59XfdzE+pQYxlc7TMf1RqXHUwMDAxjcHd85//cXfps4/mN8+S4OPSh8V2XHUwMDEwz88/TIOkMf6EXHUwMDFhZNpB0MkjibWwbVJNhERxh2PMidJQqaLqfbcrKlSyw9tcdTAwMGZToYI4QDn0b1xiQ9yrdlx1MDAxZjeIUEDJXGZtwuP1RJeGa1x1MDAxZknhwd/n/vKmiFx1MDAwZldcdTAwMGU6cZxiubpQ3pmowXb/R1Ka2j/PpvBcdTAwMDUvXHUwMDBmzszzars552Oo+teFvk7WeuqTN4K0JETf48HbzTv/XCKqlreK2lxuXG60LyhcdTAwMTSuiuD6LWRcbjlPSlvTaY5cdTAwMTWYb3i+MYOoqVx1MDAwMz08gTDKtOQgVVROM1G11I5iXHUwMDFjXHUwMDBlXHUwMDAxzdubhldFblx1MDAxZlx1MDAwZlx1MDAxMlapXHJuV3kp3/9QIZcjXHUwMDE3XHUwMDBmLH5cdTAwMWYqzyx303wxiHxIanXHLv+lYuNcdTAwMGWJo1x1MDAxMLQ3tF5cIlx1MDAwNyvrk6BcblOoQVx1MDAxMKGVo7puYmOpXHUwMDAztVxmWFx1MDAxMId2XHUwMDFlXHUwMDEyXHUwMDE4u7J2XHUwMDEz+aVP9WW4Wb5cdTAwMTRcdTAwMGZcdTAwMDZBXHUwMDBlXHUwMDE3YDdcdTAwMGWivHlEsaJcdTAwMDUrvJ5xr6hcdTAwMWXOXFy1NVx1MDAxNZrYM9ZDbflprmS4+DL5/PXttUdPx8u+roBVnu5N9a+NzsVcdTAwMDQtN0n2c9j4yT5cdTAwMDFpgX9cdTAwMTlcXMtVts5cdTAwMDIzWrzuZnXxsnG/uNI2MJiCxos3XHUwMDE3/1x1MDAwMiHc1HkifQ== \"dashboard\"\"help\"\"settings\"Active

To add modes to your app, define a MODES class variable in your App class which should be a dict that maps the name of the mode on to either a screen object, a callable that returns a screen, or the name of an installed screen. However you specify it, the values in MODES set the base screen for each mode's screen stack.

You can switch between these screens at any time by calling App.switch_mode. When you switch to a new mode, the topmost screen in the new stack becomes visible. Any calls to App.push_screen or App.pop_screen will affect only the active mode.

Let's look at an example with modes:

modes01.pyOutputOutput (after pressing S)
from textual.app import App, ComposeResult\nfrom textual.screen import Screen\nfrom textual.widgets import Footer, Placeholder\nclass DashboardScreen(Screen):\ndef compose(self) -> ComposeResult:\nyield Placeholder(\"Dashboard Screen\")\nyield Footer()\nclass SettingsScreen(Screen):\ndef compose(self) -> ComposeResult:\nyield Placeholder(\"Settings Screen\")\nyield Footer()\nclass HelpScreen(Screen):\ndef compose(self) -> ComposeResult:\nyield Placeholder(\"Help Screen\")\nyield Footer()\nclass ModesApp(App):\nBINDINGS = [\n(\"d\", \"switch_mode('dashboard')\", \"Dashboard\"),  # (1)!\n(\"s\", \"switch_mode('settings')\", \"Settings\"),\n(\"h\", \"switch_mode('help')\", \"Help\"),\n]\nMODES = {\n\"dashboard\": DashboardScreen,  # (2)!\n\"settings\": SettingsScreen,\n\"help\": HelpScreen,\n}\ndef on_mount(self) -> None:\nself.switch_mode(\"dashboard\")  # (3)!\nif __name__ == \"__main__\":\napp = ModesApp()\napp.run()\n
  1. switch_mode is a builtin action to switch modes.
  2. Associates DashboardScreen with the name \"dashboard\".
  3. Switches to the dashboard mode.

ModesApp Dashboard\u00a0Screen \u00a0D\u00a0\u00a0Dashboard\u00a0\u00a0S\u00a0\u00a0Settings\u00a0\u00a0H\u00a0\u00a0Help\u00a0

ModesApp Settings\u00a0Screen \u00a0D\u00a0\u00a0Dashboard\u00a0\u00a0S\u00a0\u00a0Settings\u00a0\u00a0H\u00a0\u00a0Help\u00a0

Here we have defined three screens. One for a dashboard, one for settings, and one for help. We've bound keys to each of these screens, so the user can switch between the screens.

Pressing D, S, or H switches between these modes.

"},{"location":"guide/styles/","title":"Styles","text":"

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

"},{"location":"guide/styles/#styles-object","title":"Styles object","text":"

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\nclass ScreenApp(App):\ndef on_mount(self) -> None:\nself.screen.styles.background = \"darkblue\"\nself.screen.styles.border = (\"heavy\", \"white\")\nif __name__ == \"__main__\":\napp = ScreenApp()\napp.run()\n

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 \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

"},{"location":"guide/styles/#styling-widgets","title":"Styling widgets","text":"

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\nfrom textual.widgets import Static\nclass WidgetApp(App):\ndef compose(self) -> ComposeResult:\nself.widget = Static(\"Textual\")\nyield self.widget\ndef on_mount(self) -> None:\nself.widget.styles.background = \"darkblue\"\nself.widget.styles.border = (\"heavy\", \"white\")\nif __name__ == \"__main__\":\napp = WidgetApp()\napp.run()\n

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 \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Textual\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

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.

"},{"location":"guide/styles/#colors","title":"Colors","text":"

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\"\n

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\nfrom textual.color import Color\nfrom textual.widgets import Static\nclass ColorApp(App):\ndef compose(self) -> ComposeResult:\nself.widget1 = Static(\"Textual One\")\nyield self.widget1\nself.widget2 = Static(\"Textual Two\")\nyield self.widget2\nself.widget3 = Static(\"Textual Three\")\nyield self.widget3\ndef on_mount(self) -> None:\nself.widget1.styles.background = \"#9932CC\"\nself.widget2.styles.background = \"hsl(150,42.9%,49.4%)\"\nself.widget2.styles.color = \"blue\"\nself.widget3.styles.background = Color(191, 78, 96)\nif __name__ == \"__main__\":\napp = ColorApp()\napp.run()\n

Here is the output:

ColorApp Textual\u00a0One Textual\u00a0Two Textual\u00a0Three

"},{"location":"guide/styles/#alpha","title":"Alpha","text":"

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\nfrom textual.color import Color\nfrom textual.widgets import Static\nclass ColorApp(App):\ndef compose(self) -> ComposeResult:\nself.widgets = [Static(\"\") for n in range(10)]\nyield from self.widgets\ndef on_mount(self) -> None:\nfor index, widget in enumerate(self.widgets, 1):\nalpha = index * 0.1\nwidget.update(f\"alpha={alpha:.1f}\")\nwidget.styles.background = Color(191, 78, 96, a=alpha)\nif __name__ == \"__main__\":\napp = ColorApp()\napp.run()\n

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

"},{"location":"guide/styles/#dimensions","title":"Dimensions","text":"

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

"},{"location":"guide/styles/#box-model","title":"Box Model","text":"

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"},{"location":"guide/styles/#width-and-height","title":"Width and height","text":"

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\nfrom textual.widgets import Static\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass DimensionsApp(App):\ndef compose(self) -> ComposeResult:\nself.widget = Static(TEXT)\nyield self.widget\ndef on_mount(self) -> None:\nself.widget.styles.background = \"purple\"\nself.widget.styles.width = 30\nself.widget.styles.height = 10\nif __name__ == \"__main__\":\napp = DimensionsApp()\napp.run()\n

This code produces the following result.

DimensionsApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0 brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0 me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0 will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see its\u00a0path.

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

"},{"location":"guide/styles/#auto-dimensions","title":"Auto dimensions","text":"

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\nfrom textual.widgets import Static\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass DimensionsApp(App):\ndef compose(self) -> ComposeResult:\nself.widget = Static(TEXT)\nyield self.widget\ndef on_mount(self) -> None:\nself.widget.styles.background = \"purple\"\nself.widget.styles.width = 30\nself.widget.styles.height = \"auto\"\nif __name__ == \"__main__\":\napp = DimensionsApp()\napp.run()\n

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

DimensionsApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0 brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0 me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0 will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0 will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0 remain.

"},{"location":"guide/styles/#units","title":"Units","text":"

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\nfrom textual.widgets import Static\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass DimensionsApp(App):\ndef compose(self) -> ComposeResult:\nself.widget = Static(TEXT)\nyield self.widget\ndef on_mount(self) -> None:\nself.widget.styles.background = \"purple\"\nself.widget.styles.width = \"50%\"\nself.widget.styles.height = \"80%\"\nif __name__ == \"__main__\":\napp = DimensionsApp()\napp.run()\n

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

60 x 2080 x 30120 x 40

DimensionsApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0 brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0 me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0 will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0 will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0 remain.

DimensionsApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0 total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0 through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0 the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0 nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.

DimensionsApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0 its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0 remain.

"},{"location":"guide/styles/#fr-units","title":"FR units","text":"

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\nfrom textual.widgets import Static\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass DimensionsApp(App):\ndef compose(self) -> ComposeResult:\nself.widget1 = Static(TEXT)\nyield self.widget1\nself.widget2 = Static(TEXT)\nyield self.widget2\ndef on_mount(self) -> None:\nself.widget1.styles.background = \"purple\"\nself.widget2.styles.background = \"darkgreen\"\nself.widget1.styles.height = \"2fr\"\nself.widget2.styles.height = \"1fr\"\nif __name__ == \"__main__\":\napp = DimensionsApp()\napp.run()\n

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\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain. I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.

"},{"location":"guide/styles/#maximum-and-minimums","title":"Maximum and minimums","text":"

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.

  • min-width sets a minimum width.
  • max-width sets a maximum width.
  • min-height sets a minimum height.
  • max-height sets a maximum height.
"},{"location":"guide/styles/#padding","title":"Padding","text":"

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\nfrom textual.widgets import Static\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass PaddingApp(App):\ndef compose(self) -> ComposeResult:\nself.widget = Static(TEXT)\nyield self.widget\ndef on_mount(self) -> None:\nself.widget.styles.background = \"purple\"\nself.widget.styles.width = 30\nself.widget.styles.padding = 2\nif __name__ == \"__main__\":\napp = PaddingApp()\napp.run()\n

Notice the additional space around the text:

PaddingApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0 that\u00a0brings\u00a0total\u00a0 obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0 over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past, I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0 to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0 there\u00a0will\u00a0be\u00a0nothing.\u00a0 Only\u00a0I\u00a0will\u00a0remain.

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\nfrom textual.widgets import Static\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass PaddingApp(App):\ndef compose(self) -> ComposeResult:\nself.widget = Static(TEXT)\nyield self.widget\ndef on_mount(self) -> None:\nself.widget.styles.background = \"purple\"\nself.widget.styles.width = 30\nself.widget.styles.padding = (2, 4)\nif __name__ == \"__main__\":\napp = PaddingApp()\napp.run()\n

Compare the output of this example to the previous example:

PaddingApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0 mind-killer. Fear\u00a0is\u00a0the\u00a0 little-death\u00a0that\u00a0 brings\u00a0total\u00a0 obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0 pass\u00a0over\u00a0me\u00a0and\u00a0 through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0 past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0 inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0 path. Where\u00a0the\u00a0fear\u00a0has\u00a0 gone\u00a0there\u00a0will\u00a0be\u00a0 nothing.\u00a0Only\u00a0I\u00a0will\u00a0 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.

"},{"location":"guide/styles/#border","title":"Border","text":"

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\nfrom textual.widgets import Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass BorderApp(App):\ndef compose(self) -> ComposeResult:\nself.widget = Label(TEXT)\nyield self.widget\ndef on_mount(self) -> None:\nself.widget.styles.background = \"darkblue\"\nself.widget.styles.width = \"50%\"\nself.widget.styles.border = (\"heavy\", \"yellow\")\nif __name__ == \"__main__\":\napp = BorderApp()\napp.run()\n

Here is the result:

BorderApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503I\u00a0must\u00a0not\u00a0fear.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0mind-killer.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0\u2503 \u2503total\u00a0obliteration.\u2503 \u2503I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2503 \u2503I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0\u2503 \u2503through\u00a0me.\u2503 \u2503And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0\u2503 \u2503the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.\u2503 \u2503Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0\u2503 \u2503nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

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

textual borders\n
"},{"location":"guide/styles/#title-alignment","title":"Title alignment","text":"

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\".

  • border-title-align sets the alignment of the title, which defaults to \"left\".
  • border-subtitle-align sets the alignment of the subtitle, which defaults to \"right\".

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

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass BorderTitleApp(App[None]):\ndef compose(self) -> ComposeResult:\nself.widget = Static(TEXT)\nyield self.widget\ndef on_mount(self) -> None:\nself.widget.styles.background = \"darkblue\"\nself.widget.styles.width = \"50%\"\nself.widget.styles.border = (\"heavy\", \"yellow\")\nself.widget.border_title = \"Litany Against Fear\"\nself.widget.border_subtitle = \"by Frank Herbert, in \u201cDune\u201d\"\nself.widget.styles.border_title_align = \"center\"\nif __name__ == \"__main__\":\napp = BorderTitleApp()\napp.run()\n

Note the addition of the titles and their alignments:

BorderTitleApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0Litany\u00a0Against\u00a0Fear\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503I\u00a0must\u00a0not\u00a0fear.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0mind-killer.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0\u2503 \u2503total\u00a0obliteration.\u2503 \u2503I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2503 \u2503I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0\u2503 \u2503through\u00a0me.\u2503 \u2503And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0\u2503 \u2503the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.\u2503 \u2503Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0\u2503 \u2503nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0by\u00a0Frank\u00a0Herbert,\u00a0in\u00a0\u201cDune\u201d\u00a0\u2501\u251b

"},{"location":"guide/styles/#outline","title":"Outline","text":"

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\nfrom textual.widgets import Static\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass OutlineApp(App):\ndef compose(self) -> ComposeResult:\nself.widget = Static(TEXT)\nyield self.widget\ndef on_mount(self) -> None:\nself.widget.styles.background = \"darkblue\"\nself.widget.styles.width = \"50%\"\nself.widget.styles.outline = (\"heavy\", \"yellow\")\nif __name__ == \"__main__\":\napp = OutlineApp()\napp.run()\n

Notice how the outline overlaps the text in the widget.

OutlineApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503ear\u00a0is\u00a0the\u00a0mind-killer.\u2503 \u2503ear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0\u2503 \u2503otal\u00a0obliteration.\u2503 \u2503\u00a0will\u00a0face\u00a0my\u00a0fear.\u2503 \u2503\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0\u2503 \u2503hrough\u00a0me.\u2503 \u2503nd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0\u2503 \u2503he\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.\u2503 \u2503here\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

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

"},{"location":"guide/styles/#box-sizing","title":"Box sizing","text":"

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 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 to the box model for border-box.

content-boxborder-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\nfrom textual.widgets import Static\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass BoxSizing(App):\ndef compose(self) -> ComposeResult:\nself.widget1 = Static(TEXT)\nyield self.widget1\nself.widget2 = Static(TEXT)\nyield self.widget2\ndef on_mount(self) -> None:\nself.widget1.styles.background = \"purple\"\nself.widget2.styles.background = \"darkgreen\"\nself.widget1.styles.width = 30\nself.widget2.styles.width = 30\nself.widget1.styles.height = 6\nself.widget2.styles.height = 6\nself.widget1.styles.border = (\"heavy\", \"white\")\nself.widget2.styles.border = (\"heavy\", \"white\")\nself.widget1.styles.padding = 1\nself.widget2.styles.padding = 1\nself.widget2.styles.box_sizing = \"content-box\"\nif __name__ == \"__main__\":\napp = BoxSizing()\napp.run()\n

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 \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503I\u00a0must\u00a0not\u00a0fear.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0mind-killer.\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503I\u00a0must\u00a0not\u00a0fear.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0mind-killer.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0\u2503 \u2503brings\u00a0total\u00a0obliteration.\u2503 \u2503I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2503 \u2503I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

"},{"location":"guide/styles/#margin","title":"Margin","text":"

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\nfrom textual.widgets import Static\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass MarginApp(App):\ndef compose(self) -> ComposeResult:\nself.widget1 = Static(TEXT)\nyield self.widget1\nself.widget2 = Static(TEXT)\nyield self.widget2\ndef on_mount(self) -> None:\nself.widget1.styles.background = \"purple\"\nself.widget2.styles.background = \"darkgreen\"\nself.widget1.styles.border = (\"heavy\", \"white\")\nself.widget2.styles.border = (\"heavy\", \"white\")\nself.widget1.styles.margin = 2\nself.widget2.styles.margin = 2\nif __name__ == \"__main__\":\napp = MarginApp()\napp.run()\n

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

MarginApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503I\u00a0must\u00a0not\u00a0fear.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0mind-killer.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2503 \u2503I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2503 \u2503I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2503 \u2503And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.\u2503 \u2503Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503I\u00a0must\u00a0not\u00a0fear.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0mind-killer.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2503 \u2503I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2503 \u2503I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2503 \u2503And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.\u2503 \u2503Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

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.

"},{"location":"guide/styles/#more-styles","title":"More styles","text":"

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.

"},{"location":"guide/testing/","title":"Testing","text":"

Code testing is an important part of software development. This chapter will cover how to write tests for your Textual apps.

"},{"location":"guide/testing/#what-is-testing","title":"What is testing?","text":"

It is common to write tests alongside your app. A test is simply a function that confirms your app is working correctly.

Learn more about testing

We recommend Python Testing with pytest for a comprehensive guide to writing tests.

"},{"location":"guide/testing/#do-you-need-to-write-tests","title":"Do you need to write tests?","text":"

The short answer is \"no\", you don't need to write tests.

In practice however, it is almost always a good idea to write tests. Writing code that is completely bug free is virtually impossible, even for experienced developers. If you want to have confidence that your application will run as you intended it to, then you should write tests. Your test code will help you find bugs early, and alert you if you accidentally break something in the future.

"},{"location":"guide/testing/#testing-frameworks-for-textual","title":"Testing frameworks for Textual","text":"

Textual doesn't require any particular test framework. You can use any test framework you are familiar with, but we will be using pytest in this chapter.

"},{"location":"guide/testing/#testing-apps","title":"Testing apps","text":"

You can often test Textual code in the same way as any other app, and use similar techniques. But when testing user interface interactions, you may need to use Textual's dedicated test features.

Let's write a simple Textual app so we can demonstrate how to test it. The following app shows three buttons labelled \"red\", \"green\", and \"blue\". Clicking one of those buttons or pressing a corresponding R, G, and B key will change the background color.

rgb.pyOutput
from textual import on\nfrom textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.widgets import Button, Footer\nclass RGBApp(App):\nCSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    Horizontal {\n        width: auto;\n        height: auto;\n    }\n    \"\"\"\nBINDINGS = [\n(\"r\", \"switch_color('red')\", \"Go Red\"),\n(\"g\", \"switch_color('green')\", \"Go Green\"),\n(\"b\", \"switch_color('blue')\", \"Go Blue\"),\n]\ndef compose(self) -> ComposeResult:\nwith Horizontal():\nyield Button(\"Red\", id=\"red\")\nyield Button(\"Green\", id=\"green\")\nyield Button(\"Blue\", id=\"blue\")\nyield Footer()\n@on(Button.Pressed)\ndef pressed_button(self, event: Button.Pressed) -> None:\nassert event.button.id is not None\nself.action_switch_color(event.button.id)\ndef action_switch_color(self, color: str) -> None:\nself.screen.styles.background = color\nif __name__ == \"__main__\":\napp = RGBApp()\napp.run()\n

RGBApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Red\u00a0\u00a0Green\u00a0\u00a0Blue\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u00a0R\u00a0\u00a0Go\u00a0Red\u00a0\u00a0G\u00a0\u00a0Go\u00a0Green\u00a0\u00a0B\u00a0\u00a0Go\u00a0Blue\u00a0

Although it is straightforward to test an app like this manually, it is not practical to click every button and hit every key in your app after changing a single line of code. Tests allow us to automate such testing so we can quickly simulate user interactions and check the result.

To test our simple app we will use the run_test() method on the App class. This replaces the usual call to run() and will run the app in headless mode, which prevents Textual from updating the terminal but otherwise behaves as normal.

The run_test() method is an async context manager which returns a Pilot object. You can use this object to interact with the app as if you were operating it with a keyboard and mouse.

Let's look at the tests for the example above:

test_rgb.py
from rgb import RGBApp\nfrom textual.color import Color\nasync def test_keys():  # (1)!\n\"\"\"Test pressing keys has the desired result.\"\"\"\napp = RGBApp()\nasync with app.run_test() as pilot:  # (2)!\n# Test pressing the R key\nawait pilot.press(\"r\")  # (3)!\nassert app.screen.styles.background == Color.parse(\"red\")  # (4)!\n# Test pressing the G key\nawait pilot.press(\"g\")\nassert app.screen.styles.background == Color.parse(\"green\")\n# Test pressing the B key\nawait pilot.press(\"b\")\nassert app.screen.styles.background == Color.parse(\"blue\")\n# Test pressing the X key\nawait pilot.press(\"x\")\n# No binding (so no change to the color)\nassert app.screen.styles.background == Color.parse(\"blue\")\nasync def test_buttons():\n\"\"\"Test pressing keys has the desired result.\"\"\"\napp = RGBApp()\nasync with app.run_test() as pilot:\n# Test clicking the \"red\" button\nawait pilot.click(\"#red\")  # (5)!\nassert app.screen.styles.background == Color.parse(\"red\")\n# Test clicking the \"green\" button\nawait pilot.click(\"#green\")\nassert app.screen.styles.background == Color.parse(\"green\")\n# Test clicking the \"blue\" button\nawait pilot.click(\"#blue\")\nassert app.screen.styles.background == Color.parse(\"blue\")\n
  1. The run_test() method requires that it run in a coroutine, so tests must use the async keyword.
  2. This runs the app and returns a Pilot instance we can use to interact with it.
  3. Simulates pressing the R key.
  4. This checks that pressing the R key has resulted in the background color changing.
  5. Simulates clicking on the widget with an id of red (the button labelled \"Red\").

There are two tests defined in test_rgb.py. The first to test keys and the second to test button clicks. Both tests first construct an instance of the app and then call run_test() to get a Pilot object. The test_keys function simulates key presses with Pilot.press, and test_buttons simulates button clicks with Pilot.click.

After simulating a user interaction, Textual tests will typically check the state has been updated with an assert statement. The pytest module will record any failures of these assert statements as a test fail.

If you run the tests with pytest test_rgb.py you should get 2 passes, which will confirm that the user will be able to click buttons or press the keys to change the background color.

If you later update this app, and accidentally break this functionality, one or more of your tests will fail. Knowing which test has failed will help you quickly track down where your code was broken.

"},{"location":"guide/testing/#simulating-key-presses","title":"Simulating key presses","text":"

We've seen how the press method simulates keys. You can also supply multiple keys to simulate the user typing in to the app. Here's an example of simulating the user typing the word \"hello\".

await pilot.press(\"h\", \"e\", \"l\", \"l\", \"o\")\n

Each string creates a single keypress. You can also use the name for non-printable keys (such as \"enter\") and the \"ctrl+\" modifier. These are the same identifiers as used for key events, which you can experiment with by running textual keys.

"},{"location":"guide/testing/#simulating-clicks","title":"Simulating clicks","text":"

You can simulate mouse clicks in a similar way with Pilot.click. If you supply a CSS selector Textual will simulate clicking on the matching widget.

Note

If there is another widget in front of the widget you want to click, you may end up clicking the topmost widget rather than the widget indicated in the selector. This is generally what you want, because a real user would experience the same thing.

"},{"location":"guide/testing/#clicking-the-screen","title":"Clicking the screen","text":"

If you don't supply a CSS selector, then the click will be relative to the screen. For example, the following simulates a click at (0, 0):

await pilot.click()\n
"},{"location":"guide/testing/#click-offsets","title":"Click offsets","text":"

If you supply an offset value, it will be added to the coordinates of the simulated click. For example the following line would simulate a click at the coordinates (10, 5).

await pilot.click(offset=(10, 5))\n

If you combine this with a selector, then the offset will be relative to the widget. Here's how you would click the line above a button.

await pilot.click(Button, offset(0, -1))\n
"},{"location":"guide/testing/#modifier-keys","title":"Modifier keys","text":"

You can simulate clicks in combination with modifier keys, by setting the shift, meta, or control parameters. Here's how you could simulate ctrl-clicking a widget with an ID of \"slider\":

await pilot.click(\"#slider\", control=True)\n
"},{"location":"guide/testing/#changing-the-screen-size","title":"Changing the screen size","text":"

The default size of a simulated app is (80, 24). You may want to test what happens when the app has a different size. To do this, set the size parameter of run_test to a different size. For example, here is how you would simulate a terminal resized to 100 columns and 50 lines:

async with app.run_test(size=(100, 50)) as pilot:\n...\n
"},{"location":"guide/testing/#pausing-the-pilot","title":"Pausing the pilot","text":"

Some actions in a Textual app won't change the state immediately. For instance, messages may take a moment to bubble from the widget that sent them. If you were to post a message and immediately assert you may find that it fails because the message hasn't yet been processed.

You can generally solve this by calling pause() which will wait for all pending messages to be processed. You can also supply a delay parameter, which will insert a delay prior to waiting for pending messages.

"},{"location":"guide/testing/#textuals-tests","title":"Textual's tests","text":"

Textual itself has a large battery of tests. If you are interested in how we write tests, see the tests/ directory in the Textual repository.

"},{"location":"guide/testing/#snapshot-testing","title":"Snapshot testing","text":"

A snapshot is a record of what an application looked like at a given point in time.

Snapshot testing is the process of creating a snapshot of an application while a test runs, and comparing it to a historical version. If there's a mismatch, the snapshot testing framework flags it for review.

This offers a simple, automated way of checking our application displays like we expect.

"},{"location":"guide/testing/#pytest-textual-snapshot","title":"pytest-textual-snapshot","text":"

You can use pytest-textual-snapshot to snapshot test your Textual app. This is a plugin for pytest which adds support for snapshot testing Textual apps, and it's maintained by the developers of Textual.

A test using this package saves a snapshot (in this case, an SVG screenshot) of a running Textual app to disk. The next time the test runs, it takes another snapshot and compares it to the previously saved one. If the snapshots differ, the test fails, and you can view a side-by-side diff showing the visual change.

"},{"location":"guide/testing/#installation","title":"Installation","text":"

You can install pytest-textual-snapshot using your favorite package manager (pip, poetry, etc.).

pip install pytest-textual-snapshot\n
"},{"location":"guide/testing/#creating-a-snapshot-test","title":"Creating a snapshot test","text":"

With the package installed, you now have access to the snap_compare pytest fixture.

Let's look at an example of how we'd create a snapshot test for the calculator app below.

CalculatorApp \u257a\u2501\u2513\u00a0\u00a0\u2513\u00a0\u257b\u00a0\u257b\u250f\u2501\u2578\u250f\u2501\u2513\u257a\u2501\u2513 \u00a0\u2501\u252b\u00a0\u00a0\u2503\u00a0\u2517\u2501\u252b\u2517\u2501\u2513\u2517\u2501\u252b\u250f\u2501\u251b \u257a\u2501\u251b.\u257a\u253b\u2578\u00a0\u00a0\u2579\u257a\u2501\u251b\u257a\u2501\u251b\u2517\u2501\u2578 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0C\u00a0\u00a0+/-\u00a0\u00a0%\u00a0\u00a0\u00f7\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a07\u00a0\u00a08\u00a0\u00a09\u00a0\u00a0\u00d7\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a04\u00a0\u00a05\u00a0\u00a06\u00a0\u00a0-\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a01\u00a0\u00a02\u00a0\u00a03\u00a0\u00a0+\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a00\u00a0\u00a0.\u00a0\u00a0=\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

First, we need to create a new test and specify the path to the Python file containing the app. This path should be relative to the location of the test.

def test_calculator(snap_compare):\nassert snap_compare(\"path/to/calculator.py\")\n

Let's run the test as normal using pytest.

pytest\n

When this test runs for the first time, an SVG screenshot of the calculator app is generated, and the test will fail. Snapshot tests always fail on the first run, since there's no previous version to compare the snapshot to.

If you open the snapshot report in your browser, you'll see something like this:

Tip

You can usually open the link directly from the terminal, but some terminal emulators may require you to hold Ctrl or Cmd while clicking for links to work.

The report explains that there's \"No history for this test\". It's our job to validate that the initial snapshot looks correct before proceeding. Our calculator is rendering as we expect, so we'll save this snapshot:

pytest --snapshot-update\n

Warning

Only ever run pytest with --snapshot-update if you're happy with how the output looks on the left hand side of the snapshot report. When using --snapshot-update, you're saying \"I'm happy with all of the screenshots in the snapshot test report, and they will now represent the ground truth which all future runs will be compared against\". As such, you should only run pytest --snapshot-update after running pytest and confirming the output looks good.

Now that our snapshot is saved, if we run pytest (with no arguments) again, the test will pass. This is because the screenshot taken during this test run matches the one we saved earlier.

"},{"location":"guide/testing/#catching-a-bug","title":"Catching a bug","text":"

The real power of snapshot testing comes from its ability to catch visual regressions which could otherwise easily be missed.

Imagine a new developer joins your team, and tries to make a few changes to the calculator. While making this change they accidentally break some styling which removes the orange coloring from the buttons on the right of the app. When they run pytest, they're presented with a report which reveals the damage:

On the right, we can see our \"historical\" snapshot - this is the one we saved earlier. On the left is how our app is currently rendering - clearly not how we intended!

We can click the \"Show difference\" toggle at the top right of the diff to overlay the two versions:

This reveals another problem, which could easily be missed in a quick visual inspection - our new developer has also deleted the number 4!

Tip

Snapshot tests work well in CI on all supported operating systems, and the snapshot report is just an HTML file which can be exported as a build artifact.

"},{"location":"guide/testing/#pressing-keys","title":"Pressing keys","text":"

You can simulate pressing keys before the snapshot is captured using the press parameter.

def test_calculator_pressing_numbers(snap_compare):\nassert snap_compare(\"path/to/calculator.py\", press=[\"1\", \"2\", \"3\"])\n
"},{"location":"guide/testing/#changing-the-terminal-size","title":"Changing the terminal size","text":"

To capture the snapshot with a different terminal size, pass a tuple (width, height) as the terminal_size parameter.

def test_calculator(snap_compare):\nassert snap_compare(\"path/to/calculator.py\", terminal_size=(50, 100))\n
"},{"location":"guide/testing/#running-setup-code","title":"Running setup code","text":"

You can also run arbitrary code before the snapshot is captured using the run_before parameter.

In this example, we use run_before to hover the mouse cursor over the widget with ID number-5 before taking the snapshot.

def test_calculator_hover_number(snap_compare):\nasync def run_before(pilot) -> None:\nawait pilot.hover(\"#number-5\")\nassert snap_compare(\"path/to/calculator.py\", run_before=run_before)\n

For more information, visit the pytest-textual-snapshot repo on GitHub.

"},{"location":"guide/widgets/","title":"Widgets","text":"

In this chapter we will explore widgets in more detail, and how you can create custom widgets of your own.

"},{"location":"guide/widgets/#what-is-a-widget","title":"What is a widget?","text":"

A widget is a component of your UI responsible for managing a rectangular region of the screen. Widgets may respond to events in much the same way as an app. In many respects, widgets are like mini-apps.

Information

Every widget runs in its own asyncio task.

"},{"location":"guide/widgets/#custom-widgets","title":"Custom widgets","text":"

There is a growing collection of builtin widgets in Textual, but you can build entirely custom widgets that work in the same way.

The first step in building a widget is to import and extend a widget class. This can either be Widget which is the base class of all widgets, or one of its subclasses.

Let's create a simple custom widget to display a greeting.

hello01.py
from textual.app import App, ComposeResult, RenderResult\nfrom textual.widget import Widget\nclass Hello(Widget):\n\"\"\"Display a greeting.\"\"\"\ndef render(self) -> RenderResult:\nreturn \"Hello, [b]World[/b]!\"\nclass CustomApp(App):\ndef compose(self) -> ComposeResult:\nyield Hello()\nif __name__ == \"__main__\":\napp = CustomApp()\napp.run()\n

The three highlighted lines define a custom widget class with just a render() method. Textual will display whatever is returned from render in the content area of your widget. We have returned a string in the code above, but there are other possible return types which we will cover later.

Note that the text contains tags in square brackets, i.e. [b]. This is console markup which allows you to embed various styles within your content. If you run this you will find that World is in bold.

CustomApp Hello,\u00a0World!

This (very simple) custom widget may be styled in the same way as builtin widgets, and targeted with CSS. Let's add some CSS to this app.

hello02.pyhello02.tcss hello02.py
from textual.app import App, ComposeResult, RenderResult\nfrom textual.widget import Widget\nclass Hello(Widget):\n\"\"\"Display a greeting.\"\"\"\ndef render(self) -> RenderResult:\nreturn \"Hello, [b]World[/b]!\"\nclass CustomApp(App):\nCSS_PATH = \"hello02.tcss\"\ndef compose(self) -> ComposeResult:\nyield Hello()\nif __name__ == \"__main__\":\napp = CustomApp()\napp.run()\n
hello02.tcss
Screen {\nalign: center middle;\n}\nHello {\nwidth: 40;\nheight: 9;\npadding: 1 2;\nbackground: $panel;\ncolor: $text;\nborder: $secondary tall;\ncontent-align: center middle;\n}\n

The addition of the CSS has completely transformed our custom widget.

CustomApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258aHello,\u00a0World!\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

"},{"location":"guide/widgets/#static-widget","title":"Static widget","text":"

While you can extend the Widget class, a subclass will typically be a better starting point. The Static class is a widget subclass which caches the result of render, and provides an update() method to update the content area.

Let's use Static to create a widget which cycles through \"hello\" in various languages.

hello03.pyhello03.tcssOutput hello03.py
from itertools import cycle\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Static\nhellos = cycle(\n[\n\"Hola\",\n\"Bonjour\",\n\"Guten tag\",\n\"Salve\",\n\"N\u01d0n h\u01ceo\",\n\"Ol\u00e1\",\n\"Asalaam alaikum\",\n\"Konnichiwa\",\n\"Anyoung haseyo\",\n\"Zdravstvuyte\",\n\"Hello\",\n]\n)\nclass Hello(Static):\n\"\"\"Display a greeting.\"\"\"\ndef on_mount(self) -> None:\nself.next_word()\ndef on_click(self) -> None:\nself.next_word()\ndef next_word(self) -> None:\n\"\"\"Get a new hello and update the content area.\"\"\"\nhello = next(hellos)\nself.update(f\"{hello}, [b]World[/b]!\")\nclass CustomApp(App):\nCSS_PATH = \"hello03.tcss\"\ndef compose(self) -> ComposeResult:\nyield Hello()\nif __name__ == \"__main__\":\napp = CustomApp()\napp.run()\n
hello03.tcss
Screen {\nalign: center middle;\n}\nHello {\nwidth: 40;\nheight: 9;\npadding: 1 2;\nbackground: $panel;\nborder: $secondary tall;\ncontent-align: center middle;\n}\n

CustomApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258aHola,\u00a0World!\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

Note that there is no render() method on this widget. The Static class is handling the render for us. Instead we call update() when we want to update the content within the widget.

The next_word method updates the greeting. We call this method from the mount handler to get the first word, and from a click handler to cycle through the greetings when we click the widget.

"},{"location":"guide/widgets/#default-css","title":"Default CSS","text":"

When building an app it is best to keep your CSS in an external file. This allows you to see all your CSS in one place, and to enable live editing. However if you intend to distribute a widget (via PyPI for instance) it can be convenient to bundle the code and CSS together. You can do this by adding a DEFAULT_CSS class variable inside your widget class.

Textual's builtin widgets bundle CSS in this way, which is why you can see nicely styled widgets without having to copy any CSS code.

Here's the Hello example again, this time the widget has embedded default CSS:

hello04.pyhello04.tcssOutput hello04.py
from itertools import cycle\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Static\nhellos = cycle(\n[\n\"Hola\",\n\"Bonjour\",\n\"Guten tag\",\n\"Salve\",\n\"N\u01d0n h\u01ceo\",\n\"Ol\u00e1\",\n\"Asalaam alaikum\",\n\"Konnichiwa\",\n\"Anyoung haseyo\",\n\"Zdravstvuyte\",\n\"Hello\",\n]\n)\nclass Hello(Static):\n\"\"\"Display a greeting.\"\"\"\nDEFAULT_CSS = \"\"\"\n    Hello {\n        width: 40;\n        height: 9;\n        padding: 1 2;\n        background: $panel;\n        border: $secondary tall;\n        content-align: center middle;\n    }\n    \"\"\"\ndef on_mount(self) -> None:\nself.next_word()\ndef on_click(self) -> None:\nself.next_word()\ndef next_word(self) -> None:\n\"\"\"Get a new hello and update the content area.\"\"\"\nhello = next(hellos)\nself.update(f\"{hello}, [b]World[/b]!\")\nclass CustomApp(App):\nCSS_PATH = \"hello04.tcss\"\ndef compose(self) -> ComposeResult:\nyield Hello()\nif __name__ == \"__main__\":\napp = CustomApp()\napp.run()\n
hello04.tcss
Screen {\nalign: center middle;\n}\n

CustomApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258aHola,\u00a0World!\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

"},{"location":"guide/widgets/#scoped-css","title":"Scoped CSS","text":"

Default CSS is scoped by default. All this means is that CSS defined in DEFAULT_CSS will affect the widget and potentially its children only. This is to prevent you from inadvertently breaking an unrelated widget.

You can disabled scoped CSS by setting the class var SCOPED_CSS to False.

"},{"location":"guide/widgets/#default-specificity","title":"Default specificity","text":"

CSS defined within DEFAULT_CSS has an automatically lower specificity than CSS read from either the App's CSS class variable or an external stylesheet. In practice this means that your app's CSS will take precedence over any CSS bundled with widgets.

"},{"location":"guide/widgets/#text-links","title":"Text links","text":"

Text in a widget may be marked up with links which perform an action when clicked. Links in console markup use the following format:

\"Click [@click='app.bell']Me[/]\"\n

The @click tag introduces a click handler, which runs the app.bell action.

Let's use markup links in the hello example so that the greeting becomes a link which updates the widget.

hello05.pyhello05.tcssOutput hello05.py
from itertools import cycle\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Static\nhellos = cycle(\n[\n\"Hola\",\n\"Bonjour\",\n\"Guten tag\",\n\"Salve\",\n\"N\u01d0n h\u01ceo\",\n\"Ol\u00e1\",\n\"Asalaam alaikum\",\n\"Konnichiwa\",\n\"Anyoung haseyo\",\n\"Zdravstvuyte\",\n\"Hello\",\n]\n)\nclass Hello(Static):\n\"\"\"Display a greeting.\"\"\"\ndef on_mount(self) -> None:\nself.action_next_word()\ndef action_next_word(self) -> None:\n\"\"\"Get a new hello and update the content area.\"\"\"\nhello = next(hellos)\nself.update(f\"[@click='next_word']{hello}[/], [b]World[/b]!\")\nclass CustomApp(App):\nCSS_PATH = \"hello05.tcss\"\ndef compose(self) -> ComposeResult:\nyield Hello()\nif __name__ == \"__main__\":\napp = CustomApp()\napp.run()\n
hello05.tcss
Screen {\nalign: center middle;\n}\nHello {\nwidth: 40;\nheight: 9;\npadding: 1 2;\nbackground: $panel;\nborder: $secondary tall;\ncontent-align: center middle;\n}\n

CustomApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258aHola,\u00a0World!\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

If you run this example you will see that the greeting has been underlined, which indicates it is clickable. If you click on the greeting it will run the next_word action which updates the next word.

"},{"location":"guide/widgets/#border-titles","title":"Border titles","text":"

Every widget has a border_title and border_subtitle attribute. Setting border_title will display text within the top border, and setting border_subtitle will display text within the bottom border.

Note

Border titles will only display if the widget has a border enabled.

The default value for these attributes is empty string, which disables the title. You can change the default value for the title attributes with the BORDER_TITLE and BORDER_SUBTITLE class variables.

Let's demonstrate setting a title, both as a class variable and a instance variable:

hello06.pyhello06.tcssOutput hello06.py
from itertools import cycle\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Static\nhellos = cycle(\n[\n\"Hola\",\n\"Bonjour\",\n\"Guten tag\",\n\"Salve\",\n\"N\u01d0n h\u01ceo\",\n\"Ol\u00e1\",\n\"Asalaam alaikum\",\n\"Konnichiwa\",\n\"Anyoung haseyo\",\n\"Zdravstvuyte\",\n\"Hello\",\n]\n)\nclass Hello(Static):\n\"\"\"Display a greeting.\"\"\"\nBORDER_TITLE = \"Hello Widget\"  # (1)!\ndef on_mount(self) -> None:\nself.action_next_word()\nself.border_subtitle = \"Click for next hello\"  # (2)!\ndef action_next_word(self) -> None:\n\"\"\"Get a new hello and update the content area.\"\"\"\nhello = next(hellos)\nself.update(f\"[@click='next_word']{hello}[/], [b]World[/b]!\")\nclass CustomApp(App):\nCSS_PATH = \"hello05.tcss\"\ndef compose(self) -> ComposeResult:\nyield Hello()\nif __name__ == \"__main__\":\napp = CustomApp()\napp.run()\n
  1. Setting the default for the title attribute via class variable.
  2. Setting subtitle via an instance attribute.
hello06.tcss
Screen {\nalign: center middle;\n}\nHello {\nwidth: 40;\nheight: 9;\npadding: 1 2;\nbackground: $panel;\nborder: $secondary tall;\ncontent-align: center middle;\n}\n

CustomApp \u258a\u2594\u00a0Hello\u00a0Widget\u00a0\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258aHola,\u00a0World!\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u00a0Click\u00a0for\u00a0next\u00a0hello\u00a0\u2581\u258e

Note that titles are limited to a single line of text. If the supplied text is too long to fit within the widget, it will be cropped (and an ellipsis added).

There are a number of styles that influence how titles are displayed (color and alignment). See the style reference for details.

"},{"location":"guide/widgets/#rich-renderables","title":"Rich renderables","text":"

In previous examples we've set strings as content for Widgets. You can also use special objects called renderables for advanced visuals. You can use any renderable defined in Rich or third party libraries.

Lets make a widget that uses a Rich table for its content. The following app is a solution to the classic fizzbuzz problem often used to screen software engineers in job interviews. The problem is this: Count up from 1 to 100, when the number is divisible by 3, output \"fizz\"; when the number is divisible by 5, output \"buzz\"; and when the number is divisible by both 3 and 5 output \"fizzbuzz\".

This app will \"play\" fizz buzz by displaying a table of the first 15 numbers and columns for fizz and buzz.

fizzbuzz01.pyfizzbuzz01.tcssOutput fizzbuzz01.py
from rich.table import Table\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass FizzBuzz(Static):\ndef on_mount(self) -> None:\ntable = Table(\"Number\", \"Fizz?\", \"Buzz?\")\nfor n in range(1, 16):\nfizz = not n % 3\nbuzz = not n % 5\ntable.add_row(\nstr(n),\n\"fizz\" if fizz else \"\",\n\"buzz\" if buzz else \"\",\n)\nself.update(table)\nclass FizzBuzzApp(App):\nCSS_PATH = \"fizzbuzz01.tcss\"\ndef compose(self) -> ComposeResult:\nyield FizzBuzz()\nif __name__ == \"__main__\":\napp = FizzBuzzApp()\napp.run()\n
fizzbuzz01.tcss
Screen {\nalign: center middle;\n}\nFizzBuzz {\nwidth: auto;\nheight: auto;\nbackground: $primary;\ncolor: $text;\n}\n

FizzBuzzApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Number\u2503Fizz?\u2503Buzz?\u2503 \u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529 \u25021\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25022\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25023\u00a0\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u2502\u2502 \u25024\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25025\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502buzz\u00a0\u2502 \u25026\u00a0\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u2502\u2502 \u25027\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25028\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25029\u00a0\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u2502\u2502 \u250210\u00a0\u00a0\u00a0\u00a0\u2502\u2502buzz\u00a0\u2502 \u250211\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u250212\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u2502\u2502 \u250213\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u250214\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u250215\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u2502buzz\u00a0\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

"},{"location":"guide/widgets/#content-size","title":"Content size","text":"

Textual will auto-detect the dimensions of the content area from rich renderables if width or height is set to auto. You can override auto dimensions by implementing get_content_width() or get_content_height().

Let's modify the default width for the fizzbuzz example. By default, the table will be just wide enough to fix the columns. Let's force it to be 50 characters wide.

fizzbuzz02.pyfizzbuzz02.tcssOutput fizzbuzz02.py
from rich.table import Table\nfrom textual.app import App, ComposeResult\nfrom textual.geometry import Size\nfrom textual.widgets import Static\nclass FizzBuzz(Static):\ndef on_mount(self) -> None:\ntable = Table(\"Number\", \"Fizz?\", \"Buzz?\", expand=True)\nfor n in range(1, 16):\nfizz = not n % 3\nbuzz = not n % 5\ntable.add_row(\nstr(n),\n\"fizz\" if fizz else \"\",\n\"buzz\" if buzz else \"\",\n)\nself.update(table)\ndef get_content_width(self, container: Size, viewport: Size) -> int:\n\"\"\"Force content width size.\"\"\"\nreturn 50\nclass FizzBuzzApp(App):\nCSS_PATH = \"fizzbuzz02.tcss\"\ndef compose(self) -> ComposeResult:\nyield FizzBuzz()\nif __name__ == \"__main__\":\napp = FizzBuzzApp()\napp.run()\n
fizzbuzz02.tcss
Screen {\nalign: center middle;\n}\nFizzBuzz {\nwidth: auto;\nheight: auto;\nbackground: $primary;\ncolor: $text;\n}\n

FizzBuzzApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Number\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Fizz?\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Buzz?\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503 \u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529 \u25021\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25022\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25023\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502 \u25024\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25025\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502buzz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502 \u25026\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502 \u25027\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25028\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25029\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502 \u250210\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502buzz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502 \u250211\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u250212\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502 \u250213\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u250214\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u250215\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502buzz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

Note that we've added expand=True to tell the Table to expand beyond the optimal width, so that it fills the 50 characters returned by get_content_width.

"},{"location":"guide/widgets/#tooltips","title":"Tooltips","text":"

Widgets can have tooltips which is content displayed when the user hovers the mouse over the widget. You can use tooltips to add supplementary information or help messages.

Tip

It is best not to rely on tooltips for essential information. Some users prefer to use the keyboard exclusively and may never see tooltips.

To add a tooltip, assign to the widget's tooltip property. You can set text or any other Rich renderable.

The following example adds a tooltip to a button:

tooltip01.pyOutput (before hover)Output (after hover) tooltip01.py
from textual.app import App, ComposeResult\nfrom textual.widgets import Button\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\"\"\"\nclass TooltipApp(App):\nCSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Button(\"Click me\", variant=\"success\")\ndef on_mount(self) -> None:\nself.query_one(Button).tooltip = TEXT\nif __name__ == \"__main__\":\napp = TooltipApp()\napp.run()\n

TooltipApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Click\u00a0me\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

TooltipApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2581\u2581I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear.

"},{"location":"guide/widgets/#customizing-the-tooltip","title":"Customizing the tooltip","text":"

If you don't like the default look of the tooltips, you can customize them to your liking with CSS. Add a rule to your CSS that targets Tooltip. Here's an example:

tooltip02.pyOutput (before hover)Output (after hover) tooltip02.py
from textual.app import App, ComposeResult\nfrom textual.widgets import Button\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\"\"\"\nclass TooltipApp(App):\nCSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    Tooltip {\n        padding: 2 4;\n        background: $primary;\n        color: auto 90%;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Button(\"Click me\", variant=\"success\")\ndef on_mount(self) -> None:\nself.query_one(Button).tooltip = TEXT\nif __name__ == \"__main__\":\napp = TooltipApp()\napp.run()\n

TooltipApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Click\u00a0me\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

TooltipApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2581\u2581 I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0 brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear.

"},{"location":"guide/widgets/#line-api","title":"Line API","text":"

A downside of widgets that return Rich renderables is that Textual will redraw the entire widget when its state is updated or it changes size. If a widget is large enough to require scrolling, or updates frequently, then this redrawing can make your app feel less responsive. Textual offers an alternative API which reduces the amount of work required to refresh a widget, and makes it possible to update portions of a widget (as small as a single character) without a full redraw. This is known as the line API.

Note

The Line API requires a little more work that typical Rich renderables, but can produce powerful widgets such as the builtin DataTable which can handle thousands or even millions of rows.

"},{"location":"guide/widgets/#render-line-method","title":"Render Line method","text":"

To build a widget with the line API, implement a render_line method rather than a render method. The render_line method takes a single integer argument y which is an offset from the top of the widget, and should return a Strip object containing that line's content. Textual will call this method as required to get content for every row of characters in the widget.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXGtT28hcdTAwMTL9nl9BsV/2Vlx1MDAwNe109zxTtXVcdTAwMGJIIISER4BcdTAwMTBya4tcdTAwMTK2bFx1MDAxNORcdTAwMDe2eG7lv99cdTAwMWWHWPJDxjY2ce5d71x1MDAwNowk2+OZc7pPP0Z/v1haWk7vmtHyq6Xl6LZcdTAwMTQmcblcdTAwMTXeLL/0x6+jVjtu1PlcdTAwMTR2/m43rlqlzpXnadpsv/rjj1rYuojSZlx1MDAxMpai4DpuX4VJO70qx42g1Kj9XHUwMDExp1Gt/W//cyesRX82XHUwMDFitXLaXG6yXHUwMDBmWYnKcdpoff+sKIlqUT1t87v/h/9eWvq78zM3ulZUSsN6NYk6L+icylx1MDAwNqjR9Vx1MDAxZt1p1DuDtWhJkJa6e0Hcfs1cdTAwMWaXRmU+W+EhR9lcdTAwMTl/aFndXHUwMDFlnVx1MDAxY16fNnfM0d398cmhkqdlzD61XHUwMDEyJ8lBepd8n4mwdH7Vyo2pnbZcdTAwMWFcdTAwMTfRcVxcTs/5vOw73n1du8GTkL2q1biqntejtv/+0D3aaIalOL3zX0J0XHUwMDBmfp+DV0vZkVv+a0VcdTAwMDVOklx1MDAwMuuUMMZcdJd9sn89KFxurFJGgUGhrXR941pvJLxcdTAwMTI8rt9UhcolmY3sLCxdVHl49XL3mrRcdTAwMTXW282wxeuVXXfz8I3JUICCXHUwMDAwVffUeVx1MDAxNFfPU1x1MDAwZiPUgVLOKtLKOevQZMOIOstcdTAwMDFaXHUwMDFhRONM9u38hze3ylx1MDAxZGT8lZ+wevlhwupXSZKN159404+mPKJyK920Z9VduFx1MDAxMVxmiFx1MDAxOGpn764qm4er3e/UXHUwMDAzv7DVatwsd898e3iWjeiqWVx1MDAwZdOHL2GURGLgWYvd80lcXL/oXHUwMDFmbNIoXWQw7Fx1MDAxY/32clxu+IOAQvzzSljhQMmx8Z9cXLy7PD1vbev9N3uJhlx1MDAwYrmLd1x1MDAxYlx1MDAwNfgvtVx1MDAxYe32ynmYls6LOIAz4lx1MDAwMIhHSeBE4LRgiFx1MDAxYqFcdTAwMDXwXHUwMDFh9JDAuMA5XHUwMDA3vCokrdVAhSRcdTAwMTCdx/QkUFJcdTAwMDaakCRcdFx1MDAwNVx1MDAwMEOoQFpcdTAwMDRcdTAwMTaYplKTpy3qXHUwMDAxKpBTXGYkUnZmVFx1MDAxOFx1MDAwMVaDRln9LGA1aFx1MDAwYrHqnCG2XGZkx1x1MDAwNqvaxvVSZecmxNXjk1x1MDAxYqrCadIuMtZ9gPt5MIWA0JA2glx1MDAxNFx0hFx1MDAwMZhK6yRcdTAwMWFUxlhw84QpXHUwMDA1XHUwMDAytEKrXGaw0bWDOEVcdTAwMWJoLZgvWqAmxvQgTlx1MDAxZCn2KFx1MDAxYWZnslx1MDAxZsOpmVx1MDAxNU6jJImb7eGKQkNcdTAwMTFKnbcsykpcdTAwMWNcdTAwMWKk+1uV6+279t37xsV++nqrtd3ePL+cXHUwMDA2pPB8IPUwJJBsw7RcdTAwMDX2IL0gtS5QhtFAiEJYJfqFzkQg/a1cdTAwMTIqVDhcYlCgQFxuROU08i822nJcdTAwMTChgIHSWjJcbkEgXHUwMDExylx1MDAwMVEh2bxaySrwf1xuoCbnVvpcdTAwMDCK1pFGY8dcdTAwMDcolurV++Rz+aB+bj/o9kH49cRcdTAwMTS5/EVcdTAwMDGo4oVcdTAwMTdCXHUwMDFha0AjsLPsXHUwMDA1qFxyXHUwMDE4XHUwMDExvFx1MDAxNk4oyVx1MDAwMNZPXHUwMDA06JlcdTAwMTBqXlx1MDAwMCW2v0Yw1Z5ccqD2OVx1MDAwMKpcdTAwMGI1qXKs0YhyjHxcZqDHe1x1MDAxN/d7n87XXHUwMDEy8UnvbO2+PvhcdTAwMWNcdTAwMDItOEBRXHUwMDA20kipXHUwMDE0aFx1MDAwMM1cdTAwMTBQvVxiNYFcdTAwMTGCueqIp4qVwJNcdTAwMTBcbnjGmnZeXGLl8TlhrNC/XHUwMDFlQkdqUetcbr08XHUwMDAwSZBKuPFBWj7Y3CuX3lx1MDAxZlx1MDAxZX01N1dcdTAwMWZut1Vauf70XFwgXHUwMDE1U4FUXHUwMDA0QILYO3JULpSPm7BcdTAwMDekoFxcXHUwMDAw0jlEXHUwMDA2MVx1MDAxOYbIk1BqnFx1MDAxMlx1MDAxNcQhrt5cdTAwMDdEikNcdTAwMDNyUqNcdTAwMWOePtBcdTAwMWO/SZSKjFx1MDAxNPwzXHUwMDA35Vx1MDAxZoZUasPhnZ5cdTAwMDSmWVrgXHUwMDA3ZOjhyLdi9HZfMySp8PHy+uaotH178WXjrb7/fLe+d4234yVcdTAwMTVejnrfuSYrXGZcdTAwMWJkSbPiXFxcdTAwMWHdpsPohrYw9EMjrVx1MDAwNDOBSzhxXHUwMDFm31x1MDAxZidcdTAwMDdvjqp2rXK1trL1QZVcdTAwMGWnS9M9o1Mgli0sSViyoPWhXHUwMDE39NFcclx1MDAwMz5swElcdTAwMDFI4Pqj0tlcdTAwMDV/mMuOZFx1MDAxNFP9lGLyK+/DZmj650SeR0HOVoVtuppcdTAwMDDkXHUwMDE5mFx1MDAxYfX0IL7vXHUwMDAwVfRcdTAwMWPdXGJrcXLXg4dcdTAwMGX6X3Vmulx1MDAxYaVcdTAwMDFPfzlqnfKnRb/f/Sn+lV+yduRcdTAwMGb7V9uel68mcdVcdTAwMTNmOYkqvUxK41KYdE+njWZ2tsTDXHT57Vpb5f6v1WjF1bhcdTAwMWUmh49cZm0qVlNxqFxmXHUwMDFjXHUwMDBikdMwfvJRXHUwMDFklsT2znFsWnt2o11cdTAwMGJ33ea7rV+C1Up4SrNzXCKle4NlNq1cdTAwMDEosFx1MDAxNkmwi2NcdTAwMTU2P1ZcdTAwMGZLNlx1MDAwZbKa1Vx1MDAxY0p2o5l5mSepL0vr7urg8qDkbPJ2O6xcXGxdfjibXHUwMDE5qVnTimzZflxuqWFxSVxyU5JajojehHLkI5qxSZ2YKt6n+9H1p9219fhzJV7Zw5tfgNQqQMGKmONcdTAwMTFcdTAwMTamprekhopcdTAwMDJhQLP168R4pm9gs/TUQ2K2IZ6anOPh2GdcInWj9CVsnVTfXHUwMDFlxXs7d+5g/8Jd7u/OjtSan/1cXFLj4pJcdTAwMWFcdTAwMWYh9fdcdFx1MDAxZsJqsLk0Wb+vJiU5XG5cdTAwMTg/aThaqy0qq1lWM29cdTAwMWRcdTAwMWFAzf6YSd6rwDWfVtrn45RcdTAwMDUgnJ9cdTAwMDJcdTAwMDewgVWGPOJRO8rlKLPUXGZcdTAwMDRoXHUwMDA1x2eWQ3Difznp8KNkzr5cdTAwMWOJ9CS0XHUwMDFmXGZ68eHIiKB3XHUwMDE0X0lLmspcdLfTsJWuxfVyXFyv9lx1MDAwZeyhJWRrjGCvw/DSlVx1MDAxZuWKXGKUlZJNtjI8JG0zseVnJmzyNTqwqKzzVXOSzFxioVx1MDAwNr480+3xQX1U5eZXiCvq6HiXPtRcdTAwMGWqq9VwtWBQXHUwMDA2wPB/1pLjNVdcdTAwMDNjXHUwMDAyXG5cYqWUwvjAz6EzXHUwMDAzY0rCdrreqNXilOd+r1x1MDAxMdfT/jnuTOaqJ/95XHUwMDE0XHUwMDBlmFx1MDAxNv5O+XP9VqLp37HX7GfPljJcdTAwMWF1/ug+/+vl0KuLsf39bD+qs/d7kf89uYFcdTAwMTNS9Fx1MDAxZv5h4CRcdTAwMThfUp+guDxauC6uhbNcdTAwMDFLe9ZcdTAwMDO+PFwiOf7qXHUwMDE1LiBcdTAwMDOn0WhpNIl8d8LMXHJcdTAwMWPKwFdg0DphhVOZQ8/6IJitvCp8gXS+ijOsJUghaWEmalx0mrl94zHgJL08k9q30WFvzpSIXHUwMDAwhFx1MDAwNuGs9Wl9XHUwMDBlrVx1MDAxNeSu+m5LePk77S+KULMxXHUwMDE5/OpjWbeTnd23qzWx6u7bZ7T1+vBetdebw4eEmlx1MDAxOExaW7Ik+HPNwJBAXHUwMDA0JFx1MDAxNS8w+1XprFx1MDAwNPdrm7dcImT7x8ogqGdm3vKxwEDzXGbrXHUwMDE29M1EY9u30Vx1MDAxYX5x7Vx1MDAxYrJEQ1x1MDAwNGRWSqFtb1x1MDAwZVx1MDAxNTXbN+NcdTAwMWJalCAgkvOLzNin+5KEVk5LadlMXHLpoFx1MDAwMVx1MDAxNVx1MDAwMFx1MDAwM1x1MDAwNSQrXHUwMDEydJZoiIWz1lxuMmaSTq+ZW7ipI64xLdzoXHUwMDFjQI85YelGzCpjkWWEM4NcdTAwMDbO16TYhnhcdTAwMDPoW+zUYMVmLFx1MDAwYrd1e5rcrVSPcUW3PsLnzXfNK7k3fEhsctF6YUasZyzpIUNcIoau0lxuoNP++ovbt0Jg+8fKIKYnNHCFWSco7lxyRI7IWDVOXHUwMDEyoI7U51x1MDAxM5m3wu7Actg+j2bZyc3YXHUwMDE2qHjWmY/ev/dFp8h+lGkhnWKZl/dcdTAwMDYzzzrl2oeyrFM21q7xXHUwMDEyXGZcdTAwMDRHM+yu6mJptkXQl6Ped751J2JdlKVTp8tmge452s1mZaWGXHUwMDFm2ayDtFx1MDAxNTd//087qvopfbnUfVx1MDAxMlx1MDAwNMFfXHUwMDA1SS3d8y7zTmo9MsKRpmNkP4fLtSn221x1MDAwZtZGPvKewH7EryVtv0nCS6rJXHUwMDBioq9cdTAwMWI3t7VcInm0IP1cdTAwMWMrXCKQaIT1qlx1MDAxM1xmWtvfXHUwMDE3XHUwMDA3Tlx1MDAwN1xuXHUwMDA0XHUwMDBiVO8/zdOa4Ofdz+FNvlx1MDAwNtLuacroiVxyXHUwMDFk8yxfkW9cdTAwMDM3z9PsxIFjYfLXi+lOz8H4RZ1K+0vdnF9cXH84PLtf+1irt+tuM1l4dqBwznI4L1x1MDAxOXhGqL6aXHUwMDBlSfa+TivB/6N4Ws/onLnB4ouv6Nm18Vx1MDAxM7gxzypcdTAwMTD5lqBcXNpnTk1JJFx1MDAwYiud2vq9RGqCRurRSZNcdTAwMDWVnDogjUL4rVNWXHSw/d1cdTAwMGJcIjDo87iW8WjNXHUwMDFjXHUwMDBinWNKTuLoz29cdTAwMTN6XHUwMDE2xTlX62+F31fzjzKciTIs7k8q7jpcdTAwMTSKpFx1MDAxNdaNnzNcdTAwMWKdNFhQhqtcdTAwMDBcdTAwMTUq0dnAxXymfrdnXHUwMDAyXHJW++SF1ajnV1x1MDAxMVx1MDAxOJPhljQoXprniSnn6sNcdTAwMThcXEr/XHUwMDEz+82X4a44Ka6N76PNV8dcdTAwMWYjOJhcdTAwMWS92fq0cXK11npT3r9d/bJn381U146i9zBh+yi9/T5O5dCXu5Umlvn9/Fx1MDAwZVgtXHUwMDEyKVx1MDAwZfeU0UZcdTAwMTdcdTAwMGLbMbb/j1x1MDAxMLaghm0hXHUwMDFk7FRija2cmSysW9Rd/lJcdTAwMTgzVWVwqk6l90yypdW9raXjTl/QXCJ0KPVcdTAwMGZpJINcdTAwMGIrW7r4XHUwMDBlXHUwMDA2KKRcdTAwMDSfa1x1MDAxZb+LePSSz3lX+FRcdTAwMTT2XHJcdTAwMTOGY0Gyxlx1MDAxN4Ogd1x1MDAxYlx1MDAwZVx1MDAwN4zOXHUwMDE3iDhe1Fx1MDAxMmGEh35cdTAwMWGBpVx1MDAwYoS0vqeRQ1x1MDAwMVx1MDAxNlx1MDAwNEPobGWgrZagJZtcdTAwMTOgXFz+6MF3I1x1MDAwN21mlvtcdTAwMDVGsk/lXHUwMDBiXHUwMDAys69ajXZcdTAwMDZLPUVwYVx00Vx1MDAxOFx1MDAxMKT81FHuqodcIjhcdTAwMDbGMZitXHUwMDAzniRp7WCPz1h1q9H7zHpcdTAwMDYloNM6gdJcdTAwMDdcdTAwMWHo01x1MDAxZYOjooC1oUFeclx1MDAwNpiU4tcuXVx1MDAxNULYP/rBm73Zi/zv6dJrVHxjXHUwMDBi35BmnZ5AhVT3bk72t1blUUlhfFgtl0qfP5ZnqkImtGDqUVx1MDAwYlx1MDAxNiAozfAxvu8kn9TsaFx1MDAxMCNcdTAwMDLpXHUwMDA0XHUwMDBiXHUwMDE0R8hRRv+wZpZbU96OknWK/OpcdTAwMDNcZuudfjS5ZslqY1xyPq2ncpFza7xcdTAwMDIzvOFLkTBXuW6HPkpcdTAwMTAvgGOvP75T39i5rq2vJPK02Tyrne5v7X51Z5s/OfKWj0rzwJBjny4sXHUwMDE4Ry5/XHUwMDAzle+0kFx1MDAwMet2sEjOXGIj5rjfb9x6rtTkd8o9z81cYizHXHUwMDAyk2w7/Sc6ntY7gSws/liw3v+Pn1x1MDAwM/uyefYubFx1MDAxZdy//Xik11x1MDAwZeMvd9vVm2e7XHUwMDFiw1TOybeFS/Y9yMFcdTAwMDSgXHUwMDEykEv6d7BcInRgWFx1MDAxM1xinlx1MDAwN806ySx0XVx1MDAxNFhnWpA0/43uI6sz/j4z8/YgUlx1MDAxNXpcdTAwMTDQYNCx3lx1MDAxON+FvK9cdTAwMWbbo1J8vH5v65XD083W12azseguhFx1MDAwMoaLUuyzfVecXHUwMDEzvVx1MDAxYlZcYjj08Dd11PyDr/rpXHUwMDBlRFx1MDAxOVL+plGz24Y20oFImKhE+I9cdTAwMDOZ2oFcdTAwMTTfY1x1MDAxNaThZfftx2MzcVVUrrd0RUNyrfZKXHUwMDFm4q9n9rqojPIsLuRRXHUwMDFh+lx1MDAxNFxmXGJGtkO/P6xv31x1MDAxOElcdTAwMGWjjXaC40q0+Z7kXHUwMDA19Fx1MDAxZlx1MDAxY4uSr3z9XFz34UvN83dcdTAwMWaFXHJhKCx6XHUwMDFkYMbXPXftjav7hjsyNqkmZXO6oe7fvVl098FoIHKotZ9vwVx1MDAxZaJcdTAwMWa4Olx1MDAxMP5mR75Q5XdcdTAwMGL99Po+XG7lt1SoXHUwMDE5llx1MDAwN0Z6XHUwMDEwjrqeelOQ/ztcdTAwMGby4iG5sFx1MDAxYzabXHUwMDA3Kc9o10LwWsXlh2nJPmf5Oo5u1obdWa/z8O/aobbnUNQxN99efPsvXHUwMDFht7uQIn0= widget.render_line(y=0)widget.render_line(y=1)widget.render_line(y=2)Strip([segment, segment, ...])Strip([segment, segment, ...])Strip([segment, segment, ...])Line API WidgetStrip([segment, segment, ...])Strip([segment, segment, ...])Strip([segment, segment, ...])

Let's look at an example before we go in to the details. The following Textual app implements a widget with the line API that renders a checkerboard pattern. This might form the basis of a chess / checkers game. Here's the code:

checker01.pyOutput checker01.py
from rich.segment import Segment\nfrom rich.style import Style\nfrom textual.app import App, ComposeResult\nfrom textual.strip import Strip\nfrom textual.widget import Widget\nclass CheckerBoard(Widget):\n\"\"\"Render an 8x8 checkerboard.\"\"\"\ndef render_line(self, y: int) -> Strip:\n\"\"\"Render a line of the widget. y is relative to the top of the widget.\"\"\"\nrow_index = y // 4  # A checkerboard square consists of 4 rows\nif row_index >= 8:  # Generate blank lines when we reach the end\nreturn Strip.blank(self.size.width)\nis_odd = row_index % 2  # Used to alternate the starting square on each row\nwhite = Style.parse(\"on white\")  # Get a style object for a white background\nblack = Style.parse(\"on black\")  # Get a style object for a black background\n# Generate a list of segments with alternating black and white space characters\nsegments = [\nSegment(\" \" * 8, black if (column + is_odd) % 2 else white)\nfor column in range(8)\n]\nstrip = Strip(segments, 8 * 8)\nreturn strip\nclass BoardApp(App):\n\"\"\"A simple app to show our widget.\"\"\"\ndef compose(self) -> ComposeResult:\nyield CheckerBoard()\nif __name__ == \"__main__\":\napp = BoardApp()\napp.run()\n

BoardApp

The render_line method above calculates a Strip for every row of characters in the widget. Each strip contains alternating black and white space characters which form the squares in the checkerboard.

You may have noticed that the checkerboard widget makes use of some objects we haven't covered before. Let's explore those.

"},{"location":"guide/widgets/#segment-and-style","title":"Segment and Style","text":"

A Segment is a class borrowed from the Rich project. It is small object (actually a named tuple) which bundles a string to be displayed and a Style which tells Textual how the text should look (color, bold, italic etc).

Let's look at a simple segment which would produce the text \"Hello, World!\" in bold.

greeting = Segment(\"Hello, World!\", Style(bold=True))\n

This would create the following object:

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN1aW1PbRlx1MDAxOH3nVzDuSztcdTAwMTM2e790ptOhJFx1MDAxNEIuUEJoaTuMsNa2gixcdTAwMTlJ5pJO/nu/XHUwMDE1RpIl7CBjUqd+XHUwMDAwe3el/Xb3nPNdpH/W1tc72c3Idn5cXO/Y665cdTAwMTdcdTAwMDZ+4l11nrn2S5ukQVx1MDAxY0FcdTAwMTfNf6fxOOnmI1x1MDAwN1k2Sn98/nzoJec2XHUwMDFihV7XossgXHUwMDFke2Gajf0gRt14+DzI7DD92f1961xy7U+jeOhnXHQqJ9mwfpDFye1cXDa0Q1x1MDAxYmUp3P1P+L2+/k/+t2JdYruZXHUwMDE39UObX5B3lVx1MDAwNirJ661v4yg3lkhFhGRcdTAwMWHLYkSQvoD5MutDd1x1MDAwZmy2ZY9r6vAjuvlhK+7ffPQvtlx1MDAwZcJNSo7CP8ppe0FcdTAwMThcdTAwMWVmN+HtVnjdwTipXHUwMDE4lWZJfG6PXHUwMDAzP1x1MDAxYrjZa+3Fdb6XXHUwMDBlwICiO4nH/UFkU7dcdTAwMDO4aI1HXjfIbtyNcNl6u1xy1XHX7pBcZkZSca41Zsatueh11zNKXHUwMDExx0ZLyZRcdTAwMTCMypphW3FcYmdcdTAwMDGGfYfzT2nZmdc974N5kV+MyVx1MDAxMi9KR15cdTAwMDInVo67miyZXHUwMDBiiqQglCqMXHKunszAXHUwMDA2/UHmjosgho1SynDMsJCiNMbmp0JcZnN2XHUwMDEyo4tcdTAwMWVnwmjXz1x1MDAxMfJ3ddtcIn+ybXeQKUHDJi2fy8W48S/rYKtcdTAwMDKugoN3e0dXxmNDtn/iXHUwMDFk7uyeblx1MDAxZqTvVLHgKXR6SVx1MDAxMl91ip7Pk2+loeOR791cIlx1MDAwZVx1MDAwMCk4XHUwMDExWlFDTdFcdTAwMWZcdTAwMDbROXRG4zAs2+LueVx00rXKStqxg1a2scZcdTAwMGVcdTAwMDBcdTAwMGLRilx1MDAxOf5gclxcnrw7vd5S9rV93dvjsYjt/lx0WYxcdTAwMWN0XHUwMDE2OdJcdTAwMTgk4n5ukFx1MDAwNblhkFwiXFxzXHUwMDAw1TQtXHUwMDE4QVhSMZNcclpSa7qLs4FiiaQhXHUwMDFjJuHGfVSTXHUwMDBlgiFtpm2744HEmlxuzivn80Q8mIdUXHUwMDAxu8SWhdTMXmf3gnQmRlx1MDAwNTVcZnSLPlxcwI9Aen57b7btx4R9XHUwMDE0Nt7ZXHUwMDBlg8GSXHUwMDA1fOlcdTAwMThlRFwirakmtFx1MDAwNlH+dFpNKtJbwJHyOlxmXHKmXHUwMDA0XHUwMDBiptqgcFxuXHUwMDFmrWT3evx7cPkq+nBp9/ax2Lu+Or5cdTAwMWPsLUl2OWfaXHUwMDE40lx1MDAwMswlauIoO1xmPtmc1FOt294wXGLzoyqac5SDgX91dmxcdTAwMTjGz9aP4yT0/+pUjyq1MHvtdu66zTDoO0Z0QtubpkpcdTAwMTZA8FR0Z/Go7O2CXHUwMDFkXHUwMDFl3C7Z9evriZOgXHUwMDFmRF74fpZNi3tcdTAwMTbGVL31jrWGXHUwMDEwIK3hXHUwMDBmZ+3u3ubbbSrVxYuD991cdTAwMWXTn3x/J5jB2lx1MDAxYfv+K79cIihGVIBCmqZjwVxiT7XWucsk73XVIzxcdTAwMGJhSNx5ldaeRTpBxZqV6/m/XHUwMDA1WMYwgkvFelwit0VmM4AoyjGRpEVwdfM2ftU/O9u/XHUwMDEx4uJ0g1x1MDAxZI9e71x1MDAxZmyuuuNcdTAwMTJMI6yVXHUwMDEyhlPnKNg0XHUwMDExuECSc8HInCjr0X5Mmyb4m35MSaG5Ulx1MDAxNU16Sj/2q/fJo3tcdTAwMDO6uSuOduTxsWTnLy+Wlj5cdTAwMTjNOG2B7sf5sVx1MDAxYz3fn8Wh/9P7ZGx/WFx1MDAwNT/WsGmxuLNidZ3AnEHKXHUwMDAwaWhcdTAwMTmafonAL0/UwcvRfnK+8bt9dbQpe1x1MDAxYofh6apcdTAwMTOYUYYoJNzwMVxuXHUwMDEy93K57nouOVx1MDAwMvpSXHUwMDE3PHHYjbp3XVwii+k9Low2XFyXkka6QK6N61rNaFRcdTAwMTBcdTAwMTBF9lQsJlNsdFx1MDAwNvZcdTAwMTNrsyDqo2kyVChcXIH616DwtEGL8Vx1MDAxN8/kL/gjXHR7rMpcdTAwMDFfou9V/8Ppm1x1MDAxNyPpXHUwMDFmvtl68UZEoThcdTAwMWOfrDp9hYJA0HBiZO6AWZ2+XG5RIDV0XHUwMDAwezWresVl87dCyTn8hWReSY1L2H+zTphcdTAwMTCBK1x1MDAxOfRXo2+aY2mV+Htr0VxcXHUwMDAy327vfUlkpVxmWvfAWlx1MDAwYlx1MDAwMVx1MDAxOU5cdTAwMGJcdTAwMGY8X69XlMJMXHUwMDBiJFx1MDAwNGR04Fx1MDAwZaRcdTAwMDF/PFx1MDAxZENzSZCmmFNtnthcdTAwMDVcdTAwMWKkQElcXKFEaqOwvqc8pDRSICXmLlx1MDAxY6hcdTAwMThzV7yXcFx1MDAwM6ZcdTAwMWVXu6eTlsVqlos71jTzkuyXIPJcdTAwMDHW04ZNnlHtPiDQy7ncXHUwMDFkOys3MMJcXFx1MDAxMVc9g3CSUWp4+WjG7Y03cqtFkuWlv8aqbeR/2Zr5XHUwMDA1z4o1YFxmwVx1MDAwNsPeMEyoNIRy1TCGMMjn8pJGw5rQS7OteDhcZjLY7v04iLL6tub7t+m4PrBeQ0BgNdW+uiiM3Fx1MDAxZKc1vfy2XpIm/1F8//vZvaNnYtl9Nlx1MDAxYTAub7dW/d9azqiaWclcdTAwMDYh4EJcYqVcdTAwMWUuZ/P914rKmWRcdTAwMWNcdTAwMTmqMVMu6IDMoaFmsFx1MDAwNdJhzEUkrGbX8tRcZuJcIsyNUkZcdTAwMGLIb4SsJP6lnCmEXHUwMDFkXHUwMDE1INOrXHUwMDE5M1EzJjXRXHUwMDFjyzZFg2XL2eLZ/lx1MDAwM+VsfuBblzNcdTAwMDaUYiBoXHUwMDEwVzqK8cq4W1x1MDAwNVx1MDAxMYgzdr+CPEjP5tfBpvWMXG7KNVx1MDAwMVx1MDAwNitOsKaGNawhXHUwMDAykVx1MDAxOer6LenZxmw45911JLdcdTAwMTS0WVx0lmGi3lokWFxmpuK0zZO5M9zt9a8hXHUwMDFhXHUwMDFkXHUwMDFmXnw8+qN38mv8epFcdTAwMWH/XCJitth7XHUwMDE1Tq5cdTAwMDTPdUxgXHUwMDAzwVlZpHA3oJQh8KJGXHUwMDEy6TA/O7tcdTAwMDLn7/H5YvZdr9drqph6UFWEQOKHtWStdGrxvOqJS/dSV6pQXyuvWqWMasFcXIpSQurNRfRcdTAwMDFbXHUwMDBiQCUtkqn5p7ySj+S4XHUwMDExiCqmlVCUS1x1MDAxMMKSkTldhUBaQSxuXHUwMDA0eHZiKq/GLI2wXHUwMDEwP3NcZiqtJOdcdTAwMWFCoEq4Vz6ZI65mg4VDPDZcdTAwMTJX9GzygFx1MDAwZbSVske+XHUwMDAw9aioQ2JcdTAwMTeutnlFqW3UMd9cdTAwMWJMuXnKMVx1MDAwM99GKTbCbUwz5lBIuUhAaYWpXHUwMDE0RuNcdTAwMDVjj/lv/9ViXHUwMDBmLMDlau5cdTAwMWVwSaFpwyhcdTAwMDKBMNPgrImGMFx1MDAxMoY0n5h+SyHIbGS7T1x1MDAwM9OzXCKQtclcdTAwMDRcdTAwMWRvNDrMXHUwMDAwdMVxXHUwMDAwylx1MDAwM39cIuDlKjuXgb365X76OVx1MDAwNq5N9tNcdJLNmfB57fO/MTl+mCJ9 \"Hello, World\"Style(bold=True)greeting.textgreeting.stylegreeting

Both Rich and Textual work with segments to generate content. A Textual app is the result of combining hundreds, or perhaps thousands, of segments,

"},{"location":"guide/widgets/#strips","title":"Strips","text":"

A Strip is a container for a number of segments covering a single line (or row) in the Widget. A Strip will contain at least one segment, but often many more.

A Strip is constructed from a list of Segment objects. Here's now you might construct a strip that displays the text \"Hello, World!\", but with the second word in bold:

segments = [\nSegment(\"Hello, \"),\nSegment(\"World\", Style(bold=True)),\nSegment(\"!\")\n]\nstrip = Strip(segments)\n

The first and third Segment omit a style, which results in the widget's default style being used. The second segment has a style object which applies bold to the text \"World\". If this were part of a widget it would produce the text: Hello, World!

The Strip constructor has an optional second parameter, which should be the cell length of the strip. The strip above has a length of 13, so we could have constructed it like this:

strip = Strip(segments, 13)\n

Note that the cell length parameter is not the total number of characters in the string. It is the number of terminal \"cells\". Some characters (such as Asian language characters and certain emoji) take up the space of two Western alphabet characters. If you don't know in advance the number of cells your segments will occupy, it is best to omit the length parameter so that Textual calculates it automatically.

"},{"location":"guide/widgets/#component-classes","title":"Component classes","text":"

When applying styles to widgets we can use CSS to select the child widgets. Widgets rendered with the line API don't have children per-se, but we can still use CSS to apply styles to parts of our widget by defining component classes. Component classes are associated with a widget by defining a COMPONENT_CLASSES class variable which should be a set of strings containing CSS class names.

In the checkerboard example above we hard-coded the color of the squares to \"white\" and \"black\". But what if we want to create a checkerboard with different colors? We can do this by defining two component classes, one for the \"white\" squares and one for the \"dark\" squares. This will allow us to change the colors with CSS.

The following example replaces our hard-coded colors with component classes.

checker02.pyOutput checker02.py
from rich.segment import Segment\nfrom textual.app import App, ComposeResult\nfrom textual.strip import Strip\nfrom textual.widget import Widget\nclass CheckerBoard(Widget):\n\"\"\"Render an 8x8 checkerboard.\"\"\"\nCOMPONENT_CLASSES = {\n\"checkerboard--white-square\",\n\"checkerboard--black-square\",\n}\nDEFAULT_CSS = \"\"\"\n    CheckerBoard .checkerboard--white-square {\n        background: #A5BAC9;\n    }\n    CheckerBoard .checkerboard--black-square {\n        background: #004578;\n    }\n    \"\"\"\ndef render_line(self, y: int) -> Strip:\n\"\"\"Render a line of the widget. y is relative to the top of the widget.\"\"\"\nrow_index = y // 4  # four lines per row\nif row_index >= 8:\nreturn Strip.blank(self.size.width)\nis_odd = row_index % 2\nwhite = self.get_component_rich_style(\"checkerboard--white-square\")\nblack = self.get_component_rich_style(\"checkerboard--black-square\")\nsegments = [\nSegment(\" \" * 8, black if (column + is_odd) % 2 else white)\nfor column in range(8)\n]\nstrip = Strip(segments, 8 * 8)\nreturn strip\nclass BoardApp(App):\n\"\"\"A simple app to show our widget.\"\"\"\ndef compose(self) -> ComposeResult:\nyield CheckerBoard()\nif __name__ == \"__main__\":\napp = BoardApp()\napp.run()\n

BoardApp

The COMPONENT_CLASSES class variable above adds two class names: checkerboard--white-square and checkerboard--black-square. These are set in the DEFAULT_CSS but can modified in the app's CSS class variable or external CSS.

Tip

Component classes typically begin with the name of the widget followed by two hyphens. This is a convention to avoid potential name clashes.

The render_line method calls get_component_rich_style to get Style objects from the CSS, which we apply to the segments to create a more colorful looking checkerboard.

"},{"location":"guide/widgets/#scrolling","title":"Scrolling","text":"

A Line API widget can be made to scroll by extending the ScrollView class (rather than Widget). The ScrollView class will do most of the work, but we will need to manage the following details:

  1. The ScrollView class requires a virtual size, which is the size of the scrollable content and should be set via the virtual_size property. If this is larger than the widget then Textual will add scrollbars.
  2. We need to update the render_line method to generate strips for the visible area of the widget, taking into account the current position of the scrollbars.

Let's add scrolling to our checkerboard example. A standard 8 x 8 board isn't sufficient to demonstrate scrolling so we will make the size of the board configurable and set it to 100 x 100, for a total of 10,000 squares.

checker03.pyOutput checker03.py
from __future__ import annotations\nfrom textual.app import App, ComposeResult\nfrom textual.geometry import Size\nfrom textual.strip import Strip\nfrom textual.scroll_view import ScrollView\nfrom rich.segment import Segment\nclass CheckerBoard(ScrollView):\nCOMPONENT_CLASSES = {\n\"checkerboard--white-square\",\n\"checkerboard--black-square\",\n}\nDEFAULT_CSS = \"\"\"\n    CheckerBoard .checkerboard--white-square {\n        background: #A5BAC9;\n    }\n    CheckerBoard .checkerboard--black-square {\n        background: #004578;\n    }\n    \"\"\"\ndef __init__(self, board_size: int) -> None:\nsuper().__init__()\nself.board_size = board_size\n# Each square is 4 rows and 8 columns\nself.virtual_size = Size(board_size * 8, board_size * 4)\ndef render_line(self, y: int) -> Strip:\n\"\"\"Render a line of the widget. y is relative to the top of the widget.\"\"\"\nscroll_x, scroll_y = self.scroll_offset  # The current scroll position\ny += scroll_y  # The line at the top of the widget is now `scroll_y`, not zero!\nrow_index = y // 4  # four lines per row\nwhite = self.get_component_rich_style(\"checkerboard--white-square\")\nblack = self.get_component_rich_style(\"checkerboard--black-square\")\nif row_index >= self.board_size:\nreturn Strip.blank(self.size.width)\nis_odd = row_index % 2\nsegments = [\nSegment(\" \" * 8, black if (column + is_odd) % 2 else white)\nfor column in range(self.board_size)\n]\nstrip = Strip(segments, self.board_size * 8)\n# Crop the strip so that is covers the visible area\nstrip = strip.crop(scroll_x, scroll_x + self.size.width)\nreturn strip\nclass BoardApp(App):\ndef compose(self) -> ComposeResult:\nyield CheckerBoard(100)\nif __name__ == \"__main__\":\napp = BoardApp()\napp.run()\n

BoardApp \u2585\u2585 \u258b

The virtual size is set in the constructor to match the total size of the board, which will enable scrollbars (unless you have your terminal zoomed out very far). You can update the virtual_size attribute dynamically as required, but our checkerboard isn't going to change size so we only need to set it once.

The render_line method gets the scroll offset which is an Offset containing the current position of the scrollbars. We add scroll_offset.y to the y argument because y is relative to the top of the widget, and we need a Y coordinate relative to the scrollable content.

We also need to compensate for the position of the horizontal scrollbar. This is done in the call to strip.crop which crops the strip to the visible area between scroll_x and scroll_x + self.size.width.

Tip

Strip objects are immutable, so methods will return a new Strip rather than modifying the original.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nOVdaXNcIkmS/d6/QlbzcZuccI97zMbWdKFcdTAwMTPd6GB3TMYpkFKAOCSktv7v645UXCI5XHUwMDEyoYvKYtVtqiqSI4h87v6eh4fHX38sLf3oPDbLP/619KPcK+bDWqmVf/jxJz9+X261a406XcL+v9uNbqvYf2a102m2//XPf97mWzflTjPMXHUwMDE3y8F9rd3Nh+1Ot1RrXHUwMDA0xcbtP2ud8m37v/n3Xv62/O9m47bUaVx1MDAwNYNcdTAwMGZJlUu1TqP1/FnlsHxbrnfa9O7/Q/9eWvqr/zsyula52MnXr8Jy/1x1MDAwNf1Lg1x1MDAwMVx1MDAxYWVHXHUwMDFm3WvU+4NcdTAwMDVcdTAwMTRotdBavj6j1l6jz+uUS3S5QmMuXHUwMDBmrvBDP453K1AvX/dcdTAwMGWLK6nWib/e1M3LjcHHVmpheNx5XGafp1wiX6x2W5FBtTutxk35rFbqVPnTR1x1MDAxZX99XbtBszB4VavRvarWy+320GtcdTAwMWHNfLHWeaTHpHh98HlcdTAwMTL+tTR4pEf/SjlcZjRcdTAwMDJ4kFx1MDAwZZXE14v8auVcXKC0tVJIROE1yJFhrTZCulx1MDAxMzSsf4j+z2BghXzx5opGVy9ccp6D6FxuZTN4zsPLlzUmXHUwMDEwylqtUGlnhFevz6iWa1fVXHUwMDBlPcVi4KQ2wlx1MDAxOIHCOTdcdTAwMThnu9y/XHUwMDFmXGJSKbruXHUwMDA3N5Q/vrlV6mPjP9FcdTAwMTmrl15mrN5ccsPBiPnCelx1MDAwNE+D13SbpfzzbVx1MDAwN2O1Rk9DoLl5vVx1MDAxZdbqN6NvXHUwMDE3Noo3XHUwMDAzpPRcdTAwMWb9+89cdTAwMGZAlO5KXHUwMDFjRK1cdTAwMDaNXHUwMDAy3OxcYl3LVm6ue8t7d1x1MDAxOXl2eFxcObnPZdeWXHUwMDEzjlBtXHUwMDAy0MpcdTAwMWGp0ErrnDfjXHUwMDE4ldJrXHRcdTAwMDRTacEkXHUwMDE2ozR85b0wesEgaoWLgyhISSZcdORDZsZo+WRzm1x1MDAwNl8olNTRQWbL7excdTAwMWPcqIRjNFx1MDAwNTJcdTAwMTDgnfZgwVx1MDAxOedgXHUwMDE4pNrIQFx1MDAxOSfoXHUwMDEyoFOj40pcdTAwMGVEQUvp6D9YOIjqOIhaa5SQxsLMXGI93y/s7VxcaFdoXHUwMDFjXHUwMDFj3ubP7zeJ37R/LUJBvOlGVWC1oWl3XHUwMDA2vFFgh1x1MDAwMGqkXHUwMDBmiFx1MDAwM3hPXHUwMDFlSmilMbFcYkWwVtFQXHUwMDE3XHUwMDBloVx1MDAxMEtFUSjiNqD97IH+NpXLy3RxOXucq96U16pcdTAwMDfrXHUwMDFiISTdiTpcYoiFKidccjqg71xmw1x1MDAxMFVcIiBcdTAwMGaqtFx1MDAwMkVE1erEQlx1MDAxNCyRXHUwMDE0TTFg0bio9bFcXFx1MDAxNFx1MDAxY01cdTAwMTdFNz27XHUwMDFi9TZvdNi4vu5cdTAwMTazcFx1MDAxOXZLjX27l3CMXHUwMDAy6sBcdTAwMWKhKcxcdTAwMTNcdTAwMDR8RFx1MDAxMD2HeU1cXNRcYutQ9mNKciHafy25lPm5UT9cdTAwMWa5JHxcdTAwMWNEtfDOXHUwMDAy+plcdTAwMDHaetquLtv1PVxym5uynFp7Slx1MDAxZlRPk1x1MDAxZefpzlKgJ1x1MDAwNmdR05/aXHJLesJtgI6UvDGoPGKSqahw4KxcdTAwMTBcdTAwMGJcdTAwMDdRXHUwMDE5n3RykmKbfIdcdTAwMTNdllx1MDAwNd+53Xpyjerm8Xr2XGbyp/WkK/pcdTAwMTRgIDmvRLLeoSBHaUYwqomMXG5E68lFXHUwMDE5n9ysXHUwMDEzgDWk6oWaX6SfXHUwMDBmRq0wcVx1MDAxOPXgPOiogHhcdTAwMGKjK141fKdzs1Jo3+CtKUH7ot5MOEbBXHUwMDEzXHUwMDA2lVx1MDAwNVx1MDAwMUpcdTAwMDLFe1x1MDAxY470ylx1MDAxOYKGoD+EQYr3oJKLUUnwcdZEXHUwMDAy42JglD4pXHUwMDBlo8zMlNJmdozuhL3qWlXmMlx1MDAxZFFcdTAwMGXXzcNj2OydJFx1MDAxY6NSyoCYptBcdTAwMTTKhTM4mrwniFxuY+mOWCFRJDgxSjZGMUBoOb/M6JzcqME4iFwiKE30Rjo9M0b1zXpYKFXu08f3WpRy5+FlfT1MOEbRqEA68j9cdTAwMWWU0TKShntcdTAwMTZMXHUwMDEwOMKuQk5MYYKdKPlcdTAwMTJA4qNcdTAwMGKHUFx1MDAxZJu7d2hQ0q2ZPeu0eqMuOnBdusruXHUwMDE1W6dcdTAwMGYtma3m80lcdTAwMTdM7EWV8kQzXHRcdTAwMDFcdTAwMDCRtYyfWSdA4ywvjoIzXHSGqJVcdTAwMTL418JBVMk4iKKj2dL+XHUwMDFkq0v6+mJze9dm95dVuYHb+6enzZxNOkTBm4Cw4STxXHUwMDFhXHUwMDAxUo5hXHUwMDE0XHUwMDAzLSjWkGJcImbuklx1MDAwYlGyMbpjOMdF+jlRUVx1MDAxZFx1MDAwYlFtke5cdTAwMWH42SU9bi7bo3C7lrtfbV5f7eNtRVxc51x1MDAxMlx1MDAxZeaVkIEha1x1MDAxNN4rXHUwMDBmVo2GeVwiXHUwMDAxSMhcdTAwMTDSeUy0oDfaWWW1XTQn6lxcbF6UvIvxRkaXSN+C6F7n8r6mbnqnzZW73HLnMXN8VF3/XHUwMDFkIGqEJS6qnZMjqXvOOVx1MDAxMVd1gqhcdTAwMDCr/eSKJS6FsVx1MDAxNtTCIVTGMlFcdTAwMDM0a1rI2X3oxe7DRqHaq4Rdd7ZV92erjfr+RdLDPNHMwFiHJJPAOI3KjkDUXHUwMDA2XHUwMDAyXHUwMDFk31x1MDAxOGe4jiixXHUwMDEwReEleVHQi5ZysjLWiyryoUJqNbtcdTAwMTNcci+ecvumqFx1MDAwZm/NQ72zv3N73To/TLhcdTAwMTNNOZJD9D3BSclcdJtcdTAwMTGEelx1MDAxMVgwSoNcdTAwMDaLTiVXLCktkWJeRFksXHUwMDA2QJ1cdTAwMTdxXHUwMDAwXHUwMDA1Vlx1MDAxMHTv9OxcYr16KKZPa21wl9nH7VpVmePc+i9Ois5Q6GRJXGZJ+qaEQZr3KD6eMVxugdJkrFZcdTAwMDOp/YiLSlx1MDAxYUbpXCLxXHUwMDE05Vx1MDAxNm3908ZjlL+wgWhZxZvVou2KKp6c7Oid5mrT5DLd6nphM+lOXHUwMDE0REA6wztcdTAwMDOoud4uUpPAb+BcYj2kokjqk2Am8CS3jMRcdTAwMTlnhV84JlxusVFcdTAwMWWFXHUwMDE2Xoj3rCx5XHUwMDExVjvdwoYt75ZqXCJzXuymdlx1MDAxYlx0Ryj5UGk8ek1cXFR7gcNy3itcdTAwMTF4hSRDXHUwMDE0XG6Z4Fx1MDAxMlx1MDAxMpLxxEXkwiXtnYivXHUwMDE1JXySdphcdTAwMWSehUZcdTAwMTWuWts7t08rXHUwMDA3t1f3ze398/VMwuGZsi6w1pIs1I6IJrnRXHUwMDExfMpAopTOoqbJwORWiqIgLi1cdTAwMTDEwvFQXHUwMDFiW2+PLJSMtbPT0OPMRerk5nxv67540d3aL4dcdTAwMGZcdTAwMGb791x0hyigXHJcdTAwMTR7SFCS5LBcdTAwMWGN8Fx1MDAwNGAgnU86RHlcdTAwMGLJLbe3aLVfQFx1MDAxN+ohNtukpLXo4Vx1MDAxZEp+fcs9XFyd7tQz9+XD7PLKSemyW096OpRcdTAwMDHKy1x1MDAxMp5cdTAwMWOQ5FxyIUNcdTAwMDAl7qlcdTAwMDMhNTlZrZXxNsGLSob/035cdTAwMDG1fHwps1x1MDAwNE9cIvZcdTAwMWRL8/dcdTAwMWJcdTAwMGad86Otq8xarry6fX2xXazmz1x1MDAxM1x1MDAwZdFcdTAwMTS5UMF1MsJpY6xcdTAwMWYpZSaM2sAp+lx1MDAxMUT0SEol14tcdTAwMDJdsDSChcuHmtgw7zQ4XHUwMDE33Y79pk7avYHbTmpzt3l3XHUwMDEwbuyk6j19eZD0ZFx1MDAxM3jL2+eFQCSkRpcwnnNNXHUwMDE4KND9XHJ3hGKdXFylXHUwMDA0QCYk1OIlRH188VxiOO2dXHUwMDE3XHUwMDA2Zlx1MDAwZvS5Wlx1MDAxNcqNVLpWz2RrOrO50fBcdTAwMGa/WMvPUuBkXHUwMDAyQqDXllx1MDAxM0p+dFWJMCqJXHIxepBcdTAwMTdtXHUwMDEyi1FjUJOkc4vmRJ2MdaIgOIXt5DtcbpzOT1x1MDAwZotccn9YujqrXHUwMDFkZU+0bj21Vs5cdTAwMTJcdTAwMWXo0fDKJil2pYiWXHUwMDEy3Vx1MDAxY07ZO7qsSMqT4JdcdTAwMWGT7EXRWuJcIkYvmlpyJjbhpFhcdTAwMWPCe7YsZc+3m1x1MDAxYrWtjaNMa2tXn3SOz07bSW+TI6VcdTAwMGVcdTAwMWNcYqKhxEhp1ofVkldcdTAwMTA4a/t1I+RpXHUwMDEznFx1MDAxMVx1MDAwNcnKwZk59iCZV1x1MDAwNV5spT2QNDBWKJw9zvuV8/rpffWyU8BcdTAwMTW3WXkqrJVcdTAwMWbuXHUwMDEyXHUwMDBlUfAuIDFvhffaaFx1MDAwMGlGMKpcdTAwMDKKn2iIqYJEmVxcQe/J0Kywi0dFMX7fp1x1MDAxMZr3jNvZNydcdTAwMWbs7dhuSp7ByeN1/cGmsVPYLCdcdTAwMWOiSlx1MDAxMDxIXHTT/UUhhVx1MDAxZNlcckJyMTCaVD1xVMNro8mFqDZcbr3Ri1bH7OPT9oCO63tRzVx1MDAwZdH1Qlhv3e1cdTAwMTfSXHUwMDBltleObjZuXHUwMDFl1sKjxEOUXHUwMDAzPVFRYclcdTAwMGaBXHUwMDE4dqL9tKi3gvyr8kbK5Fx1MDAxNo9cdTAwMDCS/Vxit4iCPt6LUtBcdTAwMTDc9WB2Lrpmd+5ztfW1p/AqbNbq4DauNm5cdTAwMTNcdTAwMGVR8p+BIMmOdPspkFx1MDAwYmdHMcqro16C1nR7olQvcSBVvDuVSfOCgVSa+I54zit05Edmz91X7/Nb6V1cdTAwMTf2Llxua2m7nXdHO+W4XHUwMDFhp1x1MDAxMbBcckN0tFx1MDAxZfP1VaV8u1p+XHUwMDA3Ru0sbUVZXHRpZTn1NJq6573JXHUwMDA2eEcl31x1MDAxOTDuUyVOnVa+3m7mW4SBcZxqrVx1MDAwMmOs05ZmXurIkskrTlx0oUG/o5TWRJ9hUs8xxWu5qL5cdTAwMGWnL1x1MDAxN1x1MDAwNsCK3O+Gvdjcv8Dl5ulx+7RSa/m6yVxmmndccqEw32o1XHUwMDFlfrxe+fvPae+bOd+9XHUwMDE2+TOzVz0uioNmoVx1MDAxM+7ordne9+VvybAucDZ+M4vgLXJcdTAwMTbf0c5v+bKS71xcXHUwMDFk7Fx1MDAxZjdSVVkopo+PN1RcXD5iqnmNrj99azM/bblcdTAwMTZNXHUwMDExTyalN8yjjXRcdTAwMDFcdTAwMDV+0CwriG/Hl2HTs/LqjVx1MDAwMFCpVMatSipHI1BI4ZY+30VcdTAwMTbTX62KXCJT4HkvXHUwMDAzyU2DUX3+umHVaMlcdTAwMWJWv45FT7Wq89LpKt5V7vaq+96Yxoo5yJz8juhcdTAwMTdcdTAwMTCf6rA0o85F6/LfQr8tbu7Ui4VO53Hn3pnlp9Ku6vaSvqShdSB5r1x1MDAwNVx1MDAxMoSUVsRyhlxyQEEgyd+DI1x1MDAwNjy0pTtpXGbIglRCLdy6MFxijMcoXHRJ40HNzn+2lrP3XHUwMDBm4U1W5S5219IrWbVij4pJhyh4XHUwMDE1oNFWcl9cdTAwMGIyWTWGUFx1MDAwMVx1MDAwNlx1MDAwNXlp3tGdZI7OXHUwMDA1XHUwMDE4Xi9cdTAwMWNH91x1MDAxMYowuqbRryrRZvZUhzvdyJz27uzmyurxzr06XHUwMDBm5ermL27DNsvCMFx1MDAwNqSYvSdcdTAwMDLurbejPYFcdTAwMTVcdTAwMDaSN1x1MDAxYzpP0TvJzVm80t5TXHUwMDE0WDg3qlxcrFx1MDAxYjVWSVx1MDAxMlc4e6TvPS7fhPlwfTt31MvlTn0h3HWtxPNcXCG04jVGr7mkeyTOXHUwMDEz0SWKS1x1MDAxNJIgqpz93I7YXHUwMDE4omtcdTAwMDOtaFx1MDAxMJKbwGk1QT7CM9tGYl7cKFx1MDAwNqL7dV52XHUwMDFiXG6Dyir4ukW3aURcdTAwMTex97jTbsPxg6rD9uPBtj+utWcjulPl429EoMthWGu2J9tcdTAwMTTxxjibQsKIXHUwMDA2Zdzs4nE1tfm0dbyXlb1cdTAwMDPZvs4101x1MDAwZvthXFxcdTAwMDJxqlHNz+1cdTAwMWKiXHUwMDFlfI5cdTAwMDa3XHUwMDFmXHUwMDEz0c6Fz05fXHUwMDEzMUHJus1+cot5Ja9R47hNsXS0xtHryb+p6IlcdTAwMWSvNuUoKqDtb9Hs96yXoyZcdTAwMDVcdTAwMWVcdTAwMTVyt/D5iMdP2FSSsK+mtPXkXHLsxsvZXHUwMDE3yVx1MDAxZkXtonq/bVx1MDAwMVx1MDAxZXZ6XHUwMDE1n013u+eYbOhbctVAmDFeSEUqeVQ2qn5iXVvlUVxu87l64kq+QN7ke9BvKfJcdTAwMTn9lfnIZMBTx+f1nOZ2rO9pUrPSOzg9Or9q1zOt+onRqdst7dLJxqc3XHUwMDAxeUyJwnpBXHUwMDE4XHUwMDFjQadcZlx1MDAxY0hcdTAwMDOWa+GI9Hxcbp2AXHUwMDA15ybw8a9AJyrlXHUwMDAwo0dcdTAwMDH8Nuh8K/NcdTAwMTa79qiNJOeJcnbJeLeVrm0yc7BcdTAwMTdP9e4y3jzI9C9u8zVcdTAwMDMhN4RBL/uiUWvrRurd+SwuuiOSdJxcIsKb3I3rXHUwMDAwaHjBZ+G6KHlcdTAwMTWfeFPsRH20XHUwMDEx9FtcdTAwMThN27ua9duVxure6nlnuXZb3ln7xZ27Z0trXHUwMDEwj9GOlCNnf4drNblHXHLvXHUwMDE1XCKhJrj1QoJrNWl00jrxO0b56aduxm9sXHUwMDAzT1x1MDAxOFx1MDAxNVx1MDAxNFneUU58mOnKXZu9W1x1MDAxM+srOzv32Vx1MDAxNYzd2fblYV58XGKh2lx1MDAwNki+UlxuirROR49cdTAwMTjtl8FpRVxi9U5JQb/d51LDxXJJlfLjXHUwMDAwlcR1pVx1MDAxNUSB0ViylkhkXHUwMDFiJDYwMF4765ltSHKYalx1MDAxNKLIXUeV/Uo3+nJholxuM2m9d5KRT1ukXdcvU3p143LvcDZcdTAwMTX257T33czVTHhSf+q08+Ji9XBv/ab31PgqdUdwILo+XHUwMDFm51x1MDAxZp/aIOZsXHUwMDE0yvdcdTAwMWPEuFJO5eHo2nWaZ49yZb1X2c2sJZ6gXHUwMDEwOVx0LFx1MDAxYc3HL1x07q0w3v1JXGLD59xcdTAwMTlF/jW5PfRcXL9kxSyc85+6Ic9xu1x1MDAwZfOOKuiLsL0s072101x1MDAwYpHN71x1MDAxYtvJp5dlsp0/qdxcdTAwMDClXHUwMDA04tDcjlSPIJS9v6BcdTAwMTBoiK55Jz/ZcFxcXHUwMDE1TbkyIVx0XHUwMDAx3lx1MDAwN9JbXHUwMDAwj9yHSkyo3yDvT+BFQzpUOOWjNVx1MDAwNT8hXG5cXH6iXHUwMDE27oCmaD/xXHUwMDExiEq2SVx1MDAxZF1cZn1cdTAwMTOhd729w4vl/HZnZ2VHnl2J1XVI/uI16lx1MDAwMKxcInhwP3mMXHUwMDFlY/HcidRcdTAwMDSSXHUwMDBiMFx1MDAxNOk8XHUwMDFmLeBJmlx1MDAxN1x1MDAwNU6kWLt4zSHIaOMwalx1MDAxNUU4mozZI31ntbZXrbWrdrmi0o3Udc2ellx1MDAwYknHKFx1MDAxYVx1MDAxOWiB5JpcZoAzY/2cTUBcdTAwMTSagihywjfBzXKl5lx1MDAxM7VcZs4tXHUwMDEzXHUwMDExab/4rdkygFg2yrkh3nw/O0Srayd+67GweplXXHUwMDA3T83zfPmweFx1MDAxOXcqeEJcdTAwMDK9dC6g6GmRmDc5Slx1MDAxY85DXHUwMDE4TVx1MDAxMVihUJI7zmvxPTJcdTAwMGZkoIhE8mJcdTAwMDFZgvGRj1x1MDAxOVx1MDAwNHqv+Cwzy1x1MDAwZV8hUZLxxTbB8mHo1d8p875Vjs3TXHUwMDAyplfBWURcdTAwMDPvqDG6gHSx2Mvu11x1MDAwZdPiuuuf0ie4XHUwMDFld1x1MDAxY2lC1jPYXHUwMDA0LHeC9rxcdTAwMWRFXHUwMDFhXHUwMDFjWXAjscY9KZSxRDC9/tySRizXhcCQtCBkU5RcdTAwMTAmmltcdTAwMWGsaSA3vFx1MDAxNMJb0d8zM8Z1uaU198iYz3Lz91pcdTAwMDBxh1xiw/2kXHUwMDA1dMq9zkTw2/hcXFx1MDAwNDvDfmO5mbF/WMrc7V9cdTAwMWTpjebVQ+8207kut+1jsrfAXHUwMDEw5lx1MDAwM97agqiJXHUwMDE1Kj16yKly/eI7XHUwMDE0ktgpfvKs6Im1S6gn8JHoyVx1MDAxNi+bWYWWnjuKzVx1MDAwNdrftbnlNH1oiqliPsy1ZK+3msvmunj8y4LGXHUwMDAwmY1657j21K8uckOPpvO3tfBxXGJdfVuiXHUwMDAx3tdanW4+vGzTXHUwMDBig5c7XHUwMDE3uf3tMlxyoP+Oeuily2Htik3vR1iuXGbbZKdGXHUwMDEz83q504is4Vx1MDAxNGkoeXq71lZp9Cs1WrWrWj1cdTAwMWaeTFx1MDAxOdZU3/A80Vx1MDAxM5yDjO9cdTAwMWLG68rkqmdcdTAwMTfY01x1MDAwMfVcdTAwMGLKXHUwMDFh3/RcZlrYgMNcdJLxK1x1MDAxN21o1H+51IHU5Fx1MDAxYVR/Z4P83Fx1MDAwMtXkosZAcDrfSevpfy0mXHUwMDE0NVwi14EpxedcdTAwMWIrjUpGT1x1MDAxNXshhY6rXHUwMDAwXFy0XHUwMDE3cZKES2TW8q3OSq1eqtWv6OLAdfwoP3/01lxmXHUwMDExpm+zxS6PMiVcdTAwMDLPRf2aj6ZWpP5cdTAwMDbHXFzwXHUwMDE05JvPtFuS6lx1MDAwM+2lN8R7lHt5xqtcdTAwMGb7Ua6X3lx1MDAxZdT0nZ/RQUFAZkM3RIO1Rlx1MDAxYTM+Jkk0qK+6pNbWXHUwMDFib8aGXHUwMDE05tud1cbtba1DU3/QqNU7o1Pcn8tltupqOT/mL+grRa+Nmn+T33E4Plxm/rY0sJH+P17//p8/Jz47XHUwMDE1i+H+1XH4XHUwMDBl3vCP6J/vdl3g4lx1MDAwZsxcdTAwMTQ0rzp6XHUwMDE4+Fu+a3rQSqTvXHUwMDAyXGacMoq4hTPK4OjiOlx1MDAwMZ9cdTAwMTNcIkKTojTuk1x1MDAxNXRcdTAwMTNJTUBKQTjuXHUwMDEyQoJcdTAwMTYnkXn0JvBoXHUwMDE0N1j2Q2vJr9VcdTAwMWbgSVx1MDAxMkj8f+a6ROCclyTDvFx1MDAwM7qFkSbdr16CXHUwMDBi6lxy3TuCOadcdTAwMDOMn+64hr/F7+Q/YnHEP2NcYnqn94hcdTAwMTNFXHUwMDE4aVx1MDAxYzbqPKRA8lTvKM5pZFx1MDAxZs9ubaPcLGbO2/uq53Xq9EOFXHUwMDBm89NEzlx1MDAwNzTd5LWVdlx1MDAwZYRcdTAwMTjO2fJOzUDxjk3pnJRcbj/XXmWy+1AzaVwiXHUwMDEwXHUwMDEyrHZfeLTJK4K+dmP+VE10eUfM/na1dLV2SVx1MDAxYXPz5GB/q1hdXHUwMDA0TfR8N5MmiZ5H9TFagT5eXHUwMDExkSP22pnZN8xOx1NcdTAwMTJphVTE5/hcdTAwMDRuzV1cdTAwMDEs4qhnQKJcdTAwMWRcdTAwMTSUXHUwMDE09y6Unz1cdTAwMTJpomdcdTAwMDBhXHUwMDAyR1x1MDAxZu1IgFx1MDAxMmsk/TXuKIhCoyT2Yzw3K3U+kjP5mUyxzlxuObfVxm/lXHUwMDE101x1MDAwM8wrZfBcdTAwMDF6a9FYllx1MDAxZdxcdTAwMDd7smhcdTAwMTKcXHUwMDA146JcdTAwMWPBZbdK/oypXy2Jnlx1MDAwN4UuXHUwMDAwzjtznLGSXHUwMDBmMpqsmoRcdTAwMTe8QEKciIZlzLhO+51ITTyE+Sc1jt530pp474XxR7p5x4fcvKPubHrUSqL3sjJwJHZI+TG43UhCXHUwMDA3+KhccuuE1EZ5bsvyXHLbVEFDXHUwMDAwXHUwMDE0XCJcYsaOTFxm3KR1voC7oDrSqJJbbMrIdtSfzotgQaFmfodf/1r3XHUwMDE1cU5AhFx1MDAxNEi1XCJcdTAwMGIk7uw35lC48Vx1MDAxZlqFkmjhc9Ob6f7r91VG8Vh6vjxcdTAwMDajd3qROHFkpm3MRXDeR1Olb/ZHrW489MJcXObx8WlVy+WM0vXK2q/0XCL+LS+S0nyomXL9XHUwMDAzd4zGkcMktFS8nmRRXHUwMDBiukNexi+WXHUwMDE2PeYx/43iqH/ajv9KdTStfppl+mBcZt+tM0hJVIJ2sdVcYsPLRqXSLidi6WXCqD5cdTAwMTapdXzrNG1cdTAwMDRZdZS8vmVj03dFJzFSp6znXHUwMDEzqsnXW66vJa8/ZGOqf4C1JetcdTAwMTJGO4VTemd+2MhcdTAwMDBVYJXnXHUwMDE0XHUwMDFjSC8hsmFrXHUwMDEwqo1cZri3XHUwMDA3elSWrHIsg8l1Q4pcdTAwMGL95tLyXHUwMDA0rDPqK0L1aFB7O4hPb1oywuWl53RcdTAwMWVqcpXCXHJK/pZcdTAwMDbJTU/S0mrF7W608vL3ZvuxQOpfXHUwMDFkhdBcdTAwMTdF6eh+37H6fetcdTAwMWRzg9mj9NHZzX796v60dt29yOS1PCnBdpjsKI3OkGrkQ1x1MDAxNLRA6e1o3SmRJ6GF8WSeXHUwMDE433huXHUwMDA29zG1rSmYXHRcdTAwMWK2JoRqkoOMdfi6XHUwMDFh/VdcdTAwMWP9ilx1MDAxNjFzTDg+Lv176SXiPiaBXHUwMDAyXGaN52PB30yR6aRrjFx1MDAxNPJcdTAwMWR1XHUwMDE3U+91XCJNXHUwMDE3hFxulCNpY6Xpn1x1MDAxYTmyOYyrZT2FXGLe4m7UlMqLT1uv8oHzvHvGcz9cdTAwMTIhJ3BcdTAwMDBSYVx1MDAwZcBcdHR82ICa0Fx1MDAwMseK/smyZi6rmGA1XHUwMDA1ky/gXHUwMDAwcZF+eihYiq5iUpwnXHTCfcOF9laPXHUwMDA3elBcdTAwMDFdI3fBrWelNm682OFr2Vx1MDAwN9FGQYzCsjDiqr5J3IN71HE9oHRa0vB/b+5cdTAwMTFcdTAwMGJg/kmNYfeLyIeRscVcdTAwMTc0XHUwMDBloKDrZ+9cdTAwMGaDXHUwMDA3qdZaeLjse2F2I1u73c6Bc8l2YOSZXHUwMDAydl7kw4zhxZAh/+UlXHUwMDA2SE5cXDllLZeifJ//slx1MDAxM1x1MDAxY9Yk9tE/1O1cdTAwMGKXQ6aRj0/sXHUwMDBlf5t8XHUwMDEwnfaRpv7fTT56g2DfS1x1MDAwMvlcdTAwMThcdTAwMWHPx8hcdTAwMDeXp8VcdTAwMTkvzbAw7zr+aPrNTqTxXHUwMDAy9yVBpygmeTJgXHUwMDFjyT2QTlx1MDAwYlx1MDAxMNGT/Vx1MDAxYe99fO1DibSHqHzCeJ1cdTAwMGZQsCZUxnKX7HFTNuTdjdJeKvLfWsFY41x1MDAwN7DAgfUri8a/xfhm5Fx1MDAxZdNDQSSGXHUwMDBiJTiuXHRyr9xBYFx1MDAwMlx1MDAxM+CVTor0XHUwMDFj4ZWy3HOByPHH2Mf0ziZcdTAwMDOyM2FcdTAwMTTc5Vx1MDAwMFx1MDAwNaBzgLxs/nuTjTjA8k9qXGarX8Q1tI7lXHUwMDFhUvSLe9/RwnzdbKz2uqc5dyGOb25qVX1cdTAwMWFmk801NK9cdTAwMDKR7XnBp11ZO9xcdTAwMWKaZjvgynVNXCKK7suUXHUwMDBl+5+lXHUwMDFhOKk63Y4lRHmbL1x1MDAxOdxcdTAwMTdcdTAwMWVcdTAwMGU8jWp8b8NcdTAwMThUVnxIaX2aaiz91//Wn5dcdTAwMWGmV1mZ4beZJ/uYNMRcdTAwMGZcdTAwMTJcdTAwMTLw8VvUKOKAtO85pWA6Jlx1MDAxMmni3lx1MDAwN55z4ZJcdTAwMWKB2JGaK1wiXCKBRd7VzkdcdTAwMTR4XHUwMDFiT0g+rSbIv5NIXHUwMDA3329RJGDSKqTl4lx1MDAxNW208dKSjejx7Shk/PRiO5+OfsinWOiP2OiMjGR6wFiKZlx1MDAxZZDPylx1MDAwND6uwFC8x/GdXHUwMDFmKuBSW3IqgMhV31x1MDAxZt2MMnVcdTAwMGL+0JCI8lx1MDAxMNEl5Vx1MDAwZp6X7u3YkFxcoPlwXGbNjVx1MDAwZpif/N51V6l4XHUwMDA09y+PgfeLKFxuQPwxcyRcInjh/Fx1MDAxZOdg1dxGXHUwMDBmM+kw18hgunS0JY5cbvvbiXZg4HSgNVx1MDAxZj/FvJDsYPR4XHUwMDAwXHUwMDEzOEt6SkpyXHUwMDBicko70lx1MDAxOc7BmurB7IQ2uZFcXPvPTlx1MDAxZlxc1+Hn2C/p06slkfa/QyxiXHUwMDEwKX6yiJVGvlVabjYn0oXI28yDLryO5dmo/nix21x1MDAxZvlm87hDs/Tq42j+a6WXrzp4x1x1MDAxZve18sPK5OV8XtH/48VQ2VwiynxcdTAwMWL++vuPv/9cdTAwMGZcYuGAVyJ9 virtual_size.heightvirtual_size.widthself.scroll_offsety = scroll_yx = scroll_xx = scroll_x +self.size.widthBoardApp"},{"location":"guide/widgets/#region-updates","title":"Region updates","text":"

The Line API makes it possible to refresh parts of a widget, as small as a single character. Refreshing smaller regions makes updates more efficient, and keeps your widget feeling responsive.

To demonstrate this we will update the checkerboard to highlight the square under the mouse pointer. Here's the code:

checker04.pyOutput checker04.py
from __future__ import annotations\nfrom textual import events\nfrom textual.app import App, ComposeResult\nfrom textual.geometry import Offset, Region, Size\nfrom textual.reactive import var\nfrom textual.strip import Strip\nfrom textual.scroll_view import ScrollView\nfrom rich.segment import Segment\nfrom rich.style import Style\nclass CheckerBoard(ScrollView):\nCOMPONENT_CLASSES = {\n\"checkerboard--white-square\",\n\"checkerboard--black-square\",\n\"checkerboard--cursor-square\",\n}\nDEFAULT_CSS = \"\"\"\n    CheckerBoard > .checkerboard--white-square {\n        background: #A5BAC9;\n    }\n    CheckerBoard > .checkerboard--black-square {\n        background: #004578;\n    }\n    CheckerBoard > .checkerboard--cursor-square {\n        background: darkred;\n    }\n    \"\"\"\ncursor_square = var(Offset(0, 0))\ndef __init__(self, board_size: int) -> None:\nsuper().__init__()\nself.board_size = board_size\n# Each square is 4 rows and 8 columns\nself.virtual_size = Size(board_size * 8, board_size * 4)\ndef on_mouse_move(self, event: events.MouseMove) -> None:\n\"\"\"Called when the user moves the mouse over the widget.\"\"\"\nmouse_position = event.offset + self.scroll_offset\nself.cursor_square = Offset(mouse_position.x // 8, mouse_position.y // 4)\ndef watch_cursor_square(\nself, previous_square: Offset, cursor_square: Offset\n) -> None:\n\"\"\"Called when the cursor square changes.\"\"\"\ndef get_square_region(square_offset: Offset) -> Region:\n\"\"\"Get region relative to widget from square coordinate.\"\"\"\nx, y = square_offset\nregion = Region(x * 8, y * 4, 8, 4)\n# Move the region in to the widgets frame of reference\nregion = region.translate(-self.scroll_offset)\nreturn region\n# Refresh the previous cursor square\nself.refresh(get_square_region(previous_square))\n# Refresh the new cursor square\nself.refresh(get_square_region(cursor_square))\ndef render_line(self, y: int) -> Strip:\n\"\"\"Render a line of the widget. y is relative to the top of the widget.\"\"\"\nscroll_x, scroll_y = self.scroll_offset  # The current scroll position\ny += scroll_y  # The line at the top of the widget is now `scroll_y`, not zero!\nrow_index = y // 4  # four lines per row\nwhite = self.get_component_rich_style(\"checkerboard--white-square\")\nblack = self.get_component_rich_style(\"checkerboard--black-square\")\ncursor = self.get_component_rich_style(\"checkerboard--cursor-square\")\nif row_index >= self.board_size:\nreturn Strip.blank(self.size.width)\nis_odd = row_index % 2\ndef get_square_style(column: int, row: int) -> Style:\n\"\"\"Get the cursor style at the given position on the checkerboard.\"\"\"\nif self.cursor_square == Offset(column, row):\nsquare_style = cursor\nelse:\nsquare_style = black if (column + is_odd) % 2 else white\nreturn square_style\nsegments = [\nSegment(\" \" * 8, get_square_style(column, row_index))\nfor column in range(self.board_size)\n]\nstrip = Strip(segments, self.board_size * 8)\n# Crop the strip so that is covers the visible area\nstrip = strip.crop(scroll_x, scroll_x + self.size.width)\nreturn strip\nclass BoardApp(App):\ndef compose(self) -> ComposeResult:\nyield CheckerBoard(100)\nif __name__ == \"__main__\":\napp = BoardApp()\napp.run()\n

BoardApp \u2585\u2585 \u258b

We've added a style to the checkerboard which is the color of the highlighted square, with a default of \"darkred\". We will need this when we come to render the highlighted square.

We've also added a reactive variable called cursor_square which will hold the coordinate of the square underneath the mouse. Note that we have used var which gives us reactive superpowers but won't automatically refresh the whole widget, because we want to update only the squares under the cursor.

The on_mouse_move handler takes the mouse coordinates from the MouseMove object and calculates the coordinate of the square underneath the mouse. There's a little math here, so let's break it down.

  • The event contains the coordinates of the mouse relative to the top left of the widget, but we need the coordinate relative to the top left of board which depends on the position of the scrollbars. We can perform this conversion by adding self.scroll_offset to event.offset.
  • Once we have the board coordinate underneath the mouse we divide the x coordinate by 8 and divide the y coordinate by 4 to give us the coordinate of a square.

If the cursor square coordinate calculated in on_mouse_move changes, Textual will call watch_cursor_square with the previous coordinate and new coordinate of the square. This method works out the regions of the widget to update and essentially does the reverse of the steps we took to go from mouse coordinates to square coordinates. The get_square_region function calculates a Region object for each square and uses them as a positional argument in a call to refresh. Passing Region objects to refresh tells Textual to update only the cells underneath those regions, and not the entire widget.

Note

Textual is smart about performing updates. If you refresh multiple regions, Textual will combine them into as few non-overlapping regions as possible.

The final step is to update the render_line method to use the cursor style when rendering the square underneath the mouse.

You should find that if you move the mouse over the widget now, it will highlight the square underneath the mouse pointer in red.

"},{"location":"guide/widgets/#line-api-examples","title":"Line API examples","text":"

The following builtin widgets use the Line API. If you are building advanced widgets, it may be worth looking through the code for inspiration!

  • DataTable
  • RichLog
  • Tree
"},{"location":"guide/widgets/#compound-widgets","title":"Compound widgets","text":"

Widgets may be combined to create new widgets with additional features. Such widgets are known as compound widgets. The stopwatch in the tutorial is an example of a compound widget.

A compound widget can be used like any other widget. The only thing that differs is that when you build a compound widget, you write a compose() method which yields child widgets, rather than implement a render or render_line method.

The following is an example of a compound widget.

compound01.pyOutput compound01.py
from textual.app import App, ComposeResult\nfrom textual.widget import Widget\nfrom textual.widgets import Input, Label\nclass InputWithLabel(Widget):\n\"\"\"An input with a label.\"\"\"\nDEFAULT_CSS = \"\"\"\n    InputWithLabel {\n        layout: horizontal;\n        height: auto;\n    }\n    InputWithLabel Label {\n        padding: 1;\n        width: 12;\n        text-align: right;\n    }\n    InputWithLabel Input {\n        width: 1fr;\n    }\n    \"\"\"\ndef __init__(self, input_label: str) -> None:\nself.input_label = input_label\nsuper().__init__()\ndef compose(self) -> ComposeResult:  # (1)!\nyield Label(self.input_label)\nyield Input()\nclass CompoundApp(App):\nCSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    InputWithLabel {\n        width: 80%;\n        margin: 1;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield InputWithLabel(\"First Name\")\nyield InputWithLabel(\"Last Name\")\nyield InputWithLabel(\"Email\")\nif __name__ == \"__main__\":\napp = CompoundApp()\napp.run()\n
  1. The compose method makes this widget a compound widget.

CompoundApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e First\u00a0Name\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u00a0Last\u00a0Name\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u00a0\u00a0\u00a0\u00a0\u00a0Email\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

The InputWithLabel class bundles an Input with a Label to create a new widget that displays a right-aligned label next to an input control. You can re-use this InputWithLabel class anywhere in a Textual app, including in other widgets.

"},{"location":"guide/widgets/#coordinating-widgets","title":"Coordinating widgets","text":"

Widgets rarely exist in isolation, and often need to communicate or exchange data with other parts of your app. This is not difficult to do, but there is a risk that widgets can become dependant on each other, making it impossible to reuse a widget without copying a lot of dependant code.

In this section we will show how to design and build a fully-working app, while keeping widgets reusable.

"},{"location":"guide/widgets/#designing-the-app","title":"Designing the app","text":"

We are going to build a byte editor which allows you to enter a number in both decimal and binary. You could use this a teaching aid for binary numbers.

Here's a sketch of what the app should ultimately look like:

Tip

There are plenty of resources on the web, such as this excellent video from Khan Academy if you want to brush up on binary numbers.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nOVdaU/jyFx1MDAxNv3ev1x1MDAwMjFf3pMmnlpvVY309MTSQCCsYe2nXHUwMDExMomTmCTOYidcdTAwMDRG/d/frUBcdTAwMTNncchcdTAwMDKM0400PVx1MDAxMNtJueqcU+de36r8/WVtbT16bHrrf66te72CW/OLbfdh/Xf7etdrh34jwEOs/3fY6LRcdTAwMGL9MytR1Fxm//zjj7rbrnpRs+ZcdTAwMTY8p+uHXHUwMDFkt1x1MDAxNkadot9wXG6N+lx1MDAxZn7k1cP/2n+P3Lr3n2ajXozazuBDMl7Rj1x1MDAxYe3nz/JqXt1cdTAwMGKiXHUwMDEw3/1/+Pfa2t/9f2Ota3uFyFxyyjWvf0H/UKyBko++etRcYvqNpVx1MDAwNDQ1lHP6eoZcdTAwMWZu4+dFXlx1MDAxMVx1MDAwZpewzd7giH1pXHUwMDFkzppcdTAwMTdnwVOnSW865dxWZn9jc/dm8LElv1bLR4+1frNcbu1GXHUwMDE4ZipuVKhcZs5cYqN2o+pd+cWoYlsw8vrrtWFcdTAwMDN7YnBVu9EpV1x1MDAwMi9cZoeuaTTdglx1MDAxZj3amySvLz53xJ9rg1d6+Jek2lFEU05cdTAwMTRcdTAwMDHDxetBe7XQ4GggWjNcdTAwMTCEMdAjrdpq1HAwsFW/kf7PoF13bqFaxsZcdTAwMDXFwTmiXHUwMDAwXklcdTAwMGXOeXi5VymoQzhjnFHNKSHy9YyK55crkb01RVx1MDAxZInNUJJcdTAwMTEmqVx1MDAxMmzQXHUwMDEyrz8mXHUwMDE0NKGcXHUwMDExMlx1MDAxOFT7+c1ssY+Pv+I9XHUwMDE2XHUwMDE0X3os6NRqgybbXHUwMDAzX2OYXHUwMDFhXFzTaVx1MDAxNt3o5WOUUoZQrYlcdTAwMTn0Rs1cdTAwMGaqo29Xa1x1MDAxNKpcdTAwMDO09F/9/vtcdTAwMDIwpYQn41RcdTAwMTNcclx1MDAwMsDMjtM9dpJxm72oVT047Fx1MDAxYz1cdTAwMTGze8C/Lo5T9k44xWF/XHUwMDEzqMrBW+VcZlxiUcA4XGYjVVx1MDAxYYdcdTAwMDHiR1HBXHJ2i1pcdTAwMDaqUdtccsKm20YkTIKrcLBcdTAwMTlcXHBcIimlXHUwMDEz0MqBOJpcdTAwMTIjXHUwMDA0cI5cdTAwMDRjMIpWMJJIzpn8NLCaT1x1MDAwMWucmKNY5VpQI1x1MDAwNZiZsZrb2bvZolx1MDAwZlutWq8rRHGDkqfrIFx1MDAwMasjePsnUUqIXHUwMDAxhVomULFGQKpcdTAwMWSthcbxXHUwMDAw0Fxu4CNBylx1MDAxZEJBMi1cdTAwMTVVRulxlDLtXHUwMDAwXHUwMDEwjlx1MDAxYU9cdTAwMThwS6xRlCrNSJ9Sq4dSr1bzm+FEjIJcdTAwMTKJXHUwMDE41YIowbWSM2PU69ZPN77dXHUwMDFmXFzlstmb6kaudbW3XV5cdTAwMDSj7zXjz4BRXHUwMDFjeUMkXHUwMDEwoVx1MDAxMa7EsGGQXHUwMDAygpRLg8rGjIXOUnN+yZVMsnF8Uu5YRyFccuCEzpBcdTAwMTJiXHUwMDFjoJQ5XHUwMDEyQFx1MDAxOJRQwlAshVx1MDAxYVx1MDAwNShcdTAwMTeUMWpibfxcdTAwMTlcdTAwMDCqmE5cdTAwMDIo+jBcdTAwMWMxodTM+DSsV7nNRlx1MDAwN7nuUfd8v51vcdVopFx1MDAxY59SXCJcdTAwMDJcdTAwMDFcdTAwMDEqXHRcdTAwMDe0pSP4VFx1MDAwZVCBRGVUSFx1MDAwNVIsXHTQO3ScXHUwMDFmXHUwMDA1UIZyYlx1MDAxOFx1MDAwMbWCXHUwMDEz/TSE6uRpXlKMXCJcdTAwMDSfXHUwMDE5oHv5ulxmy2euOLmqXHUwMDFmh8F5buN6p5NygGqOYVx1MDAxMZM4W0itXHUwMDE4lyNcdTAwMDBcdTAwMDVcdTAwMDdRY7hmiFWtR83HfPik7E5r+Dh8XHUwMDEynFx1MDAwNriWn1x1MDAxNzV9jlx1MDAxMdUyXHUwMDExoFx1MDAxOERcbowk+excdTAwMTBccjJnV0FwuXta7mZ36MZGePh0t5duiCqG8ZA0SmBgLVx1MDAxMYxmXGKiUjJcdTAwMDSwZIDgXHUwMDExeFAuXHUwMDA10VKpNFx0n1x1MDAxM1x1MDAwMFx1MDAxOWPKj1x1MDAxOVxccpzCcVx1MDAxMp9cdTAwMDN/P5AwwFx1MDAwMn955XsyLF+vXHUwMDE5XFxcdTAwMWTDUuT1XHUwMDA2Jjo28s2Th92jTKV0obY297fJ6WbLO9Drr+d9/33y2z5f/PW47rmt/Xz1kGarXHUwMDE33lGzenFcdTAwMTJcdTAwMGV/yo/Pd9vtxkPsfV9+S+KSxrBSsHgotSSXhu4/RiNhXHUwMDEyaaRBgVx1MDAxYVxuYd+i0eTOnEyjiluodNpe+omk3o9IU1x1MDAwMzrGx+nExuiEXHUwMDAxNjZCXHUwMDEzmc6QbTDWjSDK+099S0uGXt1x637tcWi4+uDE/lkz8e5cdTAwMGI9/LxnJFx1MDAwZZ25UfPLXHUwMDE2uus1rzSM6cgvuLXXw3W/WIzPXHUwMDFkXHUwMDA1/HBcdTAwMTffsZ2dRfNcdTAwMWJtv+xcdTAwMDdu7XzQtsUnK5qciTZCXHUwMDEzYSPSmVnmV0/Oj2vlzMP+9l6tK+tHJrpkqWeZXHUwMDExyDLGNNpcdTAwMTFcZnHYsOVHmXE4xnnKXHUwMDE4jFx1MDAwYthcdTAwMDfm9lx1MDAwNHdsXHUwMDFhXHUwMDFjuKVcdTAwMTBIOSm7x5hcdTAwMDPKSGyyTeRQYsbzJngpXHUwMDFlg3nC0oUmtXR4LWqSXHUwMDEzKqibIONcdTAwMDH6W/DduoON6FwiMsXb++vTXFzwcHK2XHUwMDE1h9o/9Vx1MDAxY+VtXGJrKlx1MDAxZI6ux3CDs7PUZlx1MDAwNMLCMTgmXHUwMDE4XHUwMDFhUUYwMljKcVx0UiBSTZokXHUwMDFj++Rcbj9cdTAwMDQ7XWpKJ6CXOlJcdTAwMTJF7dRtjb9cdTAwMWVcdTAwMDUvN4JK8vGGLFx1MDAxZNhlsXFcdTAwMWRPWHOFgVx1MDAxM5394cp+b/eCXHUwMDA2N1t1cXpPj8J6JnNZ99OuvUZcdTAwMDFcblx1MDAxYdpw9DFCXHUwMDFiMVx1MDAwZVxcITHg11x1MDAwMiNdJZeKZj9Fe5F7XHUwMDEy47t/XHUwMDE2v/Fu/1D8isRAVzFcdTAwMTQhXHUwMDE5XHUwMDFmz7fgXHUwMDFifVx1MDAxNU2/c1jyXHUwMDAytl1RXHUwMDE5kTO79HZcdTAwMTW0XHUwMDE3J1x1MDAxOFx1MDAwN4HKudFcdTAwMTKNOGUjXHUwMDEwXHUwMDA2h0hcdTAwMDGaXHUwMDE5gtJrlkHwh0qvfZzLmFwiep5s4TtGw88oyOe/XHUwMDFkdO/Pe3dunlSOXHUwMDAyUz2JVP3dwlb0d7FHt1x1MDAxZkpccpmYRqdAUTw4mUPaN/d6XHUwMDA10T5Sgp1vfDvdr9Jet1dLu7RjbzuUY8dcdTAwMTNcdTAwMDFExaPCV1ttn1NcdTAwMTP0JahasFxmMT5B2pmy8bdcdTAwMTK/iLRzQVx1MDAxM/HLJEhOyVx1MDAxY88p71xcssd446RVydz1mt6J37qst1ZB2y2G0XegfFNcdTAwMTR3zSeEhuhaKMNAg/LlQsPfNGjPTEi2v4e4o61cdTAwMDai8YRfXHUwMDA0vclVS1x1MDAxOP9cdTAwMTjCXHUwMDE5mT1cdTAwMDHf2rlcdTAwMTf+dnBcdTAwMWJcXJU293dP+d7N8U1SIUhqxFx1MDAxNyiaXHUwMDEyXGb4lFbow1x1MDAwMMRcYnCFI21lndBcblx1MDAwNZGnPadhn2VRXHUwMDFlL877qeGLTE9cdTAwMTRfyYnSqFx1MDAwNLPj9+yqVbx42pG128JZVVx1MDAxNKNm7f6gsFxu4lx1MDAwYlQ7XHUwMDE0XHJcdTAwMWJojFx1MDAwZlxy3vN4bKhcdTAwMTDZwI1SiObUii9lXHUwMDFh8Dao+UXgy03yXHUwMDAzepuZxLBRzVx1MDAxZVx1MDAxNz5cXFb35bedyq6f9erB8VngPVx1MDAxY6Q+rYF65lx1MDAxMCptvVxmXGLsj9G0XHUwMDA2c9D14pQsXHUwMDE1pfFAIJ3ySzlGK4LJX1x1MDAwNcBCJeY1NKP2QVx1MDAwMZ1dfm9Ubkfz4lG1ZIr5Mjts1VtVs1x1MDAxMvKr7MNHNPpoXCK0UWN5XHLEsCRGM2LQbi1ZaPKh8quFXCJGxiOan1x1MDAxYr06Oatsi4Ml9sbsqYf7w1x1MDAxM9K4PLy99MONh43QXFxH+fOktFxcauRXceUwjNgwNiNcdTAwMDTFS41AlztSoCnGIaGGkbRnle1iIFxmt3+Z3IOEZP+A3Vx1MDAwNMJQmD33kMtcdTAwMTauvcr1TeusXrjNPmRcdTAwMGav+fHxKuivXHUwMDA1sTRacS2YfVx1MDAxNqTHQUxt9lx1MDAwMf1cdTAwMTSVii9cdTAwMDPij9VfyVx1MDAxOcM2/ir6K6eU+INcdTAwMTbGMJgjdVbee3wqXHUwMDExt37VaoS3hzdcdTAwMWI8W8Omp1xcf7VE4bOVSURb68BGrUNfXHUwMDE2gSphUcGXeibyXHT6y4kkiH4+T5nqXG7jXHUwMDE3ZGL2gffLis1cdTAwMWNld1ftzVMhzEGvfXfwKFx1MDAxYrVcdTAwMWJcdTAwMWFcdTAwMTb2V0F90Tw4VKBNsE/0uIxVwP2AMChbXHUwMDFiheBcdTAwMDK0walVX4bmXFxY+/CLgJcmi68xVGCwMrv3velcdTAwMTVOdffkXHUwMDAyrlx1MDAwZnfunlqd/cvk1Vx1MDAwManRXjSLqL3CYM8zNbR+91V7cUKWiFrN4s44ndqL5uHZ//xcIvCNXHUwMDE1q43AXHUwMDE3XHUwMDE1XHUwMDAwea7nSPxcdTAwMTa7VffxdrtcdTAwMDS3j3fu08XxiV9/zK6C9lwiT1x1MDAxZEJcdTAwMThcdTAwMDDKL1omOlx1MDAxZb4pZp8/UvtoWaU48Vx1MDAwYii9gPj+eZxvUq0+Jcm6S23yXGKHi8yM3MKW2cs8VkpcdTAwMTc7ep9lg9Pc0Vx1MDAwZZUrgVxcw1x1MDAxY3RcdTAwMGKEXG4lXHUwMDEwejCCXFxcdTAwMDKOXHUwMDEyNppcdTAwMDctUZ2TXHUwMDBi9lx1MDAxMTeuWFx1MDAxMLmxRN1cdTAwMDCqZFx1MDAxNJv9dYHA4lx1MDAwNYbLXHUwMDE2679cdTAwMDJ3Ql1P7mJcdTAwMGZuu7RLOlx1MDAwN/eiXFwq34WN2FQ6XHUwMDA0sbnrelx1MDAwNKNcdTAwMWFiOYPFllx1MDAwMsQqUN5YXG4wNC6DlVx1MDAwMGLoxFlXXHUwMDAyRI1m0jKAoVx1MDAxYlx1MDAxOK35J9NL/pN4XG7JZUmKISbto6qZaXr+UKrlNk0+XGLK7u5Fdvss4zG6XHUwMDEyNLXPXHUwMDBlKWFcdTAwMTjWac3VyMNxXCLt7MPxP1xmX5VKtkjLsHSSXHUwMDFiXHUwMDFhI6mdgDBcdTAwMTD5+PU0n09cIppcdTAwMDZcdTAwMTLRxUhERTKLXHUwMDA0XHUwMDAyRtrtNWZmUY5+bfaK1b2C2JK1m8JJt83yuVVgkVbELoanXHUwMDA2JzOiOZ1AI0BJsfGzkmy0XZ862VEmXHJX8bTxz8Mkllx1MDAwNiaxXHUwMDA1mVx1MDAwNIllhuhcdTAwMTjR/+NcdHpmJu2dbXSzl5fhrp/fyarr6Lq3ebGzXG5MwlDGYVLYXHUwMDFiRnuoKYwxSYKtz5bckPhjoHdl0qRcYmeMSZRcdTAwMTOpOPC5kqGrwiSRXHUwMDA2JolcdTAwMDWZlFxcs6BcZlx1MDAwN61cdTAwMDWfPf66XHUwMDBmz13aOFx1MDAwN5/Ip8fbsOh+O6+vRNZcdTAwMTZcZjhcdTAwMDLjL4M84dyIcVwiXHUwMDAxKENcdTAwMTkwW8r9z1x1MDAxMkmjs/uUzS8+n0gyXHJEkouau+RtXHUwMDA3XGLaXGKGnTk7k85uy3tQb3qV7Y2n3YjSXG5/MKvBJC5cdTAwMWRFtX28rFx1MDAxNWej1T9cdTAwMDRcdTAwMWPWL4RcIooyPeVcdTAwMTHexzOJc6FcdTAwMTVTn7BR3OdcdTAwMTNcdNJAJFiQSDzR22HQIKyhmWNKOi+b/MmFyLiFfWRJJLdcdTAwMWW71XBcdTAwMTWIJFx1MDAxNbo3YiTBXHUwMDE5WFx1MDAwYi1GyjhwSuKcUlx1MDAxMFx1MDAxY12VgOQyjqWIRGYhkt1yz65cbvhcdTAwMTlnJJVcdTAwMDZcIqlcdTAwMDWJZJL34Fx1MDAxNVx1MDAxYeNaXHUwMDEy31XyLVwi8XLQpHukWij1NiumSup1fytpOUuqiKQpdZRdUkw44lxcqdFcdTAwMTlJONTuzStcZmg2bUnLx6dcdTAwMWJcZlx1MDAxMcauqvlcdTAwMTmJxNNAJL4gkZLTdlx1MDAxMuyihDm2XG7ptIS/V2td3T1G3V3voFx1MDAwYse3Okqg0WLbslx1MDAxNd2w4r3zdlLaIVx1MDAwNqdcdTAwMTljjJB0JDrCOdlcdTAwMTFcdTAwMDKhi7GRYYpM2XyVgyhcdTAwMTXUdFxuTdyYTc6U9pbcSPGu+1a+XHUwMDFjWIGd0pZjZzZodqJ//Ttcclx1MDAxY/3RlKlMfe7QXHRUTa5BXHUwMDEz9lx1MDAwYlx1MDAwM0Do2esgplx1MDAwZnA6d1CU4IDk6Fx0cUJDYzi8fkhcYuZQTe3G2VxiMymSXHUwMDBi2Fx1MDAxNyYqY1x1MDAwZfpVQFx1MDAxOVDcME5iNVx1MDAxOINcdTAwMWQ+ibGb4jNb1Gp3XHUwMDAxZWPfi1x1MDAwMFx1MDAxYVBr9Fxc+/ksOlx1MDAxMVwi1WQ8mzJcdTAwMDfVwshtR5t+UPSD8vpQXHUwMDA1xsu3fGRnkPw+OVx1MDAwYlx1MDAxZNtK4lx1MDAxMG1QSXH0XHUwMDAwhUzSwY5Ltlx1MDAwYtymXHUwMDFkYUdcdKLQunGFWGaoxi9nXGaqPLyg+Hajpm9cdTAwMTVcdTAwMTdrVIZYzFx1MDAxOIyUXHUwMDE5XG68xMbJsUZRh1GluMBwXHUwMDA1zyGcwVijam5cdTAwMThtNdA0Rtj5J1xyP4hGO7nfm1x1MDAxYpbYXHUwMDE1z1x1MDAxZJNcdLyp+LFRXHUwMDA1aNp3XHUwMDFjluzBb2tcdTAwMDOW9P94/f2v3yeenVxmYvszXHUwMDBl38H7fYn/f26bkVx1MDAxOPditCdcdTAwMTiXc+z+ynVR+81vN6Ql/bPcXHUwMDE27+RPL5LyRynRLiPAkYaAfC4vieVl7PVcdTAwMWGYY5fzckSixDebkjlaVLtgtlx1MDAwNKyiknE6X1n34i4jXVx1MDAxYtss5zLyXHUwMDBmPkaE6bBcdTAwMTmvbVnMZ0Dy5pdGg/0yhtlr3adcdTAwMGZxOrmqwG4wK5GLz1x1MDAxYt5cdTAwMGZzVWpHKk5sZa4ynCfv0bMwVzGmt3swo1ZcdTAwMTiceDSZQF1cdTAwMDNcdTAwMGVcdTAwMTdcdTAwMTLDfi2pxKhg/NuXXHUwMDA04OVcdTAwMTBflPuRNmNhrs1oM6ZL/vCMjlx1MDAwZVx1MDAwMnWMK+A4hDqW9Xid0rmDc5/hKLtcbm9cdTAwMDAnw8VsxvTd2WKNsjWxXFzbXHUwMDFkXHUwMDE57JdcZlHB2ViTKDpcdTAwMTFA8ae2JsNcdTAwMTiQK20yXHUwMDEyXHUwMDExbH8yY+B9L4+RuMaXXHQjQZHZM1x1MDAxObXC3XaVX3tX8p7C42XuW/787C7dskWtdzOGXHUwMDExm1BcdTAwMTdGjqyNXHUwMDA0YVVNc1uCi1xu9/6qJSdsLz8xXHKo3u/LYaaZi4+srkVlofEt8j/aXFzk3Duvllx1MDAwZW/xoylcdTAwMGJaiymlTVx1MDAwNsNcdTAwMTJcdTAwMTLfjuvNXCLBqSOcVpIyW9XE7ZbwXHUwMDA02Ti8f1x1MDAwZqC5QFVcdTAwMTRMKrvNLUwphl+UpoY7dr9jw6xcIlxuPcFbyH5cdTAwMGLt14xxobiKnfLDW6CEXGJit8H4XHUwMDE0b7Ew1Wb0XHUwMDE207V+LZ7CwFHhxm6/ZPc+l3w8WWB3PVWKgTJcdTAwMDKHj6nFjMX0pVx1MDAxZSNuXHUwMDA3m4KjiZG60XbhsFx1MDAxZW+UcLTRXHUwMDAynaxcdTAwMTREYYipV9pbZJJcdTAwMTBsf8awm2Qtvry8/7rbbOYjRNzrcCCW/eKLQlx1MDAwZm5yvet7XHUwMDBmm5NJZnn25aU7rep4/SVL3798/z/RhT5iIn0= 901245673Input()Switch()Label()

There are three types of built-in widget in the sketch, namely (Input, Label, and Switch). Rather than manage these as a single collection of widgets, we can arrange them in to logical groups with compound widgets. This will make our app easier to work with.

"},{"location":"guide/widgets/#identifying-components","title":"Identifying components","text":"

We will divide this UI into three compound widgets:

  1. BitSwitch for a switch with a numeric label.
  2. ByteInput which contains 8 BitSwitch widgets.
  3. ByteEditor which contains a ByteInput and an Input to show the decimal value.

This is not the only way we could implement our design with compound widgets. So why these three widgets? As a rule of thumb, a widget should handle one piece of data, which is why we have an independent widget for a bit, a byte, and the decimal value.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nOVdaVNcdTAwMWJLsv3uX+HwfJlcdTAwMTdx6VtZlbXdiFx1MDAxN1x1MDAxMyDAYEBcYiPWXHUwMDE3XHUwMDEzjpbUWkBcdTAwMWLqXHUwMDE2ICb831+WzKDW0kJcdTAwMGLybVx1MDAwMSYw9KKurjqZeTIrK+s/nz5//lx1MDAxMvXawZe/Pn9cdFx1MDAxZYt+vVbq+Fx1MDAwZl/+cMfvg05YazXpXHUwMDE07/9cdTAwMWS2up1i/8pqXHUwMDE0tcO//vyz4Xdug6hd94uBd19cdTAwMGK7fj2MuqVayyu2XHUwMDFhf9aioFx1MDAxMf7L/cz6jeB/261GKep4g4dsXHUwMDA0pVrU6vx6VlBcdTAwMGZcdTAwMWFBM1xu6dP/j/7+/Pk//Z+x1nWCYuQ3K/Wgf0P/VKyBio9cdTAwMWXNtpr9xlx1MDAwMkMtwGqAlytq4TY9L1xuSnS6TG1cdTAwMGVcdTAwMDZn3KEvT9XLm28n33vfOrlCrprJt2VcdTAwMTFLg8eWa/X6adSr95tV7LTCcKPqR8Xq4Iow6rRug4taKaq6XHUwMDE2jFx1MDAxY3+5N2xRT1xm7uq0upVqM1xiw6F7Wm2/WIt67iXZy8FfXHUwMDFk8dfnwZFH+ktZjzPNJVx1MDAwN8tBoVQvZ93tdNBDg6BcdTAwMTQ3hiGzI83KtOo0XHUwMDFh1Kx/sP7XoGFcdTAwMDW/eFuh1jVLg2uwqIKyXHUwMDFjXFzz8PyyXHUwMDEywWOCc8HBXGJgTL5cXFFccmqVauRcdTAwMWGiwZPGcC0541x1MDAxMjRcdTAwMGXGLlxm+oNcdTAwMDKSziM1cXC3e357v9RcdTAwMDfIv+Nd1iw9d1mzW69cdTAwMGaa7E7sxEA1uKfbLvm/xlx1MDAxZZTWRiDn2sKgXHL1WvN29OPqreLtXHUwMDAwLv2jP/9YXHUwMDAwp1x1MDAwNEZMXHUwMDA0KpeaXHUwMDAzXHUwMDA3XHUwMDEwM1x1MDAwM/VwL7NxcJcrlc+yqnfUqu9njoVcXFx1MDAxY6j8jYBKw/5cdTAwMWFStWeVRNRouZExXHUwMDAw9G9X6Fx1MDAxOSOBsGxBW7RmXHUwMDE5pEZcdTAwMWS/XHUwMDE5tv1cdTAwMGVcdTAwMDFhXHUwMDEyWtFTglx1MDAwYlx1MDAxNExcdTAwMDLAXHUwMDA0sFxuxTxcdTAwMDPMXCIqISRcdTAwMTiuxsDKjdBKk5Z5Z2DVXHUwMDEyXHUwMDEysVxuKNFqasrMWL3LXHUwMDE2dq5cdTAwMThuPcHm4VZWl0s3XHUwMDE3359cdTAwMTKwOoK3v1x1MDAxMaWSXHUwMDE5bYVS1oBRYyils6TMNKCyTOtcdTAwMTWiVHhcZpQkUaGnWW3GYcqNp1x1MDAxNFx1MDAxM2hcdTAwMTTjSlx1MDAxMKTHYGolkKDhOqrUoF6vtcOJXHUwMDE4VUYkYdRcdTAwMDJaxpTUM0P01lx1MDAwNPzkoFx1MDAwMPmbrd7RiVx0ermjy5NFIPpWXHUwMDE2/3WIaushXHUwMDExXHUwMDFjXHUwMDFhc6GsiGnK/u1cdTAwMWE9lFx1MDAxNmjotbJWxPpqXHUwMDExk1/2JXGLcXiCoDZwLq1cInvOkSz3XHUwMDA0m889qVx1MDAxNFpSoYxcdTAwMGLBUY/iXHUwMDEzJFx1MDAxMVx1MDAwMuBkXHUwMDAy31x1MDAxNUC10ElcdTAwMDBcdTAwMDVcdTAwMDKv5ZbPXHUwMDBl0KenzUaNXHUwMDFkXHUwMDFlbaszWVx1MDAwNX9v92t19yzdXHUwMDAwXHUwMDA1pjwynMqANcYy4GJcdTAwMDSiwuNkPi0zilx1MDAwYkviuiREXHUwMDBiRDlXXHUwMDA1UU28lIGAd4ZQm2jmOelcdTAwMTR6aTE7QkWb1W52oqPsTvXmQF1cdTAwMWbnXHUwMDFizcJWylx1MDAxMSrQXHUwMDEzXG40k2RnLfkvOIJQwlx1MDAwNYGT/Fx1MDAxOYtkXcUo/5hcdTAwMGahwFx1MDAwYsaoVSGUXHUwMDEzOrVUxM7WXHUwMDBmoq84TixcdKRgiXxLLYWcXHUwMDE5pfXNzon6epCv6G+t81x1MDAwNopM/qmeSzdKqas9TV6xXHUwMDAy8kaI5ckhkHJpyLcnT8pcdTAwMWHpPEi1XHUwMDE0Rsvl8iSATkBkrM//a8aJf1x1MDAxMvNcItdtXHUwMDBlXHUwMDA0/lx1MDAxN1x1MDAwYlx1MDAwMzSI5yM/k4H5cs/g7lx1MDAxOJqi4HHApGNDn+/ulvf38o9cdTAwMGbly1x1MDAxM7v1vWefXHUwMDBluzdfXq77+fzbb0P9UDtjgJc2XHTvhlx1MDAwNpuTzzl7mGDyO0+Ge9UvVrudIPWAV+LtXHUwMDAwP9X5ilx1MDAxMZaBtzVcdTAwMGV74thcdTAwMTaRpzRcYjBcdTAwMTjrVjM6rT31jTtcdTAwMWI6uus3avXe0HD1sUn989nGuy9cZuh5fcVrhq7crNcqzb52XHLKw5COakW//nK6USuV4kq+SFx1MDAwZvfpXHUwMDEzO/uz6OZWp1apNf16ftC2JazKlLCxZuQsky2dnfucibNN0auD3GiI7c7DztNcdTAwMDO/a6ZdzFx1MDAwNFwiiVx1MDAxOSlsziwoXHUwMDE0w6E4XHUwMDE0xrMo+yFcdTAwMDUgeK8uXHUwMDEyh8LTTFklXGaToKScXHUwMDE0i+PcU9pKbUnyXHJcdTAwMDNmx2NxKFx1MDAxNbXXmnnEcCHrk1x1MDAwZVrEWWL4XHUwMDAzhJRcdTAwMDaQ3P6ZXHUwMDAxfHC2KcOL6Nthpn2OXHUwMDA15V/dV6r3f/+8x1xmIFbSM8BcdTAwMTVcdTAwMTB7N9rYYVuByDyJZCVcYlrWWFx1MDAwMtdS7FxiWZFJPclQeIzcXFxcdTAwMTIoJS11PUxcdTAwMDAweORBanJ6ibxcdTAwMGIjzSh+OWlcdTAwMWOpXHUwMDE5zuNhrjV8XHUwMDEzWVx1MDAwZVjqSnLI2OzwbTX09fZ2ttjL3NbuNkVcdTAwMThcXFx1MDAxZUo/7fpXcu2Ri0lcdTAwMWVcdTAwMWYpNOByXHUwMDFjuoygXHUwMDA0XHUwMDFjOCNcdTAwMDeUL1x1MDAxNcH7XHUwMDFkXG6YMcmN4UZ/XHUwMDEwXHUwMDA0y+T5PMWJunJtZvdLg+2Tk30ondejnVx1MDAxZvLxurXFi9dmXHUwMDFkXHUwMDE0sETjcSFQgVx1MDAxMJprMCMoXHUwMDA2wo22StF1xDFSq37pXGZcdTAwMTNcdTAwMTaIQ3xcdTAwMTD0qsTgtJFSxVxy6WvQLdT3KmdcdTAwMDeb96fFQGm1n+ncdjN3aVe+XHUwMDAwzCNqa1x1MDAxOTjSKKRcdTAwMWVcdTAwMGVOo7BcdTAwMWVjKDRKXHUwMDAxXHUwMDFh5OqczDdSvmQwrTBcXHxcdTAwMTD4XG6ZXHUwMDE4XHUwMDE0JFx1MDAxMWdO+9qZXHUwMDAxvHt4e3pZzp0/tJqN/ePK9SPb7lx1MDAxZa+D7nUgNlx1MDAwNFFitprQg6PK17lwmoNVTFxuS65cdTAwMWMug+J/XHUwMDE4ZVx1MDAwMjshfv1cdTAwMTbql8yHm1x1MDAwMjRcdTAwMWaE/VxuTI4+SO2yo+RcdTAwMWNJa2fnu5eH29vlXHUwMDAzfVx1MDAxNlRcIlZu4o/N9CtgRbzBcMVcdTAwMTjXqK1cdTAwMWVGLpFfXHUwMDEwXHUwMDFjmJFutmOFXHRcdTAwMTZvo35dhlx1MDAxMCNcdTAwMDb/QbQvxnyVUfKgLLWC4eza965Z7+BBuVuA4kE2f3GQffpRTprZTpf2VS7zhli+1eSxas3GMcystYJ4XHUwMDA0MSrUy8VcdTAwMWVWqX1dviZIXHUwMDBlXHUwMDFmxHNDlpzdpvv5fHaOXHUwMDE5xd5pMTRFznd2s51K5nLv5OvGblLKcGq0L+fWI8IghZtHUVxc4Fxidok5SKZcdTAwMTDRzX2DhKWYw+/gv1x1MDAwMP08zY+igE1y7EGgy5+Nx9deQzDkq6adLVx1MDAxN24u9+5cdTAwMWHb19nKg9pPQdL7bCgmssQ5aWF0UbIxXHUwMDEwI1x1MDAxZFdSo1Nvy2VvrFL/KlwiOvTNPlxi+0WTXHUwMDFj+yW/jaivkLOjt3rQyYeNw/bOWXi3u5FcdTAwMGZt8VtcdTAwMTZTr3+N9lxiXHUwMDEw5LZcdFx1MDAxN26B8ehcdTAwMDO4XHUwMDE4XGZotDruLKRU+yrSOZqA9EHUr9TJXHUwMDA0wjDUXHUwMDEwp1Kv4fe+yO5OK7WwsbeFm9cnd5FfbZXXQvtcdTAwMWFDXHUwMDA0V1x1MDAxYmktM1x1MDAxNuOZXHUwMDE4LyBcdTAwMTY0NuS/uUVYfLnMpJXyX+E+wX6U2K9MzpyHflx1MDAxN7mMlpnxe+37aifb/da5rCt21oyAIT9Ku/5Fhlx1MDAxZVfoXCKmjjnwUeZgPcFcdNZklC1PfeqDXHUwMDEyaDR5mlx1MDAxZlx1MDAwNL5KJUZcdTAwMWaASa5cdTAwMTXOXHUwMDEzfrjZle3vR+qSseuef3WV2z82lfN1UL9cdTAwMGXC5KGRLTJWS8PNuPrlVmtcIsdcdTAwMDC4ZHL9SpWvZkyTz/1RtK/iidqXM6Vccqp4mspr8O12evuHXHUwMDE13Fx1MDAxMVet4F5vZXgzb3qp176qr32ZtVx1MDAxY7hcdTAwMTAxr/1F+2olhVx1MDAxNkwxxLRcdTAwMDd/wdL7WMnYXHUwMDA3XHSfUZcl6l9LbJBjXFzdvFx1MDAwNuDHvdtKeHPh8+vHXFw1v3lzXHUwMDFmXT9l10L/9leBgjbKXG5Ffc/lOIpcdTAwMTGFW+5O/2C5OYzVTr4xg1x1MDAwMqx8P5mTSan1XHUwMDAwidDlXGbBXHUwMDE5ojnm3W7bX4PM1VnQ/V45yD49Ni/CcmEt5o0lck9cdTAwMGLnsVx1MDAxMWqJXHUwMDAxj0RcdTAwMWaAk/7lXGKSVLNEmMJcdTAwMWRcdTAwMDRcblx1MDAxZlx1MDAxN0RubLRcdTAwMDdQZaPYXHUwMDA06Vx1MDAwMtBcdTAwMTg3mqvMrid/NVx1MDAwNpHFsutjXHUwMDFm8Ep2/VDPXHKS63HowlmT66NWOymzfuhcdTAwMDVG0+jZ9Cz6JFHSyYtbXHUwMDA10VFrxFx1MDAxY9lvW6J9b3PhUfugoDG30WlcdTAwMWSZncw6SFx1MDAxMlx1MDAxYfQss8LNYVx1MDAwYsVwNInTZVx1MDAxOXEpXWcoMlx1MDAxMlOWYS8jSpNoy7gkUWPcYtx5WMq6XGJcdTAwMTKkQZBgMUFcdTAwMDKZKEnkyoJi8VmtV1NB7ptf4ewqd1x1MDAwMUenbV3M5i+KrcZaXGJcdTAwMTKxXHUwMDE0cNEnRiBlio+LkbHOPLtcXDzF/1aLhIKcXHUwMDEyXHUwMDFiX4L7flx1MDAwNImnQZD4goKkk1x1MDAxN1xuK7DkW1x1MDAwM5+d3PXC6sPj7db53fX33Yvz5tXF1d7xWqSlXGJcdTAwMDaelsI4v1x1MDAwNInLjYpcdTAwMTLJmZGMPG9cdTAwMTJcdTAwMDNjkr2SpSRpklx1MDAxYjIuSeSSXHUwMDE4l1x1MDAxZP5cdTAwMWW5XHUwMDFkpkGScEFJSk4vUORcdTAwMGVYXHUwMDA1c1QuMVdcdTAwMDf4taDOiz/OXHUwMDFh55nocKdcXM111kGQOCrPOs+USVdcdTAwMDVCglx1MDAxYZMkblx1MDAwMazi2lx1MDAxNV5YlZs0kyhx5Vx1MDAxMu90LMfs/UiSTIMkyUXJnUmSJFJ+Uiszz1wiocvDo4ctc7b740T29i5yZYjqtbVcdTAwMTAlsjVcdTAwMWVcdTAwMTlcdTAwMWNcdTAwMDZE4MhcdTAwMWRcdTAwMDJtxkTJoGJcdTAwMDY12Wlr2GrcpFx1MDAxOa1cdTAwMTJcdTAwMTlPRoZxrnDYusiSSoMsqVx1MDAwNWVcdJP5XHUwMDFk00R7mJhjyX6mlM1sh+a8cZMrXHUwMDA03ZNWbvPworJcdTAwMTayxJXHXFzNLGt5v0TTqCwxz0pE1NZVfF2VKLFcdTAwMTlFXHRcdTAwMTmq91x1MDAxObzTaVx1MDAxMCW9mChxlrz+RLmiySDZ7GZcdH50wzB6aLDj8z14ZFmf5UpJ1S9SJUpCcY9cdTAwMTO3XHUwMDAzMJqTXHUwMDBlMSPRO2Y90iuuQFx1MDAxZFx00rRVKKuPOmhUXHUwMDFhLYjfXHUwMDEzdEAyw/DbJEmkQZLEXHUwMDEyxWRscja2XHUwMDE0ynKNZnbD1Ny/vTu4lFulzrm82snrS773mH/jXHUwMDE5/ZJcdTAwMWZWg1x1MDAwNFFiXHUwMDBiiZJcdTAwMDTjITdMMlx1MDAxN1x1MDAwZZdsJOrAhMcsXG6ppNJkqKfU4Sha7nN/qihNndN3XHUwMDE1pN1cdTAwMDNcdTAwMTQ3rmrNpNJlWnmayKawQjBqT9xxe/amXHUwMDE4J1x1MDAwZahQvZ3dej4xUrLsOdRkbOPh4TJ7XFyyt5uV3Yub3o5ccl/ebFxihn6n03pIYc0ySF6PS+4zMVx1MDAwMFx1MDAwMDV7XHUwMDE0+yravc0yntssfisp2/leOdkpJk1cdTAwMDelR1x1MDAwNJRcdTAwMTSeXHUwMDA1REt9bp1cdTAwMWIzJFx1MDAwMsJcdTAwMTJt45qTOpBcdTAwMTIlT15Rs6xcdTAwMDRALD9jSt0yJVG4+mlvZ1HWXHUwMDA345Ps1exF0bZq0elDjVx1MDAxOMw//2ey5VqsOtqilivenKnC+6tjJ0hvPFx1MDAwM2tsa1x1MDAwMtMvkzBHStr0oU5pSppC6TF62X6VXHUwMDEylzI5Yr9cXD14QSBcdTAwMTP0rfiUelx1MDAxMEvbL+Zxi1x1MDAxMo0w9CBcdTAwMTNPm1x1MDAxZlxis/Ikama50UowXHUwMDFlr/k6qMVJhpjFd5RIZVHCMPI70VatWao1K1+Gsoqet4XZn8Eg9CW22O1rbk+7jG3JXHUwMDE5aMuJdsjYRVx1MDAxNb9Nl1x1MDAxOI+4LVxyN/F+RaOtn89cdTAwMGbSloJm6fVcdTAwMTZNZ2lDLVx1MDAwMueZ0oOk1Vx1MDAxY21sqeVLk8Ajb11Zolx1MDAxY9KQbVx1MDAwMDueS1X3wyjTajRqXHUwMDEx9XyuVWtGoz3c78pNJ+LVwFx1MDAxZlNcdTAwMWH0UvFzo7qg7T5xWIlcdTAwMGZ++zyQlf5cdTAwMWYvv//7j4lXbyRj2H2No3fwgZ/i/y9W2FFPKSymjVuKPUf91Ivi9cHTRifs7FfK/KZ519RVm5RcdTAwMWSeXHUwMDFlJmKkJ1x1MDAxNaGNSctcdTAwMTSPr2v5xUS0p6VcdTAwMDRgVkvNpuSkXHUwMDAwKUO/sLguI5XpKdRu6p5+XGJpJySqXHUwMDE4XCLj5NhKrV1cdTAwMDaeVLFuet69QjO0Kp4/uUqeclx1MDAxNvpPcHxRr1x1MDAwNyivRKF1nrHifM24uJ1qzSWNylx1MDAxY8vbj/Ld7t2PTDPfXHQvXHUwMDFl7cVcdTAwMTM8QEGnXlx1MDAwMiR6iEBcdTAwMDZbWaflcZiLS4PkXHUwMDAxXHUwMDEy+pnb1YdjMlx1MDAxN19WXHUwMDAyJm4sNKGGsHSRUivecCZ8fUC+JFx1MDAxOe9FwX6z3Y1SQsZjzVmMjHNIXFzexFx00W5XojnSWKZcdTAwMGZ1Ssk4MOOJX8XXmDDkTVx1MDAwZlenkFp5XHUwMDE2hVx1MDAwMSO1ZdM2XHRbVnyJqlCPc86MsYbHp/hfhNlcdTAwMTJ5c3tHWGO4dcHkMdnWxEJcdTAwMTU19H1Q8en2IEZ8N4j5up3C0FxyoUFr9ThcdTAwMTfXnuHSXG43TSU1qlx1MDAxNyY4J1x1MDAxOZ/O0obdXHUwMDAz0vi/Qn9cdTAwMTJVrNrugIuvNfVOXHUwMDA0rPvaXHUwMDE4w+qbXHUwMDEyb56Y6kBcdTAwMWTNNeBcdTAwMWMxhLMrv36+fdPbPtBfe49cdTAwMGa7ufvd3lsvKp7GOlx1MDAxNtySS3jo0qxcdFmCSzWyuSFnrqKv1lx1MDAwMiXTgvh3Mu2QRYustFx1MDAwNPFcdTAwMTbcI34vgLqd+t5MmGJcdTAwMTJgPCW0KzFM9IjJ8Z04ibZcdTAwMGJt4uuaVslJXG7lw9ZtXHUwMDA3mtunnfLpcclcdTAwMDS3N42t9Vwi3skxcCBcdTAwMTJKqnCeXHUwMDE4+OZjq7jTuS40N+Do/vvtzs2VLrdTL1x1MDAwMZxxr79lXHUwMDAwMuVKQlxme56ESE/2tylSbqNcIm2SXHUwMDBim/CC8UWwXHUwMDA08eaTplUnXHUwMDEwb7fRXHUwMDE1zLVpwHvB+PK8e6e/jXN6iPdLe1x1MDAxNmPekFxck95tXHUwMDFlIIycI+11+linlHiTufBcdTAwMTi3RJtcdTAwMTVze9uO7NiHzPFcdTAwMDdcdTAwMGJCMUcweHJCxNLia0hRKCPIXFxcdTAwMTJNIco2gXlL44JLxlx1MDAwNbpcdTAwMTUqsmJcdTAwMTOkW1omh2pcdTAwMDGvM/Weblx1MDAxMoapN2Ouklx1MDAxNtFu5crzgIs7j1Nd8MiddDXPXHUwMDA1uVOAOFx1MDAxZXaeiX5PJ2tD9Jsxji40LJjWNGh2kFx1MDAxZftOXGJ4MnDd1zhkk1x1MDAxOPin5yd88dvt04iA9tL/XHUwMDA04VrpWV1cdTAwMGZe88t9LXjYmryjm9vU7dNzhzrFXHUwMDEz9Fx1MDAxN3D//PTz/1x1MDAwMcOHY8gifQ== 901245673BitSwitch()ByteInput()ByteEditor()

In the following code we will implement the three widgets. There will be no functionality yet, but it should look like our design.

byte01.pyOutput byte01.py
from __future__ import annotations\nfrom textual.app import App, ComposeResult\nfrom textual.containers import Container\nfrom textual.widget import Widget\nfrom textual.widgets import Input, Label, Switch\nclass BitSwitch(Widget):\n\"\"\"A Switch with a numeric label above it.\"\"\"\nDEFAULT_CSS = \"\"\"\n    BitSwitch {\n        layout: vertical;\n        width: auto;\n        height: auto;\n    }\n    BitSwitch > Label {\n        text-align: center;\n        width: 100%;\n    }\n    \"\"\"\ndef __init__(self, bit: int) -> None:\nself.bit = bit\nsuper().__init__()\ndef compose(self) -> ComposeResult:\nyield Label(str(self.bit))\nyield Switch()\nclass ByteInput(Widget):\n\"\"\"A compound widget with 8 switches.\"\"\"\nDEFAULT_CSS = \"\"\"\n    ByteInput {\n        width: auto;\n        height: auto;\n        border: blank;\n        layout: horizontal;\n    }\n    ByteInput:focus-within {\n        border: heavy $secondary;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nfor bit in reversed(range(8)):\nyield BitSwitch(bit)\nclass ByteEditor(Widget):\nDEFAULT_CSS = \"\"\"\n    ByteEditor > Container {\n        height: 1fr;\n        align: center middle;\n    }\n    ByteEditor > Container.top {\n        background: $boost;\n    }\n    ByteEditor Input {\n        width: 16;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nwith Container(classes=\"top\"):\nyield Input(placeholder=\"byte\")\nwith Container():\nyield ByteInput()\nclass ByteInputApp(App):\ndef compose(self) -> ComposeResult:\nyield ByteEditor()\nif __name__ == \"__main__\":\napp = ByteInputApp()\napp.run()\n

ByteInputApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258abyte\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u00a0\u00a0\u00a0\u00a07\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a06\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a05\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a04\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a03\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a02\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a00\u00a0\u00a0\u00a0\u00a0\u00a0 \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

Note the compose() methods of each of the widgets.

  • The BitSwitch yields a Label which displays the bit number, and a Switch control for that bit. The default CSS for BitSwitch aligns its children vertically, and sets the label's text-align to center.

  • The ByteInput yields 8 BitSwitch widgets and arranges them horizontally. It also adds a focus-within style in its CSS to draw an accent border when any of the switches are focused.

  • The ByteEditor yields a ByteInput and an Input control. The default CSS stacks the two controls on top of each other to divide the screen in to two parts.

With these three widgets, the DOM for our app will look like this:

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN1daVPjSNL+3r+CYL/0Row1WXfVREy8wdnc0OZo4O1ccsLYXHUwMDAyXHUwMDBijG1kmWtj/vtmXHUwMDE5XHUwMDFhSdZh2fiiPUPTLclyuiqfzCezslL//bKwsFx1MDAxODy33cW/XHUwMDE2XHUwMDE23adqpeHV/Mrj4lx1MDAxZvb4g+t3vFZcdTAwMTNP0d6/O62uX+1dWVx1MDAwZoJ2568//7yr+Ldu0G5Uqq7z4HW6lUYn6Na8llNt3f3pXHUwMDA17l3n/+yfe5U79+92665cdTAwMTb4TvghJbfmXHUwMDA1Lf/1s9yGe+c2g1x1MDAwZd79//HfXHUwMDBiXHUwMDBi/+39XHUwMDE5kc53q0Gled1we2/onVxuXHUwMDA1VET0XHUwMDFm3Ws1e8JSXHUwMDBlVFx1MDAxOcPDXHUwMDBivM4qflxc4Nbw7Fx1MDAxNYrshmfsocUr9nK7d9N9rJtm6eB43Vx1MDAwZs5L5+vhp155jcZh8Nx4XHUwMDFkiUq13vUjMnVcdTAwMDK/dev+8GpB/dfARY6/v6/TwkFcYt/lt7rX9abbsd+fvFx1MDAxZm21K1UveLbHXHUwMDAw3o++XHUwMDBlwl9cdTAwMGLhkSd7hWFcdTAwMGVQQY0xRIDR6v1s7/1cblx1MDAxY6GZltQoKpRhfXKttFx1MDAxYThcdTAwMTMo17/EXHUwMDE1q1V5KNllpXp7jeI1a+/XXHUwMDA0fqXZaVd8nK/wuse3b0xEONB117uuXHUwMDA3eFCa8PPc3rhcdTAwMTOliFx1MDAwNk01fz9jP6W9WeupwH+iI9OsvY1Ms9tohILZXHUwMDEza/1qXHUwMDEzVZ2Y+lx1MDAwNO5TKG1ksluVdndcdTAwMWJMUN7erizz48btaaVZXny/7p8/0m/7+ubO3vdye31cdTAwMWb2dlx1MDAxZb7V+MWe/H7sPsU/5dfnV3y/9Vj0vjv+wVx1MDAwZn61U/7O+ePekeeXvNXvlWL3fftbOIDddq3yqutEKlxyXFyDUSZcdTAwMDKXhte87Vx1MDAxZttGq3pcdTAwMWLC40tE4Fx1MDAwNCxj41x1MDAxYUGkzEakXHUwMDAwbrSRQlx1MDAxNkZk+iSNhEgyNURSMI6IIDJcdTAwMDKC3lxyXGadXHUwMDE2JClNQpKKXHUwMDA0JKlihlx1MDAxMEXk2CCZoYVKXHUwMDBiqUBLoYbQwnC2W83g0Hvp2XaIXHUwMDFkXa/ceY3n2IT11Fx1MDAxM1x1MDAwN2j5OXDXev7m67+jI9lx8ZPtrYiOvWep4V1bNV6s4ndx/ZiGXHUwMDA3XHUwMDFlurD3XHUwMDBi7rxaLeqUqihIXHUwMDA17+lvXHUwMDE2cSYt37v2mpXGUZqcucB7XHUwMDA1flxu8lxiRIal31x1MDAxOVx1MDAxMqI1IyxyxSDs5du4ecWe0Fx1MDAwZYBUgilKXHUwMDE1NSBj2KNcXDqCXHUwMDE4oVx1MDAwNbUjolQ2+KD3XHUwMDFhXHUwMDFkfEY5QDgxXHUwMDA0lOT4k0SiXHUwMDEwXHUwMDBl2lx1MDAwN4WAXHUwMDEwXHUwMDFhgCnSXHUwMDBmTHtKgpHDuMrQqfxSXHUwMDE4+nbknynDtVx1MDAxM1T8YNlr1rzmdVxcsDfOV1x1MDAwNCf2O1fa1q04hlx0nFPKcUg0ZyxyxVWr2rXfo1x1MDAwNFx1MDAwZcd5XHUwMDE30nAj0c1IymTiy7vN2mChLq7u/aP9l2Zru37hi43qQ1N8f0xcbmVcdTAwMWOCdp5cdTAwMWHG0KArSXm6UIJcdTAwMDHHadRIzigorlx1MDAxMjI1Kp1gpXV351x1MDAwNTj2XHUwMDA3La9cdTAwMTn0j3FvMJcs8utupdZ/XHUwMDE2v1P0XFy/iWjbO8apUvi3hVx1MDAxMEG9f7z//T9/pF5dylJs+0qodHi3L9HfWbYtl+tTKmX/4Xf7XHUwMDA2SC6oXHUwMDA2XHUwMDE1erxB9i1/jmdi39Qg86aII1x1MDAwNdVUcEMojm+c7DNcdTAwMDKOMlx1MDAwMq1cdTAwMWLHWUDjJ/rkXHUwMDFhn3UjOiQ171x1MDAwNi1cIv4vXHUwMDBihryTcsZDXHUwMDFmO1x1MDAxYrJ/6W8/updcdTAwMWKb9eP6snfmnrfXl1fOZk32L7rPOytwt/tcdTAwMWPcd8pAXHUwMDFloHJTL81zXHUwMDEwMZI/XHUwMDE4KYigVOssqGtmJEFcdTAwMTdQnMmkz/6cI13mI11MXHLpTCWRLpNIXHUwMDA3gaKCoWND+qRjiEi4MyCGWPnF7L/+bNpcdTAwMTNV9NdcdTAwMWS38/fPxaDV/rn4s5lcdTAwMWVZXGJcdTAwMWW703vg0HCvgtHjilx1MDAwMW6rP64oIPtcdTAwMDc8slxmeVg/TFx0Rr+AlEdcdTAwMTfH6crhnqqdbHzb89ae2lxcb7i+u+TNe/6NXHUwMDEx9MlcdTAwMThYIf9AXHUwMDAyQkGyXHUwMDE4UjlnjqaMIDUhXHUwMDFjgOpMoH442jeQXHUwMDA0qkpE+5Io4JJFvtdsXFzyzfpccl8+u1/deKhcXH6/a+12N48vr4q6uJuLk12x3azed4+f6jdUXHUwMDFk3d22XHUwMDFlxuA6XHUwMDBmbsr153OvfHrbeVhT61stf3XDXHUwMDFiw323r6ube+tcdTAwMGZPasv9UTnaa+/drWP8MS6XzFx1MDAxNGOhXk3KJUtNM7HOuOGaUiiO9fTpn6lPLoB1XHS5WEd6zlwiWFdcdTAwMTPDuklL7CV8sjXBlKE846Pf8+OUbcJss9nuXHUwMDA2WXm9XGbv+9G83lx1MDAwMCeVltf7JebojpYh581cdTAwMDKfMMC1UkNk9nboXHUwMDBm/2bHXdl9WuvSMrku7Z+Wm/PuZylTXHUwMDBlXHUwMDE4xJbgXHUwMDFh/zPxxJ5cdTAwMDTiXGKNJopcdTAwMTjFpFx1MDAxMlx1MDAxM3SzJMXNykTujjD0sdJwOevQl2JQeVjdO395emr5vPHy/HJ/dTRrP3tx841cdTAwMWVtPK2xTX/l4rl+u+5cdTAwMDdwPob78v3DXHUwMDA3s7NcdTAwMTI86lx1MDAxMu8snWnXK2k1Lj+rqWZcIlT4XHT5WcZ19pI2hnpcbqO+SEZ4XHUwMDEw1tOnf879LOU8XHUwMDBm65Q6MFx1MDAxNayblKx90s1SQimyXHUwMDAxXHUwMDBl41vSXHUwMDFlp1x1MDAxNn7QzXrB4aNcdTAwMTdU619hun52gJNK+NmInFx1MDAwYlx1MDAxZlx0aVx1MDAxNWR6WoywMJzT0lx1MDAxNF/AfvK8yr1/e3dSWq+wXHUwMDE3ft899PZmvIgmXHUwMDA3pp7AXHUwMDExxiiOP4wzwXhcZn2cI8lVlFx1MDAwMVd4XHRcdTAwMDOSzXI/nHoyPIk/naS5XHUwMDA06ZEydIy5p9FcXO3a/snN7qM8XT2g66XT1WD7eUOuTSFcdTAwMWI82HVNKWurc7CjqUKFkUOkg9KHc86xI3KxI+LY6WfP41xcnyFJ6CSztkhmmcJIVY4vXHUwMDFiND9cdTAwMTHia9j1mvbslSrWW42a6//9c/FcdTAwMTKDss60XHUwMDEzt1x1MDAwM1xcQb9DKyR9LkwzXHUwMDBiRVx1MDAxNCGZMLWL2ZrpyIrdwHXUXFzDNadcZpNI5lx1MDAxMFsqXHUwMDAwXHUwMDA2tZ/RXHUwMDE4TJm2oSaA0JZeXG5cdTAwMWVZdlx1MDAxZTdOwaFcdTAwMTRxITV+XHUwMDE0YsPoXHUwMDE0jyeYXHUwMDAzQjFcdTAwMDCNYT5cdTAwMThcdTAwMTFBwHuih4PNtqthKrg+XaVIwaJcZulQw7mixtbgXHRJZFpVXHUwMDA2OFx1MDAxOEpJW4kqUG6NLMYkvnyhSpF8VL9cdTAwMGJFcJ654UJcdTAwMTBbmWKkyKhfXHUwMDAxblBcdTAwMWKkoTjjSlFJXHUwMDEyUn2qWpFs9bavpGKHN/xcdTAwMTL9PbSFo1xcZK5cdTAwMWZcdTAwMTODNIUoPVx1MDAwNIvPT4PMqYljWjvSXHUwMDE45F2ao1wi0ThcdTAwMTVBaDiK4yBQsGUz0ULtcVx1MDAxN8JRXHUwMDA3yZ8gqM5MUqHCiVx0iVx0d4iU1GhGgWiTXFy00ohcdTAwMGIzY/uGcpFIiD9++5afWY7bXHRcdTAwMDaaIZxcdTAwMTjYKmI0/ilW0NixJoxwUFx1MDAxY936aOYtP1xuj8uEQ8RcYrJeXCJ6dc9JmexKqVwi1iRTYVx1MDAxZFx1MDAxNv/cxi1Tse0rodJDmrb8JFx1MDAwNc+u9MVcdTAwMTkgXHUwMDE0lVx1MDAxNYrnXGJ3L1a+lcs3zD+puZetnSXxtNahc15cdTAwMWbDwDjoVDRDXHUwMDAzJ1x1MDAwNbKPOIMj3DGMgFx1MDAxMpzgXHUwMDBmzsLkXCKtVINcdTAwMTZJirwlXHRcdTAwMTFcdTAwMTNcdTAwMWMjv1kvXHUwMDA3bJ2s3DWvK8es/qw3ZGX95P6YXHUwMDA2s15cdTAwMWX/XHUwMDFkKtY4z2RcdTAwMWNI5jgyXHUwMDEyXHUwMDE4XCL3kT5Nc1x1MDAwZUkh8iDJtMOnXHUwMDAzSV0obY9cdTAwMDabXHUwMDAzj/Ki3yf1XHUwMDExVn1NN2k/wJNkXHUwMDE3p4285yW6kbBcdTAwMGZ2XHUwMDFj41x1MDAwZWLZbWHU5du3OeX5XHUwMDFjtFx1MDAwM5pcYs21pHh5nOczbVx1MDAxYyEw1jWogISTSEZy/LlcZoZ2TuGAI8pcdOEqJVx1MDAwNckxOqfCbv/UWuNcdTAwMTU6uVx1MDAxYk1cdTAwMGKD0IzuZfpccnNcdTAwMTn5QInzalx1MDAwMkJyYFx1MDAwMlVcdTAwMTlcdTAwMDNpmuTVysGJ14ZzrnrUO5k0KMT1XHUwMDBix1x1MDAxZuBgyI7ScMlQ+YxkJpbOeFx1MDAxNVxuldIuliE1XHUwMDA2IGj4PzfXz9Zt+0pq9ZBsP9u+oYPKsm+Ca/u/LJ6qzedZc2rfcMxcdTAwMWSltVwiXHUwMDFjObTk0X3mr1v6LOtAdkVcdTAwMDBpXHUwMDA3V5NcXFSh1JGglOQ2KWzT5CnMn6HeS6KYZFRcdTAwMTiI7C59s29cXKJcdTAwMWVcdTAwMTk6VLHApzNvhTf1UVx1MDAwMTiQhNg9myotJ1xuXHUwMDBlx1x1MDAxM1xubZvhQFxig1x1MDAxMXf0XHI0uL8kslx1MDAxYslcdTAwMDBQjVx1MDAxOLHqlCqSoIg80au8xVA8ZZPhZ7Jt2VptX1x0fVx1MDAxZdKy5Zc1yki7ikSaXHUwMDE2Z5zhNFx1MDAxNN/Rx5fXuydrm62V04p7UX95MK2no/05XHUwMDBmmohgjpBMqN6ym2AsXHUwMDFlNSlGXHUwMDFjXHUwMDA13O5lRf+OqjY541asrlEjhVRy9u076lx1MDAwZlx1MDAxYvewXq67bOX2qnbaofWDy4PfvfwwXHUwMDFhNE+q/FBFdnb2QVx1MDAxMqmcXHUwMDEyisjiXHUwMDAxVfoszTlcIqXMRaSgXHUwMDBlnVxuXCLT1oKTaVxmLa2QZjo1/kPrYDjVo6QxdiqXbuPrz0X4ufjvhakmMlx1MDAwNriS/kRGXFzQXHUwMDBmOEStMkszKJJcdTAwMWZi2DBcdTAwMWLqXHUwMDBlzrfcs+f7Yyit7m6elPeuyrf1kzmHXHUwMDFm1cg4rLZJXHRUXHRcdTAwMTJcdTAwMDdcdTAwMWa6Q0OQiFCMXHUwMDAzlJi5N1Q4I2Cj4Fx1MDAxOXvD/bVcdTAwMDefL61cdTAwMWZcdTAwMWRstq+6Tb3JO+V9PVx1MDAxZl7LkGhcdTAwMTOWSXktozKz78hsXHUwMDE1V2irXHUwMDBiwyZ9NOdcdTAwMWI2XGYsj8yCXHL6LD5cctjIlK5cdTAwMTApLsvG8kzy6dTLXHUwMDBmq4BcdTAwMWZzWW9F6FN2V1x1MDAwM1xmfb+7XG6FzEVbdlbKiEw3hbGxJU5miEYsuWx8XrNSXG5pIJpcdTAwMTZGXHUwMDE1XHUwMDExOtqAoVcnrLjDNZ5lXHUwMDAwljH3yzU+xFx06ShJXGIyM2X7f6mUjaE2O8mRI1wiU1x1MDAwNIPxNEvm3Fx1MDAxNcG36qhlmEF9jaJ6tI0tXHUwMDA1k1LD1LIwu5hiXHUwMDA3lnDbtimSXHUwMDEyXHRrWVxmgM1cXDGMXHUwMDA2XHUwMDAwVT7x5Vx1MDAwYuWl8olmXFwornBcdTAwMTJccqBMVEguaVIocIxdf+VS4JByopNCfabMVClTue0rqdbh/b5Ef49QPZhTXsOBMsX0XHUwMDEwW93z+dW82jejUd9Q4yjYoSe0b1x1MDAwZp7CIJlcdTAwMTnCJVx1MDAxOE3BTM7AcVxmXHUwMDA3mN1Qj26FoUFlKVEx5460+1x1MDAwNFx1MDAxNcFYWEqW4OnUNiuj3JjZXHUwMDE2XHUwMDEwXCJcdTAwMTOZZNa9sIGzffKQMFx1MDAxYZtXt/tyeKxz3ZstYY5cdTAwMTFcYitbISGYXHUwMDFk99FcZlxcPjWJXHQlXGJcdTAwMTFcdTAwMDS0XaCgXGIxkpRcdFx1MDAxY2XLqI0t39WA4n1q+5at2r2z/Uo9pHlcdTAwMWKQeYfsblx1MDAxZb2F5KF6hZ7cXHUwMDFmXFysLYlD+dgodVx1MDAwZk42Lk7rL4ejmbjpdVx1MDAxNEDn4dhcdTAwMGVcdTAwMTmcXHUwMDBiTanS8ZhcdFx1MDAwN8hRXGZcdTAwMDRcdTAwMDBnyI50dtA0rY5cdTAwMDJcdTAwMTKhoW2XlbGFTe9cdTAwMWE1VLKhc7ZVW/Jqa+Z4/9ErLZeO5OXZ9lx1MDAxNDrh5N634ZLzXHUwMDFk0Tl9MFtcdTAwMDcvXHUwMDE1v96+WSndjeG+3aPmcX1rp7y0XHUwMDEx1O9fSof+j63zsW3L1EyISMg8qeSINJlduoSxLbmH6VxunDr5c05muGF5SKfMoVNBetp+5pS2PcCR3JNxds1cdTAwMWOnXHUwMDBlhnP9sX5cdTAwMDJquqWJXHUwMDAzXFxUdj9cdTAwMDE1cpqEyuwulkTbsihFSPE8Sb7pnFfoMeIwRjFcdTAwMWVlxvbcVvE4QlDuYEBLMNzoXHUwMDE5oslVJ2JcdTAwMDTg2MpcdTAwMGWjgCkqIa2Fllx1MDAwMlx1MDAwNykos5V0XHUwMDFhf6KlRG9xXHUwMDA0Tlx1MDAxYTBOh+rqM/Y4QlxiSkfKaI57I1x1MDAxMjhUXCJZx+jbprq0MskwQjjEXHUwMDFlN4RcdG2f/ZBk7IWiiHz0xqNcYklt/Vx1MDAwZVx1MDAxN7ZcZl0xlVx1MDAxMEk7XHUwMDEyY1x1MDAxZmVcdTAwMWK5aUWJTM7GZ1xuXCKy9dq+XHUwMDEyXHUwMDFhPdYgQqvMVkVWJ1RsZ/Ug81Zb3VIrRy+Px7v3z3urm9v+9f7jjDtcdTAwMTVcclxcdeFUOVxmXGZD48VRoYyMW7de9Vx1MDAwZdo1MDaLRcRcdTAwMDStW7FcdTAwMTiCXG6NTidatDCbXHUwMDEwgtzfXHUwMDFjlLdWa+X25Vn1uHPaZJuXT7831Udzwya+XHUwMDBiXHTNWnZBXHUwMDFkKG2fVjBE5jJ9muZcdTAwMWOSnDg6XHUwMDA3krZ8ZyqQTOvAkkL2kVx1MDAxYtk1hOl0YFx1MDAxOVpcdTAwMGI/RvZ/lcWoqdfvXGZwJln1O+qj9TsmO9TWUvPeWlFh+Fx1MDAxZEO9Jsjyj4fjhzOoPnWPbneuunNcdTAwMGU/ZF9cdTAwMGUyfeRV1G5cdTAwMDeH/vpcdTAwMWS7tIaUXHUwMDEx9dDk9Medkju0JVx1MDAxMchcdTAwMTWVnnVKbf3+XHUwMDA2bpmskcf79o/d5TX329Nz4c5hXHUwMDEz9Vu2gymfuN+yj8fJ9FtcdTAwMTTokHVv6cM558DR1NHZwFx1MDAxMeDoKVx1MDAwMKdYXHUwMDAxXHUwMDBmwUBdXHUwMDE5JulUMlRDq+DHnNZsKnhcdTAwMDbY+vFX8GSXeWtkioxcdTAwMDEp3lx1MDAxZiefkc9pZsp2wLG8XHUwMDBiXHUwMDE0aIK0MN5cdTAwMDNMKoaAY1x1MDAxYc+B4Vx1MDAxYbKbzH54gdtcdTAwMTJSYXu54MdQalJcdTAwMTDIpW0mQoxcIr0nN5DEU1W17fIuh2s2Pe60lEGNXHUwMDE56jFcdTAwMDSRXHUwMDExLZSWKpxcdTAwMDOybaeEwbhcXFx1MDAwMtJdXCIjnfhcdTAwMTdcIkvJtm8oKIlRkaIjPykun2jGheJ2kLTtXGbJKfL+lDV34iiGsVx0Z0Joxpn55Fx1MDAxZHIyVdu+XHUwMDEySlx1MDAxZN7uS/T38Gl3XHUwMDE2cZ79xcD24YHUXGaxZzafXc2rbVx1MDAxM9ox9slrRCvOXHUwMDE19Fx1MDAxN+9oRzImeoVcdTAwMWaCkcl14kBcdTAwMDVcdTAwMDD7uC6tKUFcdTAwMWIlU9a/mHGQ/YHtQsqZ3Vx1MDAxY9tv26Tdi4k2epZcdTAwMWJmR6cg47ZtNn6SYG2IsrUhXHUwMDEwZmBDK8JcdTAwMWRj60okXHUwMDA2o4xSKZJcdTAwMGacLGTa8ilJXFwmxlx0t9V46E2FTjFsXHUwMDE4XHUwMDE0okhUo1x1MDAwMUT+ysTnzrlnqrV9JVx1MDAxNHpIu5ZcdTAwMTUjXHSZmWy3T3lcdTAwMTmi39fz877ZbW3uXpOT/c7SycqZ9+1ged5NXHUwMDFhU8ThvTJPaqjdnFx1MDAxZDdpgCaNgCUhhGnOJ/dQbZPymHuRiJC4ffpcdTAwMTmHKVx1MDAwNUh2q/pIzGukR2o7jvOz+ZUsXHUwMDA0db+7INNX8SNcdTAwMTMwXFyYXHUwMDE0tNpZMVLsXHUwMDBi9Vx1MDAwN0T9Qr1C7MtcdTAwMWKEXHUwMDE3K+32YYCj9m7ucD682ttXXHUwMDBmb7z44LmPy0mN+NdV72Xv2oOtxYjbczT/fPnnf+6uXHUwMDA3ViJ9 ByteEditor()Container( classes=\"top\")ByteInput()BitSwitch(0)Input( placeholder=\"bytes\")Container()Label(\"0\") Switch() BitSwitch(7)Label(\"7\") Switch() ...(1 thru 6)

Now that we have the design in place, we can implement the behavior.

"},{"location":"guide/widgets/#data-flow","title":"Data flow","text":"

We want to ensure that our widgets are re-usable, which we can do by following the guideline of \"attributes down, messages up\". This means that a widget can update a child by setting its attributes or calling its methods, but widgets should only ever send messages to their parent (or other ancestors).

Info

This pattern of only setting attributes in one direction and using messages for the opposite direction is known as uni-directional data flow.

In practice, this means that to update a child widget you get a reference to it and use it like any other Python object. Here's an example of an action that updates a child widget:

def action_set_true(self):\nself.query_one(Switch).value = 1\n

If a child needs to update a parent, it should send a message with post_message.

Here's an example of posting message:

def on_click(self):\nself.post_message(MyWidget.Change(active=True))\n

Note that attributes down and messages up means that you can't modify widgets on the same level directly. If you want to modify a sibling, you will need to send a message to the parent, and the parent would make the changes.

The following diagram illustrates this concept:

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1bbU/byFx1MDAxNv7eX4HYL3uljTtvZ14qXV1cdTAwMTHabmm7QFx1MDAwYrS3vVpVJjHEJYmzjlx1MDAwM6Wr/vd9xkDsxHFcYmkoUF1LpYk9XHUwMDFlXHUwMDFmz5znec45M/n70draenY+iNafrK1HX1phN26n4dn6b/78aZRcdTAwMGXjpI9LXCL/PkxGaStv2cmywfDJ48e9MD2JskE3bEXBaTxcdTAwMWOF3WE2asdJ0Ep6j+Ms6lxy/+P/boe96N+DpNfO0qB4SCNqx1mSXjwr6ka9qJ9cctH7//B9be3v/G/JujRqZWH/uFx1MDAxYuU35Jdcblx1MDAwM6W2bPr0dtLPreWGXHUwMDE5LomsXHUwMDFht4iHT/HALGrj8lx1MDAxMYyOiiv+1Hp7I1x1MDAxOVxmXlx1MDAxZf4uP71qXHUwMDFldk5cdTAwMWJCfNz5XFw89yjudvey8+7FWIStzigtWTXM0uQkelx1MDAxZrezztXQlc6P71x1MDAxYiZcdTAwMTiG4q40XHUwMDE5XHUwMDFkd/rR0I9cdTAwMDBcdTAwMWafTVx1MDAwNmErzs79OVa84MUwPFkrznzBN6VYwPFHSSdcdTAwMWOXjunxZd9cdTAwMDHOXHUwMDA2TFx1MDAxMmdCa+Gc4VOGbSZdTFx1MDAwNlxm+4WOZLulXG7TXHUwMDBlw9bJMezrt8dtsjTsXHUwMDBmXHUwMDA3YYopK9qdXb4yJ1x1MDAxYmjmpOKKlDFE41x1MDAxNp0oPu5kaGJN4Egz6yRcdO6kdoUxUT4rVlx1MDAxYiVcdTAwMWOZYvi8XHUwMDA1g6127iF/loet375cdTAwMWO2/qjbLYz2XHUwMDE3nk17VdmzJrwri75cdTAwMTRvUvKEdCdcdTAwMWWdb1Bz1Njdf7v59Vlj820nXlx1MDAxZrf79tvsbi9ubkafs99Pn37gpn9+4I6V2Op3pp5y9fwwTZOzRftcdTAwMWQ4+vzcbDe/dj+yNOp8lVx1MDAwM2lcdTAwMGZW0O+zt9m2ftE8dPZsZ1s8c3uvKVmFve+fvZJvXHUwMDA3XHUwMDFmtYuy5qe9/us/WKPXWazfy0/FhI9cdTAwMDbt8Fx1MDAwMrhcXFx1MDAxYsuEs0qRLlxcvVx1MDAxYvdPpn2hm7ROXG6sPypcdTAwMTlcXGGZXHQ/KFx1MDAxM4w1NH16TDDWOSGVYHZhgpntVktcdTAwMTHMNI5vkWBcZlx1MDAwYpSyhitNmoQuXtffr7hccpzj3Fx0xom70mCsml9cXNH1mFCo8IBLXHUwMDA2IW1cdTAwMTiXJTO+l0BW6YPFTCf9bC/+6lx1MDAwN1vIwFx1MDAxOKOc0lJcdTAwMTitmJto9Dzsxd3zibnLfVx1MDAxNYO1m4/Tr/8qj+gwglx1MDAxMb5XZSfab3TjY+/P6y3cXHUwMDEypVx1MDAxM66exZDmcYPDJMuSXtGgXHUwMDA1I0L0mW4tXCKRSVx1MDAxYVx1MDAxZsf9sLs/beNcXPTN1XjFLK+FoDRcbjOxOFx1MDAwMF80P785y87Tk8M/PvRenGyop6/l2d1cdTAwMDLQXFyHP2FUoJ0w0mjOlZN8UuDJWK//Vlx1MDAwYoDTcuFqXHUwMDAxyPLjblx1MDAwNVx1MDAxZfZJRCpOr1xmn8tcdPxr2j7/LyUv9ejAfvncO2Bb7tPxzyrwbz5svd9I3iE0fNVP4iMxXHUwMDE4ZsasSIitI0xp4di3JMSIXG51XHUwMDFkXHUwMDBiQJtcdTAwMDRBf1x1MDAxNmaB2ZN/v1lAQmeVM2BDKa21JZnwt2tGXHUwMDAxlFx1MDAxY3PBXHUwMDE4Mabp1kiglEDMUWEr4Gyal7Kz25NheKDmdyfDm5242/7BKnyNjE2r8JWJ3yHC4PNaXHUwMDExhi44y1x1MDAxMFx1MDAwMS5cZkDqf+2evmxcdTAwMWOIvZPPO/uHZ+HWwba551x1MDAwMNRcdTAwMDJcdTAwMTCDe8DhhFx1MDAxNFx1MDAxNVx1MDAxMTZwXHUwMDFmidRVO01GqlvD30pEWCHFdkqy1cFzOVx1MDAxNY7C072NdP9Mmv2/tt8kfGh3mns/QNV+wnSYXHUwMDE0XHUwMDE38tZVXHUwMDE42VUtXHIwqaWU7lx1MDAwNvW22dN/z2lA+3zXh+NETDNpJnhAM1x1MDAxNVx1MDAxOMRDllx1MDAxOKJcdTAwMTI9p9r2Y3TYOKRcdTAwMDZCKLUyoK/SXHUwMDA3XHUwMDFmvFx1MDAwZV+jY0vp8Fx1MDAwNfZngE9oUa/BllxmyF6oxVPh+UnMfS12W1x1MDAxNzhcco+2UivLidRcdTAwMDT8oGtcdTAwMTBH4sxoq1x1MDAxOLPThlx1MDAxNfDjVqvw8DuKUSpAXHUwMDFjYJhcdTAwMDLtcTzRVNFoRIDQXHUwMDE00TozTlx1MDAxMKeKXGZjvlx1MDAxMFCQpZtUu1x1MDAwYmG58lx1MDAxOHF55ttyoF26hjXMwjRrxv123D+eNOxyVWeRilGO69bIW9lggVx1MDAxMFx1MDAxY1x1MDAxY8KJWbJWXCKyLzU7XHUwMDBlXHUwMDA3efhcdTAwMTNoLlx1MDAxZIfN1nKjuKy8fdRvX2/V/FxietIqXHUwMDEwkZPge8hcdTAwMWLhqFx1MDAxYeVcdTAwMDKtlOKwyFx1MDAxMUlesahcdTAwMWJcdTAwMGWzzaTXizNcZv1uXHUwMDEy97PpIc7HcsMjv1x1MDAxM4Xt6at4o/K1aYpcdTAwMTj4XHUwMDFlJ6Ow4tNagaD8y/jzn7/NbN2QNmCOw3G5VVx1MDAwZTGk1OX7XHUwMDE1hoOUVYj3cZVLc21/tUjxR1x1MDAwNSNFd4/K/9+YLJWoXHJUyFxi6Vx1MDAwNWbxOGV+pLlcdTAwMWGqbCfeOVx1MDAxNudKupYqSVx1MDAwNcz4ckH+skpPZiyai0AppoEgXHUwMDAxPiUzZdgqM1x1MDAxNlxuYIxcdTAwMTKet5mRZlx1MDAwNlfCXHJcZlx1MDAxN4asYX6lUlbL+iSVsfpuqXLp+GZBqrxcdClcdJJcZrNmpDbMKWZLsLpkJcFcdTAwMDLIoCGlreRcdTAwMDaR6XJMOT/GKVx1MDAxOcWCXHUwMDFj4dxIJpnmXG6onknfXHUwMDFhyavQxLWDUfSgybLetf1RcepcdTAwMWKSW1x1MDAxZbzO4DZuarnNK1x1MDAxMIaWXHUwMDE3VYHruK3RODxcdTAwMTl0e413nz703n462uc7NnyzXHUwMDFjt01cdTAwMTc9bi9cZoRPXHUwMDA3ym92IORcYkK5qWJcZiSHXHSGLFxmLlxi4qintpZcdTAwMTOhXGKXpzZJXHUwMDAxQWl8XHUwMDFjKiGYppSAjalccnpqXHUwMDEwqXCutJLWQVx1MDAwMitxoECw6inmzrhccjKAaEtcdTAwMTdcdTAwMTO4PLdNY7HmyopRPnFtxfFQ7Vx1MDAxY/ujOrsrXHUwMDAyuWD1q57EuGWalVxue9eh/JXZ33p3tN//a3evXHUwMDExd3Y/7vY2yzi+pyjnXGahKHNcdTAwMWF6XCJcdTAwMWSjyVJcdTAwMGI5XHUwMDFl+HqTXHUwMDE2mlx05Ma3XHUwMDA3cul11GJCuJRCilx1MDAxOeFcdTAwMGLoXGJcdTAwMTk4poWk1UaUmlxcVVxckUEohqt3inFokSpM+z/Gr47aXHUwMDE59kdlbm9cYvD6XHUwMDE0xcnps+NcdTAwMTRFwFuM0IunKPOXju9pNUdqXHUwMDFiKM9kklx1MDAxY1x1MDAxM0pNXHUwMDE2c0grjDyzSmryy1x1MDAxOfVrKpE25nsyXHUwMDE0K1x1MDAwMzyFM1x1MDAwYiVcdTAwMTZOmFx1MDAxOWsqXHUwMDA2TSxcdTAwMTfqosTNbaWY44zz+UkxXHUwMDEwP2Ep5yb5XHT32039tjHpk49S4lbkXHUwMDAy0pfymN9GZ1x1MDAwNMPs28rbL5Sg3KjAhEArz4c1hEXzqk0skPA0MLVcdTAwMGaJyIxcdTAwMGJcdTAwMTJcdTAwMGYzP/HFdMF93VxuaVwiXHUwMDFj1JXvbihcdTAwMTko5vNIvKt23OrruqvFSd5dXHUwMDA1XCKrXCJKcHLtJkxcdTAwMTCHVCRcdTAwMTYve89fhbunREkqr0JcdTAwMTJcdTAwMTG3xu+PniBK5D+B4yRcdTAwMTTmXHUwMDA1woHxqGXK7y17I71Hwmu19lm8sULPqnv7QoBcdTAwMTVcdTAwMDC+c1x1MDAwNKtLw3RcdTAwMTlcZjGEyoIxW9j5XHUwMDEzXHUwMDE2c1x1MDAxNqYlsJIzoCUlXHL+Icx1pVxm44qXMMPgLtKCIVx1MDAxM1x1MDAxMXrJqvdccmo5/udcdTAwMDJKYKqM407MsIhcdTAwMDXgUWNcdTAwMTCngDyMIFMx6SExpVx1MDAxNFx1MDAwMahcdTAwMTDqIJj0xepcdGqTKlBcdTAwMDZcdTAwMTFcYsf0cKmEvK63epj4o1x1MDAwMpBVXHUwMDExJWZrzs9hpDQgS7Z4TDl/I8Q9pUptKPDcXCKl31x1MDAxZlOiwjykJFx1MDAxYvhanZXI11x1MDAxOVx1MDAxOODWQkpHgfNLQlpcdTAwMTE8h5XixTFPalx1MDAwM6+CjVx1MDAxMpE+VNNUat7cec1G9Fvc/Fx1MDAxM/LkTerLXG40o1x1MDAwNVx1MDAwNtRxSZzPiCldQIjsuEDCRuAtW2Wl1caULIBNiLBcdTAwMWPYm/zCs6pcdTAwMTbiPVU643dDglx1MDAxMrhGXvOgqbIhXHUwMDFj0mXn02XwpCPi5dtcdTAwMWKKw/MtoCfhv1baa8myUYuV/GpcdTAwMDUmN2TLur1Mtv6HPbBcdTAwMDQxp6XC7a+jyk7nTTM92n16MNJnh9REYrT//PzeUyVcdTAwMDZcdTAwMWUyZIB0+G15xfTqh4O+oCm130RcYlx1MDAwN741ruRyxi97RKVMzpGpSFx1MDAwZeYuLLm1zUxcYscs5Hupgnh5M1x1MDAxM5s4O2f3Ulx1MDAwZuaGx9Fw7dfRYPZcdTAwMWUmXrOHqVx1MDAxYlx1MDAxZE369+RcdTAwMGWmLFx1MDAxOdRtX5p4mem9SpNcdTAwMDYthTBobFx1MDAxZMSQYlvos1i8gp18jJ/2qdGmdCS2XHUwMDA2zVx1MDAxZGaH2+9cdTAwMWVcdTAwMDLCXHUwMDA09EprST66nMrbiFx1MDAwN2SsX1xi9Vx1MDAwYqeO32LeZtRCXHUwMDEw035DkrP6x0DMr1UuVY9eXG5iYZal8eEo8z7dTs7691x1MDAwMmZVoy6g9uhSLtfDwWAvw7iNg1x1MDAxNcxI3L58+aLr9dM4OmtWveKXo/zwvebw9UCJ8jjx26Nv/1x1MDAwMC2LavoifQ== Parent()Child()Child()messages (up)attributes (down)"},{"location":"guide/widgets/#messages-up","title":"Messages up","text":"

Let's extend the ByteEditor so that clicking any of the 8 BitSwitch widgets updates the decimal value. To do this we will add a custom message to BitSwitch that we catch in the ByteEditor.

byte02.pyOutput byte02.py
from __future__ import annotations\nfrom textual.app import App, ComposeResult\nfrom textual.containers import Container\nfrom textual.message import Message\nfrom textual.reactive import reactive\nfrom textual.widget import Widget\nfrom textual.widgets import Input, Label, Switch\nclass BitSwitch(Widget):\n\"\"\"A Switch with a numeric label above it.\"\"\"\nDEFAULT_CSS = \"\"\"\n    BitSwitch {\n        layout: vertical;\n        width: auto;\n        height: auto;\n    }\n    BitSwitch > Label {\n        text-align: center;\n        width: 100%;\n    }\n    \"\"\"\nclass BitChanged(Message):\n\"\"\"Sent when the 'bit' changes.\"\"\"\ndef __init__(self, bit: int, value: bool) -> None:\nsuper().__init__()\nself.bit = bit\nself.value = value\nvalue = reactive(0)  # (1)!\ndef __init__(self, bit: int) -> None:\nself.bit = bit\nsuper().__init__()\ndef compose(self) -> ComposeResult:\nyield Label(str(self.bit))\nyield Switch()\ndef on_switch_changed(self, event: Switch.Changed) -> None:  # (2)!\n\"\"\"When the switch changes, notify the parent via a message.\"\"\"\nevent.stop()  # (3)!\nself.value = event.value  # (4)!\nself.post_message(self.BitChanged(self.bit, event.value))\nclass ByteInput(Widget):\n\"\"\"A compound widget with 8 switches.\"\"\"\nDEFAULT_CSS = \"\"\"\n    ByteInput {\n        width: auto;\n        height: auto;\n        border: blank;\n        layout: horizontal;\n    }\n    ByteInput:focus-within {\n        border: heavy $secondary;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nfor bit in reversed(range(8)):\nyield BitSwitch(bit)\nclass ByteEditor(Widget):\nDEFAULT_CSS = \"\"\"\n    ByteEditor > Container {\n        height: 1fr;\n        align: center middle;\n    }\n    ByteEditor > Container.top {\n        background: $boost;\n    }\n    ByteEditor Input {\n        width: 16;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nwith Container(classes=\"top\"):\nyield Input(placeholder=\"byte\")\nwith Container():\nyield ByteInput()\ndef on_bit_switch_bit_changed(self, event: BitSwitch.BitChanged) -> None:\n\"\"\"When a switch changes, update the value.\"\"\"\nvalue = 0\nfor switch in self.query(BitSwitch):\nvalue |= switch.value << switch.bit\nself.query_one(Input).value = str(value)\nclass ByteInputApp(App):\ndef compose(self) -> ComposeResult:\nyield ByteEditor()\nif __name__ == \"__main__\":\napp = ByteInputApp()\napp.run()\n
  1. This will store the value of the \"bit\".
  2. This is sent by the builtin Switch widgets, when it changes state.
  3. Stop the event, because we don't want it to go to the parent.
  4. Store the new value of the \"bit\".

ByteInputApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a32\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u00a0\u00a0\u00a0\u00a07\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a06\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a05\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a04\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a03\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a02\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a00\u00a0\u00a0\u00a0\u00a0\u00a0\u2503 \u2503\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2503 \u2503\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u2503 \u2503\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

  • The BitSwitch widget now has an on_switch_changed method which will handle a Switch.Changed message, sent when the user clicks a switch. We use this to store the new value of the bit, and sent a new custom message, BitSwitch.BitChanged.
  • The ByteEditor widget handles the BitSwitch.Changed message by calculating the decimal value and setting it on the input.

The following is a (simplified) DOM diagram to show how the new message is processed:

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1daXPiSNL+7l9BeL/MXHUwMDFiMdbUlXVMxMaGb7vb99l4e8MhQLZlZKBBPjfmv29cdTAwMTZ2I4FcdTAwMGVcdTAwMDTGgHteOtrYkiilqvLJfDKrsvjvQqm0XHUwMDE4Pre8xT9Li95T1Vxy/FrbfVxc/N1cdTAwMWV/8NpcdTAwMWS/2cBTrPt3p3nfrnavvFx0w1bnzz/+uHPbdS9sXHUwMDA1btVzXHUwMDFl/M69XHUwMDFidML7mt90qs27P/zQu+v8y/7cc++8f7aad7Ww7UQ3WfJqfthsv97LXHUwMDBivDuvXHUwMDExdrD1f+PfpdJ/uz9j0rW9aug2rlx1MDAwM6/7ge6pmIBcdTAwMWH44OG9ZqMrrSGEK0JcdTAwMDTtXeB31vB+oVfDs1cos1x1MDAxN52pdVx1MDAxZs9cdTAwMTfl/ZPjQ7ax/0Rf/MOL5tI2j2575Vx1MDAwN8Fx+Fx1MDAxY7x2hVu9uW/HhOqE7WbdO/dr4c3PnotcdTAwMWTvfa7TxF6IPtVu3l/fNLyO7YBI0GbLrfrhsz1GSO/oay/8WYqOPOFfXHUwMDAwwpHAiFRGXHUwMDFhQ0x05+7nJXWEUExSpoXgQOSAYKvNXHUwMDAwx1x1MDAwMlx1MDAwNftcdTAwMDdcXPFaVUSiVdxq/Vx1MDAxYeVr1HrXhG230Wm5bVx1MDAxY7House3R6Y8uvWN51/fhHhQRlx1MDAwZtXxulx1MDAxZFx1MDAwZlx1MDAxYSRKZFTvhL1Ja7vW1YH/xHumUXvrmcZ9XHUwMDEwRHLZXHUwMDEz64N6XHUwMDEz150+/Vx0vadI2NhgP1x1MDAxZl3723opuPpa11+vvJ1rslx1MDAxNJ4v9q776/f0Zl8/zF42audnXHUwMDE3x0vN3X3plVx1MDAxZla/3Kq1/rv8vL/bbjdcdTAwMWZj7b79XHUwMDE2Peh9q+a+6iSVSlx1MDAxM6FcdDdMR4NcdTAwMWP4jfpgXHUwMDFmXHUwMDA0zWo9UuOFmMBcdPz0PX9cdTAwMWM6hoos6FDNmEFRiC6MnfTuXHUwMDFjXHUwMDBiO3R62FE0XHUwMDBmO5o51ExcdTAwMDU7RiehQ80gdKhcdTAwMDZcdTAwMDZcXFGYXHUwMDE4dLK0kDJKiVx1MDAwMjKKXHUwMDE2RmPdbITH/ktXkWTf0Vxy985cdTAwMGae+4arq57YPSvPobfedVxmv/1fvFx1MDAxZjtcdTAwMWXeudtcdTAwMTTv+8xy4F9bNV6s4rN47T5cclx1MDAwZn30Nb1cdTAwMGLu/Fot7j2qKIiLbba3i1x1MDAxOP1m27/2XHUwMDFibnCSJmcu8HJcdTAwMWRcdTAwMTdHdGWhj1FcdTAwMDaGoz6qwujTle3d1fKjvOzcbLeCSiB2XaPn3nNJ7lx1MDAxMK0oVcSAVMz0oY9p6aAx1Fx1MDAxYzVeXHUwMDAyZVx1MDAwND5cZn2o60U8XHUwMDE32lx1MDAwMkEkxaGZsetqy/UtVl7+tvTilqv1tc7S4flyY1xuriu3XbF3V79cclx1MDAxZaTim+et2y/N8mXdrU+g3VBcdTAwMDSbT4dXXHUwMDBmXHUwMDE1oYOV2vPm84/bJz1cdFerNChD0PhHivVBrpZrk8lSKTIhTlx1MDAwNep3YbCnXHUwMDBm/ydwtdlg50Q6MFx1MDAxZLBcdTAwMWKRxHqKq1XGXGJ0+zGv81x1MDAwYvlaPzx+9MPqzW9qur52iJtK+NqYnKV8Z/tcbv007IEhmdijVCilmCpcdTAwMWVcIuZbzznFnqTSQTZhXHKe6uKv39Ey6ShcdTAwMTDCWkJqiFx1MDAwMJ2JPdJ9jY894qBcdTAwMDWQXHUwMDFhKbVcdTAwMDEgRpuUiFx1MDAxMYgjXHUwMDE41VQrJVx1MDAwNIYgbFx1MDAxMJqGI2Y4k6NcdTAwMDSQkV/5qTHs7chfo1x1MDAwM/ZniEbHXHUwMDAxbCd02+GK36j5jet+wd5SIUVYqX1mt4XXXHSHXHUwMDEzjFx1MDAwN1xml0pII6L+7Fx1MDAxYYHqfafb6Tiw1t1cdJAouFx1MDAwNtCJZ/dcdTAwMWG14TLlo7cnk8ZBRlx1MDAwMov/0LlyXHUwMDAxXCJdJlxmuqjSXG7sf1x1MDAxY26iXHUwMDEyQlx1MDAwNW4nXFxt3t35Ifb9QdNvhIN93O3MZVx1MDAwYv1cdTAwMWLPrVxynsWHip9cdTAwMWK0XHUwMDExLdtiP1x1MDAwYot+K0VcdTAwMTDq/tH7/T+/p1+dqdn2ldDpqLmF+PtYoVx1MDAwNDbHXHUwMDA2XHUwMDBm9yxcdTAwMWNcdTAwMTJcdTAwMGJcdTAwMWNwXHUwMDE5c7fDLFxc5UTedbbWn/ZcdTAwMGVEsFX/8m19tbV1PFtcdTAwMGKnhpJcdTAwMGJcdTAwMDKOUlx1MDAwNrhcdTAwMDTgQotcdTAwMDFcdTAwMGKHps1yXHUwMDBmSz9cYmp/pJFcdTAwMTM3cKmRXHUwMDA0RMfeTFx1MDAxOGPaUFxudNY5sCevfHZ5e7N50ZKrrfrB8u7LVc2fXHUwMDAy4Z9cdTAwMTdijujIzoFcdTAwMTmUwlx1MDAxMCOL58DSu3POoYPYYFx1MDAxMXRcdTAwMDaAw2FKwIk501x1MDAxY1ZutFx1MDAxMFIjaZtcdTAwMWEpXHUwMDFmy8ePRcp33IpcdTAwMTf89n1RfV9EsjtNVj7E4lx1MDAwZrLyfkHf4bdoXHUwMDBlM1x1MDAwNyqBXGJcdTAwMTGLm4eBr3a6t7dzclY7P0TQ3Vx1MDAxY79UV59cdTAwMGXUnINPcqTehHGMM20kovvzz1x1MDAwMmk7kUxQJJyKXGIjs4PiKfktqlx1MDAwNMbnJJ7OmI3jkpvkaUM9fttvXHUwMDFmeU+rXHUwMDE3661V9+jub5RRXHUwMDEyLCejhOacauQ8xTNK6d0559hcdTAwMDHlsGzscDot7EiThE7SdXHKXGaGXHUwMDA1ZnpzN9NzXW9Jmim7rSFcdTAwMDZ/0G1FQuZcIi4zkcQgliZcdTAwMWGAnNboy/goUVY+eZ7XPFx1MDAxMoZZVDFUY2lcdTAwMTBYnPZBjlx1MDAwMzhcdTAwMWPhyKnmiEnGP1xmcYDYNpJcdTAwMTjOXGbjUsTuXHUwMDE0+S7pUINcdTAwMDNiU+tKMMVcdTAwMDfxSFx1MDAxObOeN25cdTAwMTimm0ZcdTAwMWHLWcS6tFBcdTAwMWGpcMpcdTAwMDZcdTAwMDNSMFJcdTAwMDB6d8K0jl3xM2WzZONcdTAwMDTCKJGK2excdTAwMWJRSiZcdTAwMWW+UFx1MDAxZSmfb8aEXHUwMDAywOFlaDhcdTAwMTUnPDJH/UJcdE1cdTAwMTWnwIBcdTAwMThhM2BcdJk+U1x1MDAxYWkpU7XtK6HUUXNcdTAwMGLx99FtXHUwMDFidlxcpm0jXGJ5SiFS02G2LZ9fzatt48xBViVcdTAwMTRcdTAwMWE3XG5aRqHuq20zXHUwMDBl2jeBJyQ3mis1INhkjVx1MDAxYva3wpDbKIRaynSVQLyitlM0XHUwMDFlXHUwMDA2JY5f8zPBRJTQXHUwMDA0Ufr/1s2aeodcdTAwMTMp0WppJVx1MDAxNGozxC6JMtJcdTAwMWOQMnNjKHInKWQyXHUwMDFmXci45bOSmHFcdTAwMTNGXHUwMDAxt/8p1TLVuKFInFx1MDAxYqIpJ8SumDNJe/uZbFumYttXUqVHtG25qVx1MDAwNqpoJndD5ibRmlIovtqGtOvbX7dcdTAwMGWDXHUwMDEzctmRrdN6eHrS+jL3XHUwMDA2zlx1MDAxOIcx27WMaWl0xO67k4CGOVRqVDc8XHUwMDBm6G6z7VtFMVxyufbt7Vx1MDAxYSGS1o2ylGhcdFx1MDAxMvZcdTAwMGJVRFxuytHQvst+0eH2q/eZkfJcdTAwMGbaf1bNaq3R2Vx1MDAxNPvqWMOB6bxcdTAwMTROnMPJ9cph58vt3XVjjSw/XHR2tr4rP1f+gVwiWcxcdTAwMDJcdTAwMTRGQtTOXHUwMDAwysJ4Su/NOceTXCJcIlx1MDAxN096cnjKT92RlPCHJ/JcdTAwMGZMXCKbVFx1MDAwMqa3noWNoITRWMfyXHUwMDBm1EHDzImWTKBKkdhUTV8+oj+9sNhcdTAwMGL1ndVcdTAwMWJcdTAwMWM5r/bb94Y9++BcdTAwMDb33j9P2vfe90b6qlx1MDAxNy77WurlIVx1MDAwMu+qXHUwMDFmXHUwMDA0I6UphjiL9DRFruzjcXyusyk+XGJcdTAwMGV8hEKJfFx1MDAwYjZcdTAwMTnA1tzOjTdRxGqGPk5TXHUwMDEwXHUwMDEyQCPp6l+CJoh0XHUwMDE05UA018hR9MchXHUwMDE2hIO3oFx1MDAxOHNR5OiKcEhcdTAwMDJcdTAwMThcdTAwMTRyViosYUJahOBcdTAwMWTEM0ib1Fx1MDAwND5cbp4nTvC54eojXHR++NU/OHKXbzfPt063j2twpra93TiZjqgyM2huKTfUXHUwMDE4ZVikyz3GXHJcdTAwMGXaarxCXHUwMDAypZxRNuZcIph8OPeLpDhcdTAwMTeW7uJQXHUwMDAz4TIhXHUwMDEyd5jgikpk91xmLTP/7LmLTL1+PT2o0pNk+JzFqOLgenpAXHUwMDFlaVx1MDAwN7+wfSt7QeXg7GqLPD7Cycry/ZHoXFzuz/1yeux/ooFcYlx1MDAwNmBcZiH9XHUwMDE5XGbQxulcdTAwMDaTimMsqSE7gfH+1fSRsYrsWYKQaHTsXFxyMevF9GflzfJcdTAwMTKtrHjV1epcdTAwMGZf3vDypbgqSuXp4cF6cNsgq/7h3kZwtXfyrHaCidSBWVx1MDAxNtWN/j+aynNmMpFDQWpcdTAwMDPoXHUwMDE0WWHopHfnnHP510qUXGI6/VxcXlx1MDAxMuOg3ZpcdTAwMDJ0XG6WgVx1MDAxMYw7qITY/MtHcvlRtTCVy89/XHUwMDFk2Fx1MDAxMJv/QXVggtHMRTCAPotJw4tcdTAwMDfSO/ri6kbpx1v/oHy+fPz88Lgmzubeb0nmXHUwMDEwZsBcdTAwMTBKpCZU94FcdTAwMGa7XHUwMDAw/Vx1MDAxNmBcdTAwMTQtKFKKWFx1MDAxMDOrKjAlJGKPTbAyZDzH1VDrbnAsXq7Kl1qrs8ZdsIy+5+NcdTAwMWRXbruHpzWobZRltVx1MDAxNXYuqD5mlaO9/Vx0tNtoP/JNODva6ZgnODhZXHRX7vjW53K0gsVcdTAwMTZ/JCpRrOtcdTAwMTkpaZY+/PPuaFx1MDAxNcnDumCOnlxu1otcdTAwMTWBcTvNSmBKa3am7GdnVVx1MDAwMzbER31EXHKYkCxz+sdmgjhBT1tcdTAwMTh4+aZzToEnKXGEsrVCXGZcdTAwMTksVf25L1x0zFx1MDAwMZDKXHUwMDA2yoxzPSjX5Ga3hWNX7DFcdTAwMDWKo1x0gFx1MDAxNFx1MDAxOErqoFx1MDAxOVSaaWWUMDpJf1FAjpxcdTAwMWNmVlx1MDAwMNZFq2CxXGJ78qmvfDpaXHUwMDFhLO/SXHUwMDFjiNLG1lGp+Oqdn2ViXHUwMDE4Llx1MDAxOIGBg1x1MDAwMmCCQ+LZXHUwMDBi5b7yoVvKLO/CuybzcdpB6ElDqVx1MDAxNFx1MDAwNohmn3tyO0uv7Suh0VFjXHUwMDBi8ffxXHUwMDAyXGIhs6e2hUShsG8jRVx1MDAxZGbc7k9b6ujBe3jY2Hrcba1cdLPjXHUwMDFkzdi4XHUwMDE1qP7iaNuwbzU3nLBcdTAwMDFSgdbOXHUwMDAxQrStuELkfmB1a9FF9MxWwaNGzDqA8I8qa1crtdtccvdCfDup1Fx1MDAwM1J396dA9OeGkEM2dFBPXHUwMDEwWECL04L03pxz5FDlmGzkSDkl5Fx1MDAxNKv+UoxcdTAwMDDGSXJyXHUwMDE54/mh47Or/lx1MDAxYWLxP6z6XHUwMDBiVOZcblx1MDAxMqQo1Fx1MDAxOGGKXHUwMDA3w99u7veO/fLVKWeN3VbraYtvm4c5XHUwMDA3XHUwMDFm+mZHcIyDpTJMx6ffX91cdTAwMTaSKGq3YEPk0fhsznTcVjLvhUxcdTAwMDOMgZnvfnS6slG5fdg/uEJd9Y/LnFc3Ksd/pzySjFx1MDAxOH5yMaOmwG1qpTB00rtzzqFcdTAwMDPaMdnQkeDAVKBTrPSLMqa7ZWi/4nzNbGq/htj7Sdd+cWmyIVx1MDAwN5JQu1a1eJCVz53nNYNEmMNcdTAwMTlGuJJcdTAwMTDDgffvXHUwMDE0oFxmdaRcdTAwMTZcdTAwMTjjaoVRp1x1MDAxOZw+mmBcdTAwMDbJOCCMUZJwgy5Jpm0hhFx1MDAwMbe0Y4ZgQHljXHUwMDBiP3r7b0hQo/ixT5c+KpyqsWVd0ih0XHUwMDFjSPPRoEZ7kpSyqsN44tFcdTAwMGJlj/KJZim7qFx1MDAwYjiFXHUwMDE0ofqqw0RCps+UPVrKVGr7Sqhz1NxC/H10s2ZU5s5BjFFcdTAwMWPzUTh4PrWaV6vGucPRbFx1MDAxMSYpKvfAKm5lwDFCITaowivIx+XFke1TxolmVlx1MDAxOMVT4mFcdTAwMDFcdTAwMGW39Y1GIErBJEsmUH2UlCNFx7+uWXst51x1MDAxMnainiB2XGKw5PJLaic9bEZcXOFVaEjUmCVf+WSklF3QlVx1MDAxNEk7SGmFIYBcdTAwMDZQXHUwMDEzwz73itAspbavQXVcdTAwMWXRpuV/K1x1MDAwMIPsJW2ca6kkXHUwMDFiIUI65N5cdTAwMGVpiy1cdTAwMTnenZZbT5VvlVx1MDAxM3ExW8Mmhld7Scfmy4TQXHUwMDAwXHUwMDE4gPTvjqFcdTAwMTh3hMUqdoPdXHUwMDA0M5utuZJ6opZr1/7hSWE3i0+r9lx1MDAxMilcdTAwMGLaIEHHMEzjwtD3bun4YdVe9y7dXGb8h/37L/5hsKb3g43640rRjFx1MDAwM/9xcv+1/nxSP/56015fuffPrlduP1fGgdHsxdVUgFGCY1RQXHUwMDE4T+ndOd94wuA9XHUwMDBmT5xOXHUwMDBlT/nJurTNLpLVXpTYRJ31NCMgasYph7HLvXpLROak4muIv8hcXOEyftFXvjfkMnPdXHUwMDE59jVcdTAwMTOKj7DHobtz0Vxcalxcr9HTXHUwMDAzXb/wNm+bZ8/e3LN8Y3fO0EaBVlQgw+pDL1x1MDAxOEQvVcyWR1x1MDAxYVx1MDAxZV/sPlx1MDAxYm8ojWGESP2+vVx1MDAxYj7MXHUwMDFidiqHoVe+8E7MQV2dtFdum5Xdtb+TN2RcInu/XfRcdTAwMTNcdTAwMTKdoSjuXHLTu3POXHUwMDAxZd1hXHUwMDFloMzkXHUwMDAwNVx0d8hcYjVcdTAwMTQ0+URcdTAwMGI5f1x1MDAxOW84xGF8gDfMzHdRqTPT+Fx1MDAwNlx1MDAxOLWhd/F8V74pm9dcImgllUOUNFx1MDAxYdWKoaPpX1xiqqh0tOXz1NhccnNIzjZH7922wFx1MDAxMVozSbjmWktqTErRoOSv+11cdJTHriZMbuJmP6pcdTAwMTSwWVVBTyXplc8nS32Jc8LRJNtcdEfVXVwiK2M5l7dcdTAwMTSTdIBLpYjm3e9bgTHXgubDutS3XHUwMDE2XHUwMDE0PVx1MDAwMZPC1lxc21R+MuslXHUwMDFjprlB3VVgR1MlJ1x1MDAxOD5T1mspW7W7p1x1MDAxM1pcdTAwMWQ1uFx1MDAxMH9cdTAwMWZzp6PY5PlgSlx1MDAxZs2fVYziu1Tm19/PhJvAcFx1MDAwM8fsvrCCXCI/wchnoJZTULtcdTAwMDeE5Izh8NiKz5x9Yaex0Vx1MDAxMVxiYlBcdTAwMGJgXrn+16PDcufMO79i4erezsHDt6WD2nzsczTq3lx1MDAxM2NxfVtlm8n1XHUwMDE5t7KoXHUwMDExguf07px3PKlcXDzpXHTiaVx1MDAxMlx1MDAxYlx1MDAxZFEgqFx1MDAxZDJe8vPRO1x1MDAxZI3l/H+9nY6GeItJ73SUidlcdTAwMWNcdTAwMGaImJVSk1x1MDAxMVxuXCI6wU1l47D98uOyeaafVn/slDfXZuxcdTAwMDJcdTAwMGKUWVxu4oDgXHUwMDE4fVx1MDAxYrtiU/av61x1MDAxNkY51FZZaqT5hKpY0elkfSCPfcFNXHUwMDBmsbEvOowqqi3t0yNtVjo+Ylx1MDAwMVx1MDAxOYJ6L2JcdTAwMGKvj1t2Sq+KXuogXHTtlPq1vnSHT+Nee6kojX0/8ygoXHKbrSyI9j3lIFx1MDAxZVx1MDAwYko6XHUwMDE2JmVOkZKynnyETVx1MDAwZfjT5Y/VJ7qp1lx1MDAxZlr8eV08Xla/VudcdTAwMWWRhDukXHUwMDFidFx1MDAxM40/419waVx1MDAxYjDMfoFcdTAwMTVgPIbRpCTZlVx1MDAxNkUy0NmAjH+bXrSwJPn1VGg60EqS6UxcdTAwMWVJSsW7PWjxumen1Esxldpep9W0ul557io9hrylRFx1MDAwMup7I2yW/LBTXHUwMDFhJCZxhzpdqL7/IV5RvPBGsVx1MDAxN91W6zjEcelFXHUwMDE3OOJ+7a1zI1FcdTAwMTZcdTAwMWZ873ElReWuui/batcyWFx1MDAxMHrdUOWvhb/+XHUwMDA3KtdcdTAwMDdDIn0= ByteEditor()BitSwitch(7)Label(\"7\") Switch() Switch.Changed( value=True)ByteEditor()BitSwitch(7)Label(\"7\") Switch() BitSwitch.Changed( value=True)BitSwitch.Changed( value=True)Switch.Changed( value=True)A. Switch sends Switch.Changed messageB. BitSwitch responds by sending BitSwitch.Changedto its parent"},{"location":"guide/widgets/#attributes-down","title":"Attributes down","text":"

We also want the switches to update if the user edits the decimal value.

Since the switches are children of ByteEditor we can update them by setting their attributes directly. This is an example of \"attributes down\".

byte03.pyOutput byte03.py
from __future__ import annotations\nfrom textual.app import App, ComposeResult\nfrom textual.containers import Container\nfrom textual.geometry import clamp\nfrom textual.message import Message\nfrom textual.reactive import reactive\nfrom textual.widget import Widget\nfrom textual.widgets import Input, Label, Switch\nclass BitSwitch(Widget):\n\"\"\"A Switch with a numeric label above it.\"\"\"\nDEFAULT_CSS = \"\"\"\n    BitSwitch {\n        layout: vertical;\n        width: auto;\n        height: auto;\n    }\n    BitSwitch > Label {\n        text-align: center;\n        width: 100%;\n    }\n    \"\"\"\nclass BitChanged(Message):\n\"\"\"Sent when the 'bit' changes.\"\"\"\ndef __init__(self, bit: int, value: bool) -> None:\nsuper().__init__()\nself.bit = bit\nself.value = value\nvalue = reactive(0)\ndef __init__(self, bit: int) -> None:\nself.bit = bit\nsuper().__init__()\ndef compose(self) -> ComposeResult:\nyield Label(str(self.bit))\nyield Switch()\ndef watch_value(self, value: bool) -> None:  # (1)!\n\"\"\"When the value changes we want to set the switch accordingly.\"\"\"\nself.query_one(Switch).value = value\ndef on_switch_changed(self, event: Switch.Changed) -> None:\n\"\"\"When the switch changes, notify the parent via a message.\"\"\"\nevent.stop()\nself.value = event.value\nself.post_message(self.BitChanged(self.bit, event.value))\nclass ByteInput(Widget):\n\"\"\"A compound widget with 8 switches.\"\"\"\nDEFAULT_CSS = \"\"\"\n    ByteInput {\n        width: auto;\n        height: auto;\n        border: blank;\n        layout: horizontal;\n    }\n    ByteInput:focus-within {\n        border: heavy $secondary;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nfor bit in reversed(range(8)):\nyield BitSwitch(bit)\nclass ByteEditor(Widget):\nDEFAULT_CSS = \"\"\"\n    ByteEditor > Container {\n        height: 1fr;\n        align: center middle;\n    }\n    ByteEditor > Container.top {\n        background: $boost;\n    }\n    ByteEditor Input {\n        width: 16;\n    }\n    \"\"\"\nvalue = reactive(0)\ndef validate_value(self, value: int) -> int:  # (2)!\n\"\"\"Ensure value is between 0 and 255.\"\"\"\nreturn clamp(value, 0, 255)\ndef compose(self) -> ComposeResult:\nwith Container(classes=\"top\"):\nyield Input(placeholder=\"byte\")\nwith Container():\nyield ByteInput()\ndef on_bit_switch_bit_changed(self, event: BitSwitch.BitChanged) -> None:\n\"\"\"When a switch changes, update the value.\"\"\"\nvalue = 0\nfor switch in self.query(BitSwitch):\nvalue |= switch.value << switch.bit\nself.query_one(Input).value = str(value)\ndef on_input_changed(self, event: Input.Changed) -> None:  # (3)!\n\"\"\"When the text changes, set the value of the byte.\"\"\"\ntry:\nself.value = int(event.value or \"0\")\nexcept ValueError:\npass\ndef watch_value(self, value: int) -> None:  # (4)!\n\"\"\"When self.value changes, update switches.\"\"\"\nfor switch in self.query(BitSwitch):\nwith switch.prevent(BitSwitch.BitChanged):  # (5)!\nswitch.value = bool(value & (1 << switch.bit))  # (6)!\nclass ByteInputApp(App):\ndef compose(self) -> ComposeResult:\nyield ByteEditor()\nif __name__ == \"__main__\":\napp = ByteInputApp()\napp.run()\n
  1. When the BitSwitch's value changed, we want to update the builtin Switch to match.
  2. Ensure the value is in a the range of a byte.
  3. Handle the Input.Changed event when the user modified the value in the input.
  4. When the ByteEditor value changes, update all the switches to match.
  5. Prevent the BitChanged message from being sent.
  6. Because switch is a child, we can set its attributes directly.

ByteInputApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a100\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u00a0\u00a0\u00a0\u00a07\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a06\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a05\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a04\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a03\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a02\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a00\u00a0\u00a0\u00a0\u00a0\u00a0 \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

  • When the user edits the input, the Input widget sends a Changed event, which we handle with on_input_changed by setting self.value, which is a reactive value we added to ByteEditor.
  • If the value has changed, Textual will call watch_value which sets the value of each of the eight switches. Because we are working with children of the ByteEditor, we can set attributes directly without going via a message.
"},{"location":"guide/workers/","title":"Workers","text":"

In this chapter we will explore the topic of concurrency and how to use Textual's Worker API to make it easier.

The Worker API was added in version 0.18.0

"},{"location":"guide/workers/#concurrency","title":"Concurrency","text":"

There are many interesting uses for Textual which require reading data from an internet service. When an app requests data from the network it is important that it doesn't prevent the user interface from updating. In other words, the requests should be concurrent (happen at the same time) as the UI updates.

Managing this concurrency is a tricky topic, in any language or framework. Even for experienced developers, there are gotchas which could make your app lock up or behave oddly. Textual's Worker API makes concurrency far less error prone and easier to reason about.

"},{"location":"guide/workers/#workers_1","title":"Workers","text":"

Before we go into detail, let's see an example that demonstrates a common pitfall for apps that make network requests.

The following app uses httpx to get the current weather for any given city, by making a request to wttr.in.

weather01.pyweather.tcssOutput weather01.py
import httpx\nfrom rich.text import Text\nfrom textual.app import App, ComposeResult\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Input, Static\nclass WeatherApp(App):\n\"\"\"App to display the current weather.\"\"\"\nCSS_PATH = \"weather.tcss\"\ndef compose(self) -> ComposeResult:\nyield Input(placeholder=\"Enter a City\")\nwith VerticalScroll(id=\"weather-container\"):\nyield Static(id=\"weather\")\nasync def on_input_changed(self, message: Input.Changed) -> None:\n\"\"\"Called when the input changes\"\"\"\nawait self.update_weather(message.value)\nasync def update_weather(self, city: str) -> None:\n\"\"\"Update the weather for the given city.\"\"\"\nweather_widget = self.query_one(\"#weather\", Static)\nif city:\n# Query the network API\nurl = f\"https://wttr.in/{city}\"\nasync with httpx.AsyncClient() as client:\nresponse = await client.get(url)\nweather = Text.from_ansi(response.text)\nweather_widget.update(weather)\nelse:\n# No city, so just blank out the weather\nweather_widget.update(\"\")\nif __name__ == \"__main__\":\napp = WeatherApp()\napp.run()\n
weather.tcss
Input {\ndock: top;\nwidth: 100%;\n}\n#weather-container {\nwidth: 100%;\nheight: 1fr;\nalign: center middle;\noverflow: auto;\n}\n#weather {\nwidth: auto;\nheight: auto;\n}\n

WeatherApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aEnter\u00a0a\u00a0City\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

If you were to run this app, you should see weather information update as you type. But you may find that the input is not as responsive as usual, with a noticeable delay between pressing a key and seeing it echoed in screen. This is because we are making a request to the weather API within a message handler, and the app will not be able to process other messages until the request has completed (which may be anything from a few hundred milliseconds to several seconds later).

To resolve this we can use the run_worker method which runs the update_weather coroutine (async def function) in the background. Here's the code:

weather02.py
import httpx\nfrom rich.text import Text\nfrom textual.app import App, ComposeResult\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Input, Static\nclass WeatherApp(App):\n\"\"\"App to display the current weather.\"\"\"\nCSS_PATH = \"weather.tcss\"\ndef compose(self) -> ComposeResult:\nyield Input(placeholder=\"Enter a City\")\nwith VerticalScroll(id=\"weather-container\"):\nyield Static(id=\"weather\")\nasync def on_input_changed(self, message: Input.Changed) -> None:\n\"\"\"Called when the input changes\"\"\"\nself.run_worker(self.update_weather(message.value), exclusive=True)\nasync def update_weather(self, city: str) -> None:\n\"\"\"Update the weather for the given city.\"\"\"\nweather_widget = self.query_one(\"#weather\", Static)\nif city:\n# Query the network API\nurl = f\"https://wttr.in/{city}\"\nasync with httpx.AsyncClient() as client:\nresponse = await client.get(url)\nweather = Text.from_ansi(response.text)\nweather_widget.update(weather)\nelse:\n# No city, so just blank out the weather\nweather_widget.update(\"\")\nif __name__ == \"__main__\":\napp = WeatherApp()\napp.run()\n

This one line change will make typing as responsive as you would expect from any app.

The run_worker method schedules a new worker to run update_weather, and returns a Worker object. This happens almost immediately, so it won't prevent other messages from being processed. The update_weather function is now running concurrently, and will finish a second or two later.

Tip

The Worker object has a few useful methods on it, but you can often ignore it as we did in weather02.py.

The call to run_worker also sets exclusive=True which solves an additional problem with concurrent network requests: when pulling data from the network, there is no guarantee that you will receive the responses in the same order as the requests. For instance, if you start typing \"Paris\", you may get the response for \"Pari\" after the response for \"Paris\", which could show the wrong weather information. The exclusive flag tells Textual to cancel all previous workers before starting the new one.

"},{"location":"guide/workers/#work-decorator","title":"Work decorator","text":"

An alternative to calling run_worker manually is the work decorator, which automatically generates a worker from the decorated method.

Let's use this decorator in our weather app:

weather03.py
import httpx\nfrom rich.text import Text\nfrom textual import work\nfrom textual.app import App, ComposeResult\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Input, Static\nclass WeatherApp(App):\n\"\"\"App to display the current weather.\"\"\"\nCSS_PATH = \"weather.tcss\"\ndef compose(self) -> ComposeResult:\nyield Input(placeholder=\"Enter a City\")\nwith VerticalScroll(id=\"weather-container\"):\nyield Static(id=\"weather\")\nasync def on_input_changed(self, message: Input.Changed) -> None:\n\"\"\"Called when the input changes\"\"\"\nself.update_weather(message.value)\n@work(exclusive=True)\nasync def update_weather(self, city: str) -> None:\n\"\"\"Update the weather for the given city.\"\"\"\nweather_widget = self.query_one(\"#weather\", Static)\nif city:\n# Query the network API\nurl = f\"https://wttr.in/{city}\"\nasync with httpx.AsyncClient() as client:\nresponse = await client.get(url)\nweather = Text.from_ansi(response.text)\nweather_widget.update(weather)\nelse:\n# No city, so just blank out the weather\nweather_widget.update(\"\")\nif __name__ == \"__main__\":\napp = WeatherApp()\napp.run()\n

The addition of @work(exclusive=True) converts the update_weather coroutine into a regular function which when called will create and start a worker. Note that even though update_weather is an async def function, the decorator means that we don't need to use the await keyword when calling it.

Tip

The decorator takes the same arguments as run_worker.

"},{"location":"guide/workers/#worker-return-values","title":"Worker return values","text":"

When you run a worker, the return value of the function won't be available until the work has completed. You can check the return value of a worker with the worker.result attribute which will initially be None, but will be replaced with the return value of the function when it completes.

If you need the return value you can call worker.wait which is a coroutine that will wait for the work to complete. But note that if you do this in a message handler it will also prevent the widget from updating until the worker returns. Often a better approach is to handle worker events which will notify your app when a worker completes, and the return value is available without waiting.

"},{"location":"guide/workers/#cancelling-workers","title":"Cancelling workers","text":"

You can cancel a worker at any time before it is finished by calling Worker.cancel. This will raise a CancelledError within the coroutine, and should cause it to exit prematurely.

"},{"location":"guide/workers/#worker-errors","title":"Worker errors","text":"

The default behavior when a worker encounters an exception is to exit the app and display the traceback in the terminal. You can also create workers which will not immediately exit on exception, by setting exit_on_error=False on the call to run_worker or the @work decorator.

"},{"location":"guide/workers/#worker-lifetime","title":"Worker lifetime","text":"

Workers are managed by a single WorkerManager instance, which you can access via app.workers. This is a container-like object which you iterate over to see your active workers.

Workers are tied to the DOM node (widget, screen, or app) where they are created. This means that if you remove the widget or pop the screen where they are created, then the tasks will be cleaned up automatically. Similarly if you exit the app, any running tasks will be cancelled.

Worker objects have a state attribute which will contain a WorkerState enumeration that indicates what the worker is doing at any given time. The state attribute will contain one of the following values:

Value Description PENDING The worker was created, but not yet started. RUNNING The worker is currently running. CANCELLED The worker was cancelled and is no longer running. ERROR The worker raised an exception, and worker.error will contain the exception. SUCCESS The worker completed successful, and worker.result will contain the return value.

Workers start with a PENDING state, then go to RUNNING. From there, they will go to CANCELLED, ERROR or SUCCESS.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nOVbaVPbSlx1MDAxNv2eX8EwX2aqQqf3JVVTU2BcdTAwMWN2YzDLkFevUsKWjWLZciSZ7VX++7uSwdq8gnFMxkmxqFvS7dv33HNud/PXh7W19fChZ69/Xlu37+uW6zR86279Y3T91vZcdTAwMDPH60JcdTAwMTONf1x1MDAwZry+X4973oRhL/j86VPH8tt22HOtuo1unaBvuUHYbzhcdTAwMWWqe51PTmh3gv9GXytWx/5Pz+s0Qlx1MDAxZiUv2bBcdTAwMWJO6PmDd9mu3bG7YVx1MDAwME//XHUwMDAzfl9b+yv+mrLOt+uh1W25dnxD3JRcdTAwMTjIhM5frXjd2FiKXHUwMDE10YRiTIY9nGBcdTAwMWLeXHUwMDE32lxyaG6CzXbSXHUwMDEyXVq3znrXZLtd3dpUXHUwMDBm21xyfSrtRilIXtt0XFy3XHUwMDE2PrhcdTAwMDNXWPWbvp8yKlxifa9tXzqN8Fx1MDAwNtpJ7vrwvoZcdTAwMTXcgFx1MDAwMcNm3+u3brp2XHUwMDEwZG7yelbdXHRcdTAwMWbgmsTDi1x1MDAwMy98Xkuu3MNvgjDEXHUwMDE1JpozKolcImbYXHUwMDFh3U6MRsxAo1CYXHUwMDExKnJmlTxcdTAwMTdmXHUwMDAyzPonjj+JXddWvd1cdTAwMDLjuo2kXHUwMDBmXHUwMDExlnXdVCrpdfc8XFylkZGaYENcdTAwMTjWwrBkWm5sp3VcdTAwMTNCXHUwMDFmRZDWXFxJIdNm2PFsXHUwMDEww6lcdTAwMTBaXHUwMDEwOWyJXt7ba8SR8WfaXd3Gk7ueQyVcdFx1MDAxNvZ05WcyjKh/OVx1MDAxZmTpQMtcdTAwMDRbaN+Hw9GlXCKDXHUwMDFlYm653vbRySU+7SmveVx1MDAxZPh368N+Pz+Ofuzg5tbX3bPbg4rg2K0+bjaCutNcdTAwMGV19i3P77d830s/9+mnZPz9XsNcdTAwMWFcdTAwMDQwkVx1MDAxYVx1MDAwYmaEMJypYbvrdNvQ2O27bnLNq7eTmP+QMrhcdTAwMDC2zPjTiYCbcTgjlElCXHKZXHUwMDFkZqOduViYXHUwMDA1XHUwMDFlJJtFokxwxFx1MDAwNadMa0MkTsVpdDulXHUwMDE0acYphVxizob3/ChcdTAwMGJ9q1x1MDAxYvQsXHUwMDFmXHUwMDAytogzo4q4oiyPJsNcZotiY1x1MDAxZTBlXCKmgJpFXHUwMDA2YDLRXjesOY+DZJ25+sXqOO5DZq7iyFx1MDAwNPdUy5XtvcpO2oWBXHIvjUNRZbpvuk4rXG7e9TpcZsP2M3FcdTAwMWQ6QEfDXHUwMDBlXHUwMDFkp9FIXHUwMDEzTFx1MDAxZGyw4Jn+3iy84PlOy+la7lnOxIlIm0hrVLPxcMOGXG6qUi6fhrcq/966XGKCbvussnlMSuLhuH9cdTAwMTj+WryZ6aymXHUwMDExQFx04lx1MDAwYuBmXGalXHUwMDE5vHFsXHUwMDEwIJFTpVx1MDAxOFx1MDAwNN/rWFxyUHttyzdhNcKYoVLNXHUwMDA1xCWy2rfqZVmwb3T/ZrfU8qonrTPaO1pcdTAwMDKrTXxuXHUwMDE4XHUwMDFjXHUwMDE4uSVlu3K811xid75x9ztcclx1MDAxN/Dcsz0lws7Z8Vx1MDAxN1BBzdOr+uFV6bv1vliYXHUwMDE4OjYtQHxqRjgzM6eF0bO/4mlBYkQ4XHUwMDA2b3NNOc2JXc5cdTAwMThcdTAwMTJcdTAwMWFcdTAwMDNgXHUwMDA1l/p1aWEyXHKLYlwiKNKwXHUwMDAyNc5cck9N2u9Dw6fnlcqyaXhcbo/lafjZxJfTME/JrVx1MDAxY95cdTAwMDRcdTAwMTPGXGLK2Mxw278qNbulSvXm6urkXFxaTJ5WW1x1MDAwZr9cdTAwMTZuXHUwMDA0T8NcdTAwMWI1XG5cdTAwMTjQ4NE0XGakh0BcZnNJoHpTUGy/ioeb1jXGYvEsrOHNUFx1MDAxZq8qXHTLi3ur6e5cXDx6m1++063+5Vx1MDAxMS6fLoEsV4XUOFx1MDAxZLuEXHUwMDAzYWUwwXNw2mhnrjjIXHUwMDE4VVBbMjya1GAukMRU86j4pK9cdTAwMDTZRFYjqfQ/gdaI0Vx1MDAwNv5cdTAwMTP2O/JaabNSKlx1MDAxZlx1MDAxZZa3l8psU7ghz2yJkS/nNkHEONhRoYWSxMzObf2gVz06bzfZ0ebBl1p5V5VZzVl12Fx1MDAwMXsjQoFcdTAwMWSE0YRcdTAwMDNcdTAwMDBz3CZcdTAwMDCUUlGJXHUwMDE1YFLr18BcdTAwMGW4TURF+1vUmIxSRoRKXHJ3pehtyznY3WnKq9ZuhXnNvc6J7LR+zEpvj03F5f79t83H8I5cXO79aFx1MDAwNY1cdTAwMGLzzuiN87FcdTAwMWFSSC4wobOv5Ix25qrjTClEgFtkviqLYaZcZpLcQPmqtVSvhNlEdlOkXGKsUeRmKMSHTs3a70Nu5dPT49OlXHUwMDEy21x1MDAxNGLIXHUwMDEz28DAl5Oa5HJcdTAwMWPYiIZMbrjWc+xT6KteKdxix51wrytkTe5vXf7iXHUwMDA1kuloU1x1MDAwMiNKXHUwMDE0ZYYpo3VKXFxcdTAwMGZYTSHMXHUwMDE0V9Fmm3ntyinHdSzeZjfQRHeLXHUwMDE1Ldno0f1t7+7r8WG5XSv/j11cXJrKJf8/WodcdTAwMTSpdbpcdTAwMDLMXGKIJVx1MDAwZVbMXHUwMDBls5HeXFx1mFx1MDAxOYZcdTAwMTRwt2FcdTAwMDaUY3pjfVCzXHUwMDExRKmJlmSNfu1cdTAwMDbF5JVIWoTWXGJWXHUwMDAzuGtcIomeRyS+XHUwMDE3Vqudl0rlWm2pvDaFXHUwMDFh8rz2bOJEtFxy0D5cdTAwMDJuTI9dIYlyK+irOUq1yds2v1x1MDAwNG16XHUwMDFh2CRmQFx1MDAxNlx1MDAxMMRcbpiBM5JcdTAwMDVcdTAwMWKUcIjA1Vx1MDAxMXSy2EpcckF0S0O0UlxmftIjkFx1MDAwNypcdTAwMTeIj0FCgFxmSDiWuoBEbahkOj2lL6E2+lbU5tbUj6ZcdTAwMWSow2qtVf8udv3b6ld/PlxugrQok3icXHUwMDAz/UFo+eGW02043VZ21E+nxGbZjY/zRb1cdTAwMWa5YFx1MDAwMyNcZlLEKFx1MDAxZS1RR3pcIknEkd+tXpRAkZagilx1MDAxNOFxoi741e42pps0eWdcImdcdTAwMTKEXHUwMDEwN8JoZVx1MDAwNFCE5Fx1MDAwNZM0opoorJRkUUlEdMEo11xuwpLX6TghuL7qOd0w7+LYl5tRRrmxrUa+XHUwMDE1XHUwMDA2lW7Lp55e9MRsXHUwMDEw/ZFcblx1MDAxNZyOXHUwMDFiPPz5z48je2+MxU30KVwiJnneh/T3eTWKXHUwMDFjL1EkOJ5cdTAwMDPIZ19WXHUwMDFljYrVTprgUmSUVlB6Q+LkJLt1w6hBhEJcdTAwMWNySplk9M2Wt/hsXHUwMDAyRVx1MDAxMYh3upyiW2GskvG+tTy59Py27aNcdTAwMTiQ//r3UlXKXHUwMDE0rs+rlJylL1x1MDAxMytqwnaOwVQxSMazXHUwMDAzb/L+1orWXHUwMDA2QktcdTAwMDS9KDGKZlx1MDAwNUl8SEFcdTAwMWLEhVEkOlx1MDAxN/TKQ1xuXHUwMDEzgEdcdEZSYoVcdTAwMTnoJUzFiNUvXCI5YoRcdTAwMDEzsvyxxSdUQtbGMFx1MDAwMjLPTs9cdTAwMTLFyr7TOvH6cqu822w/XFw2N1x1MDAxZZ3z8423rZfnXHUwMDE1K/MoXHUwMDAzwVxyg6mimkPZyLRK9XpcdTAwMTZcdTAwMDZES0HZy4XK5I2mrDk0OlJcdTAwMDRpXHUwMDE5XmhcdTAwMDCzpGBcdTAwMGVFYKRkw1N471xcplx1MDAwMC5ZdFTAsKfD8un7pVx1MDAwMFFmXHUwMDE0Y4Nl6uFgxz9vLFx1MDAwMKNPXHUwMDAxelx1MDAwYlI9avwxbcJcdTAwMTVhkHJm321cdTAwMThccq9cdTAwMTVPvoyKaFx1MDAxYUFKgtjUXHUwMDAw70zyXHUwMDE1nCFQPVxcgNBkmYXJRcueROJM3G3gWlExVzH4XpZl7lx1MDAwNmqibkH8uUtcdTAwMTY+U2RDXvjkTX2Z8oFcdTAwMDJzbM0ho9NcdTAwMWRcdTAwMDC/2cE3eZl4NU9nSmZcdTAwMDBdRFx1MDAxMKi2WeaoSix8XGaBXHUwMDFjXG7131Nd+2bSh1x1MDAxOIOUXHUwMDE22DBcdTAwMDI1XHUwMDA1o4pcdTAwMTexSCTUnURjXG4l0lx1MDAxOPHDuVx1MDAwMv7jYkXFz87Rdui2+4dcdTAwMWLbvavHg1x1MDAwNt1cdTAwMGZvN3bfqfjBXGLIScnon+QwJSbVZ6A1SLSWQyeJjZn0z+R124xFQDWQnomKTlVcdTAwMGLNaVH/cKRcdTAwMTmTRFx1MDAwM5njhMLfp/whSiCjwfWEXHUwMDE4XHUwMDEyXHUwMDBmJ6N+XGZcIuB6XCJcdTAwMDfbedPVz3hcYsatXHUwMDA18C1I/tDxxadcdTAwMDAsQ1Jis2//jlx1MDAwNthqZ2ClXHUwMDE1UpSDqmC4eNhCcI1cZsWYXHUwMDE3Nq1cdTAwMTabgMVMx+OBXHUwMDA0XGbEXHUwMDEz/lx1MDAxZE9abHtd+1x1MDAxZkuVPFP0Ql7yXGZcZnyZ0GF6rM5cdTAwMTGR8Dacz46yyUe8Vlx1MDAxM2VcdTAwMTLYSGGsoWRcdTAwMDRIXHSVXHUwMDA1XHUwMDE5jir55JBcdTAwMDV/I5BcdTAwMTFgKEJcdTAwMTWWUF1CYoM0O0LlXGKo0bnCQGD5fehcdTAwMDFcdTAwMDbh2VBcdTAwMDeJV56NfzONU71t7Vx1MDAxY7Q23E1v1z5o3oqT+0M9527UojROnpmna43JZ5/WsptCoJaJplx1MDAxNGzlXHUwMDAyXHUwMDE0NC/uXG5RJGT0Z708PqKm3/1yy9jwjT5CI1x1MDAwNlU8V0+nb2dYb1x1MDAwMUhcbqKMUVx1MDAxOL5CVDOVfmBcdTAwMDFcdFx1MDAwYlJcdTAwMWNi/DZcdTAwMTOL8iCAa/ZcXDg63Fc7XHUwMDE3gnpD0Z9CcfB45NnseTPBXHUwMDE1YuD1p02mV/+F0LhkaEaUeEXFQZXBkkv6Oy63lO/rdi+MgnKZqmNcbntcdTAwMTfOd1x1MDAwZY1cdTAwMWOA7cNcdTAwMTOc161er1x1MDAxNoL/hulcdTAwMTRmxmk8OSHx2fqtY99tjYqM+Fx1MDAxMz01XHUwMDA2cFx1MDAwNFx1MDAxNTvmqp9cdTAwMWZ+/lxycIktXGYifQ== PENDINGRUNNINGCANCELLEDERRORSUCCESSWorker.start()worker.cancel()Done!Exception"},{"location":"guide/workers/#worker-events","title":"Worker events","text":"

When a worker changes state, it sends a Worker.StateChanged event to the widget where the worker was created. You can handle this message by defining an on_worker_state_changed event handler. For instance, here is how we might log the state of the worker that updates the weather:

weather04.py
import httpx\nfrom rich.text import Text\nfrom textual import work\nfrom textual.app import App, ComposeResult\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Input, Static\nfrom textual.worker import Worker\nclass WeatherApp(App):\n\"\"\"App to display the current weather.\"\"\"\nCSS_PATH = \"weather.tcss\"\ndef compose(self) -> ComposeResult:\nyield Input(placeholder=\"Enter a City\")\nwith VerticalScroll(id=\"weather-container\"):\nyield Static(id=\"weather\")\nasync def on_input_changed(self, message: Input.Changed) -> None:\n\"\"\"Called when the input changes\"\"\"\nself.update_weather(message.value)\n@work(exclusive=True)\nasync def update_weather(self, city: str) -> None:\n\"\"\"Update the weather for the given city.\"\"\"\nweather_widget = self.query_one(\"#weather\", Static)\nif city:\n# Query the network API\nurl = f\"https://wttr.in/{city}\"\nasync with httpx.AsyncClient() as client:\nresponse = await client.get(url)\nweather = Text.from_ansi(response.text)\nweather_widget.update(weather)\nelse:\n# No city, so just blank out the weather\nweather_widget.update(\"\")\ndef on_worker_state_changed(self, event: Worker.StateChanged) -> None:\n\"\"\"Called when the worker state changes.\"\"\"\nself.log(event)\nif __name__ == \"__main__\":\napp = WeatherApp()\napp.run()\n

If you run the above code with textual you should see the worker lifetime events logged in the Textual console.

textual run weather04.py --dev\n
"},{"location":"guide/workers/#thread-workers","title":"Thread workers","text":"

In previous examples we used run_worker or the work decorator in conjunction with coroutines. This works well if you are using an async API like httpx, but if your API doesn't support async you may need to use threads.

What are threads?

Threads are a form of concurrency supplied by your Operating System. Threads allow your code to run more than a single function simultaneously.

You can create threads by setting thread=True on the run_worker method or the work decorator. The API for thread workers is identical to async workers, but there are a few differences you need to be aware of when writing code for thread workers.

The first difference is that you should avoid calling methods on your UI directly, or setting reactive variables. You can work around this with the App.call_from_thread method which schedules a call in the main thread.

The second difference is that you can't cancel threads in the same way as coroutines, but you can manually check if the worker was cancelled.

Let's demonstrate thread workers by replacing httpx with urllib.request (in the standard library). The urllib module is not async aware, so we will need to use threads:

weather05.py
from urllib.request import Request, urlopen\nfrom rich.text import Text\nfrom textual import work\nfrom textual.app import App, ComposeResult\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Input, Static\nfrom textual.worker import Worker, get_current_worker\nclass WeatherApp(App):\n\"\"\"App to display the current weather.\"\"\"\nCSS_PATH = \"weather.tcss\"\ndef compose(self) -> ComposeResult:\nyield Input(placeholder=\"Enter a City\")\nwith VerticalScroll(id=\"weather-container\"):\nyield Static(id=\"weather\")\nasync def on_input_changed(self, message: Input.Changed) -> None:\n\"\"\"Called when the input changes\"\"\"\nself.update_weather(message.value)\n@work(exclusive=True, thread=True)\ndef update_weather(self, city: str) -> None:\n\"\"\"Update the weather for the given city.\"\"\"\nweather_widget = self.query_one(\"#weather\", Static)\nworker = get_current_worker()\nif city:\n# Query the network API\nurl = f\"https://wttr.in/{city}\"\nrequest = Request(url)\nrequest.add_header(\"User-agent\", \"CURL\")\nresponse_text = urlopen(request).read().decode(\"utf-8\")\nweather = Text.from_ansi(response_text)\nif not worker.is_cancelled:\nself.call_from_thread(weather_widget.update, weather)\nelse:\n# No city, so just blank out the weather\nif not worker.is_cancelled:\nself.call_from_thread(weather_widget.update, \"\")\ndef on_worker_state_changed(self, event: Worker.StateChanged) -> None:\n\"\"\"Called when the worker state changes.\"\"\"\nself.log(event)\nif __name__ == \"__main__\":\napp = WeatherApp()\napp.run()\n

In this example, the update_weather is not asynchronous (i.e. a regular function). The @work decorator has thread=True which makes it a thread worker. Note the use of get_current_worker which the function uses to check if it has been cancelled or not.

Important

Textual will raise an exception if you add the work decorator to a regular function without thread=True.

"},{"location":"guide/workers/#posting-messages","title":"Posting messages","text":"

Most Textual functions are not thread-safe which means you will need to use call_from_thread to run them from a thread worker. An exception would be post_message which is thread-safe. If your worker needs to make multiple updates to the UI, it is a good idea to send custom messages and let the message handler update the state of the UI.

"},{"location":"how-to/","title":"How To","text":"

Welcome to the How To section.

Here you will find How To articles which cover various topics at a higher level than the Guide or Reference. We will be adding more articles in the future. If there is anything you would like to see covered, open an issue in the Textual repository!

"},{"location":"how-to/center-things/","title":"Center things","text":"

If you have ever needed to center something in a web page, you will be glad to know it is much easier in Textual.

This article discusses a few different ways in which things can be centered, and the differences between them.

"},{"location":"how-to/center-things/#aligning-widgets","title":"Aligning widgets","text":"

The align rule will center a widget relative to one or both edges. This rule is applied to a container, and will impact how the container's children are arranged. Let's see this in practice with a trivial app containing a Static widget:

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass CenterApp(App):\n\"\"\"How to center things.\"\"\"\nCSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Static(\"Hello, World!\")\nif __name__ == \"__main__\":\napp = CenterApp()\napp.run()\n

Here's the output:

CenterApp Hello,\u00a0World!

The container of the widget is the screen, which has the align: center middle; rule applied. The center part tells Textual to align in the horizontal direction, and middle tells Textual to align in the vertical direction.

The output may surprise you. The text appears to be aligned in the middle (i.e. vertical edge), but left aligned on the horizontal. This isn't a bug \u2014 I promise. Let's make a small change to reveal what is happening here. In the next example, we will add a background and a border to our text:

Tip

Adding a border is a very good way of visualizing layout issues, if something isn't behaving as you would expect.

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass CenterApp(App):\n\"\"\"How to center things.\"\"\"\nCSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    #hello {\n        background: blue 50%;\n        border: wide white;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Static(\"Hello, World!\", id=\"hello\")\nif __name__ == \"__main__\":\napp = CenterApp()\napp.run()\n

The static widget will now have a blue background and white border:

CenterApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eHello,\u00a0World!\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

Note the static widget is as wide as the screen. Since the widget is as wide as its container, there is no room for it to move in the horizontal direction.

Info

The align rule applies to widgets, not the text.

In order to see the center alignment, we will have to make the widget smaller than the width of the screen. Let's set the width of the Static widget to auto, which will make the widget just wide enough to fit the content:

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass CenterApp(App):\n\"\"\"How to center things.\"\"\"\nCSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    #hello {\n        background: blue 50%;\n        border: wide white;\n        width: auto;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Static(\"Hello, World!\", id=\"hello\")\nif __name__ == \"__main__\":\napp = CenterApp()\napp.run()\n

If you run this now, you should see the widget is aligned on both axis:

CenterApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eHello,\u00a0World!\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

"},{"location":"how-to/center-things/#aligning-text","title":"Aligning text","text":"

In addition to aligning widgets, you may also want to align text. In order to demonstrate the difference, lets update the example with some longer text. We will also set the width of the widget to something smaller, to force the text to wrap.

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nQUOTE = \"Could not find you in Seattle and no terminal is in operation at your classified address.\"\nclass CenterApp(App):\n\"\"\"How to center things.\"\"\"\nCSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    #hello {\n        background: blue 50%;\n        border: wide white;\n        width: 40;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Static(QUOTE, id=\"hello\")\nif __name__ == \"__main__\":\napp = CenterApp()\napp.run()\n

Here's what it looks like with longer text:

CenterApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eCould\u00a0not\u00a0find\u00a0you\u00a0in\u00a0Seattle\u00a0and\u00a0no\u00a0\u258a \u258eterminal\u00a0is\u00a0in\u00a0operation\u00a0at\u00a0your\u00a0\u258a \u258eclassified\u00a0address.\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

Note how the widget is centered, but the text within it is flushed to the left edge. Left aligned text is the default, but you can also center the text with the text-align rule. Let's center align the longer text by setting this rule:

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nQUOTE = \"Could not find you in Seattle and no terminal is in operation at your classified address.\"\nclass CenterApp(App):\n\"\"\"How to center things.\"\"\"\nCSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    #hello {\n        background: blue 50%;\n        border: wide white;\n        width: 40;\n        text-align: center;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Static(QUOTE, id=\"hello\")\nif __name__ == \"__main__\":\napp = CenterApp()\napp.run()\n

If you run this, you will see that each line of text is individually centered:

CenterApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u00a0Could\u00a0not\u00a0find\u00a0you\u00a0in\u00a0Seattle\u00a0and\u00a0no\u00a0\u258a \u258e\u00a0\u00a0\u00a0terminal\u00a0is\u00a0in\u00a0operation\u00a0at\u00a0your\u00a0\u00a0\u00a0\u258a \u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0classified\u00a0address.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

You can also use text-align to right align text or justify the text (align to both edges).

"},{"location":"how-to/center-things/#aligning-content","title":"Aligning content","text":"

There is one last rule that can help us center things. The content-align rule aligns content within a widget. It treats the text as a rectangular region and positions it relative to the space inside a widget's border.

In order to see why we might need this rule, we need to make the Static widget larger than required to fit the text. Let's set the height of the Static widget to 9 to give the content room to move:

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nQUOTE = \"Could not find you in Seattle and no terminal is in operation at your classified address.\"\nclass CenterApp(App):\n\"\"\"How to center things.\"\"\"\nCSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    #hello {\n        background: blue 50%;\n        border: wide white;\n        width: 40;\n        height: 9;\n        text-align: center;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Static(QUOTE, id=\"hello\")\nif __name__ == \"__main__\":\napp = CenterApp()\napp.run()\n

Here's what it looks like with the larger widget:

CenterApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u00a0Could\u00a0not\u00a0find\u00a0you\u00a0in\u00a0Seattle\u00a0and\u00a0no\u00a0\u258a \u258e\u00a0\u00a0\u00a0terminal\u00a0is\u00a0in\u00a0operation\u00a0at\u00a0your\u00a0\u00a0\u00a0\u258a \u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0classified\u00a0address.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258e\u258a \u258e\u258a \u258e\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

Textual aligns a widget's content to the top border by default, which is why the space is below the text. We can tell Textual to align the content to the center by setting content-align: center middle;

Note

Strictly speaking, we only need to align the content vertically here (there is no room to move the content left or right) So we could have done content-align-vertical: middle;

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nQUOTE = \"Could not find you in Seattle and no terminal is in operation at your classified address.\"\nclass CenterApp(App):\n\"\"\"How to center things.\"\"\"\nCSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    #hello {\n        background: blue 50%;\n        border: wide white;\n        width: 40;\n        height: 9;\n        text-align: center;\n        content-align: center middle;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Static(QUOTE, id=\"hello\")\nif __name__ == \"__main__\":\napp = CenterApp()\napp.run()\n

If you run this now, the content will be centered within the widget:

CenterApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258e\u258a \u258e\u00a0Could\u00a0not\u00a0find\u00a0you\u00a0in\u00a0Seattle\u00a0and\u00a0no\u00a0\u258a \u258e\u00a0\u00a0\u00a0terminal\u00a0is\u00a0in\u00a0operation\u00a0at\u00a0your\u00a0\u00a0\u00a0\u258a \u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0classified\u00a0address.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258e\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

"},{"location":"how-to/center-things/#aligning-multiple-widgets","title":"Aligning multiple widgets","text":"

It's just as easy to align multiple widgets as it is a single widget. Applying align: center middle; to the parent widget (screen or other container) will align all its children.

Let's create an example with two widgets. The following code adds two widgets with auto dimensions:

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass CenterApp(App):\n\"\"\"How to center things.\"\"\"\nCSS = \"\"\"\n    .words {\n        background: blue 50%;\n        border: wide white;\n        width: auto;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Static(\"How about a nice game\", classes=\"words\")\nyield Static(\"of chess?\", classes=\"words\")\nif __name__ == \"__main__\":\napp = CenterApp()\napp.run()\n

This produces the following output:

CenterApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eHow\u00a0about\u00a0a\u00a0nice\u00a0game\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eof\u00a0chess?\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

We can center both those widgets by applying the align rule as before:

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass CenterApp(App):\n\"\"\"How to center things.\"\"\"\nCSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    .words {\n        background: blue 50%;\n        border: wide white;\n        width: auto;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Static(\"How about a nice game\", classes=\"words\")\nyield Static(\"of chess?\", classes=\"words\")\nif __name__ == \"__main__\":\napp = CenterApp()\napp.run()\n

Here's the output:

CenterApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eHow\u00a0about\u00a0a\u00a0nice\u00a0game\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eof\u00a0chess?\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

Note how the widgets are aligned as if they are a single group. In other words, their position relative to each other didn't change, just their position relative to the screen.

If you do want to center each widget independently, you can place each widget inside its own container, and set align for those containers. Textual has a builtin Center container for just this purpose.

Let's wrap our two widgets in a Center container:

from textual.app import App, ComposeResult\nfrom textual.containers import Center\nfrom textual.widgets import Static\nclass CenterApp(App):\n\"\"\"How to center things.\"\"\"\nCSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    .words {\n        background: blue 50%;\n        border: wide white;\n        width: auto;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nwith Center():\nyield Static(\"How about a nice game\", classes=\"words\")\nwith Center():\nyield Static(\"of chess?\", classes=\"words\")\nif __name__ == \"__main__\":\napp = CenterApp()\napp.run()\n

If you run this, you will see that the widgets are centered relative to each other, not just the screen:

CenterApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eHow\u00a0about\u00a0a\u00a0nice\u00a0game\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eof\u00a0chess?\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

"},{"location":"how-to/center-things/#summary","title":"Summary","text":"

Keep the following in mind when you want to center content in Textual:

  • In order to center a widget, it needs to be smaller than its container.
  • The align rule is applied to the parent of the widget you want to center (i.e. the widget's container).
  • The text-align rule aligns text on a line by line basis.
  • The content-align rule aligns content within a widget.
  • Use the Center container if you want to align multiple widgets relative to each other.
  • Add a border if the alignment isn't working as you would expect.

If you need further help, we are here to help.

"},{"location":"how-to/design-a-layout/","title":"Design a Layout","text":"

This article discusses an approach you can take when designing the layout for your applications.

Textual's layout system is flexible enough to accommodate just about any application design you could conceive of, but it may be hard to know where to start. We will go through a few tips which will help you get over the initial hurdle of designing an application layout.

"},{"location":"how-to/design-a-layout/#tip-1-make-a-sketch","title":"Tip 1. Make a sketch","text":"

The initial design of your application is best done with a sketch. You could use a drawing package such as Excalidraw for your sketch, but pen and paper is equally as good.

Start by drawing a rectangle to represent a blank terminal, then draw a rectangle for each element in your application. Annotate each of the rectangles with the content they will contain, and note wether they will scroll (and in what direction).

For the purposes of this article we are going to design a layout for a Twitter or Mastodon client, which will have a header / footer and a number of columns.

Note

The approach we are discussing here is applicable even if the app you want to build looks nothing like our sketch!

Here's our sketch:

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1daW9cdTAwMWK7kv1+f0WQ92VcdTAwMDa46ktcdTAwMTbJXCJ5gcHAi1x1MDAxY1mOl8iKt8GDoaUtyda+eHvIf5+ivKjVUsstW7JbuTaQxGktzWafOudcdTAwMTRZTf7njy9fvvbv2v7Xv7989W9LhXqt3C3cfP3TXHUwMDFkv/a7vVqrSS/B8P+91qBbXHUwMDFhvrPa77d7f//1V6PQvfL77Xqh5HvXtd6gUO/1XHUwMDA35VrLK7VcdTAwMWF/1fp+o/e/7u+9QsP/n3arUe53vdFJUn651m91XHUwMDFmzuXX/Ybf7Pfo2/+P/v/ly3+Gf1x1MDAwN1rX9Uv9QrNS94dcdTAwMWZcdTAwMTi+NGogZ0aFXHUwMDBm77Waw9ZyLoxGxVxyPL+j1tukXHUwMDEz9v0yvXxBjfZHr7hDX1x1MDAwZnL9dT+3q/ay56WbXCK2djHdXHUwMDFkjM57UavXXHUwMDBm+3f1YbtK3Vavl6pcdTAwMTb6peroXHUwMDFkvX63deVcdTAwMWbXyv3qU/dcdTAwMDWOP3+216KuXHUwMDE4farbXHUwMDFhVKpNv+d6gT9cdTAwMWZttVx1MDAwYqVa/254lez56ENX/P1ldOSW/pdC4Vx0LY1VqLW1KtAr7lx1MDAwYkAoXHUwMDBmhFx1MDAxNKhcdTAwMDVarahfQi3baNXpjlDL/sWGP6O2XHUwMDE1XHUwMDBipatcbjWwWX5+T79baPbahS7dt9H7blx1MDAxZa9ZSemhcCdjinM+akfVr1WqfXqHQOZcdTAwMTnOrJQohLs9OGqMP7w1XHUwMDFjjNCopVx1MDAxNs+vuCa0t8tDnPw72HHN8mPHNVx1MDAwN/X6qNXuhXRcdTAwMDBbo89cZtrlwlx1MDAwM1x1MDAwMjhcdTAwMWFcdIpJ1Fxio1x1MDAwZanXmlfhr6u3Slcj0Fxmj/7681x1MDAxNXDVKlx1MDAxYa1WcVx1MDAwNIlcdTAwMThcdTAwMWKsuVLn5367sL63s3+Lza2fXHUwMDA3nZ3tTlx1MDAwNFhDgPtImFx1MDAxYcGVtIpxJjhBIIRT6Vx1MDAxOaUlgcJcdTAwMDJKi8vEqfBcdTAwMThHXHUwMDA1dD6urTaTQFx1MDAwNeMhMiFccjJAQaCeXHUwMDAwKt0zJYGuZvVw6tfrtXZvKkpxXHUwMDA2p1x1MDAxYZCcI1cmNkzbP86OS+lstssvc9f5+3TqrtG8fVxyTPn7wVRxz1xuhdTxQljOQIbolDjOMCMl11x1MDAwZcqgw1x1MDAxMTRcdTAwMTdM/3VRUKBgXHUwMDEyolxceJJcdTAwMDEoi0D/SGPkJEY5eFxuKVKISFx1MDAxOVxiXHUwMDAxUocxyql9ilNcdTAwMWL5b1x1MDAwNVItbFx1MDAxNEi1QXfNXHUwMDEwX/dP1jrpu8ZB7qC4tnXT2upvna3jTcIxXG7KY9TdyLnhlsBcdTAwMDEhiFxuT1x1MDAxObRkXG4kUZhV8o1cdTAwMTAtMqaWXHUwMDA1Uc1cdTAwMTXpgeC/XHUwMDE5Qq2IQijnjJSFS81iQ7TeSbdTV1x1MDAxNexs1upXWLrrb53UXHUwMDBlk1xyUVx1MDAxMljgXHUwMDAyQWjBmWRWh1x1MDAxMFxujmQl3Vx1MDAxNVxyxLVv41BcdTAwMGVFY3BZXHUwMDAwpcsgqkcuV1DoZ+dPKlx1MDAxMqPu1iihXHUwMDAzb3hcdKLr37ZcbrsldbLV2V1vnt3+OL6pXHUwMDFjNJJcctFcdTAwMTTlXCKk4lZcdTAwMTFFcnBJyThGXHUwMDExXHQ+hFxmTTTKJcOlYFRJ5oym4Vx1MDAxNCnaQiBOXHUwMDAyOZMnNaekytphO0CFIUquTJGLNYrNXHUwMDAx0SewjOBcIlx1MDAxZY/8ikbu82dGn1x1MDAwZcCt79+O3HZcdTAwMDBcdTAwMWPd9mG1cbB9tj/YK+VzlbXWgVx1MDAxOdx9fX7fr8ffosJCXGImjTLWLiosxtpcdTAwMTmMXGJcdTAwMTNcdTAwMTVcdTAwMTCczs5dvqBiR8T0i55cdTAwMWVcdTAwMTHVQqk66PpcdTAwMWZcdTAwMWZcdTAwMTNcXCO5W+JKQIK+YTLE21x1MDAxYTxcdTAwMDdB4nNcdTAwMTlcbpnXXHUwMDA0XHUwMDA1xVx1MDAxZbuYYn4p8oBcdTAwMTPaXHUwMDE5p1x1MDAwZSeRmJafTVx1MDAwNoFcdTAwMDWuyZ3PXHUwMDE1XHUwMDA0r+Pp11x1MDAwMHJ041vN/mHt3vU9sLGjW4VGrX43du+GSKWuyviFst/9OnZ8rV6rONR+LVF7gy9cdTAwMTJ2+7VSof78hkatXFxcdTAwMGVqQIlOVqg1/e52XHUwMDFj7m51a5Vas1DPR7SFrt3PPOupXHUwMDE3uDPFQs93r1x1MDAwZVx1MDAwM+j1Qlx1MDAwNVx1MDAxOJmTkns0QltcdTAwMTN/5GRtc6O+M2Bddn/XuK/YfL5/lc68LizfcexEXHUwMDE5j5NRkoxsfUhcdTAwMDPcN1xihp7ipGQ8XHUwMDE4XHUwMDE3XHUwMDBiXHUwMDFlNeGKuEFScvEkiKPrXHUwMDFmhSX5KbCGXHUwMDFisnXKqCA/PLkp0IZy6sDgz1x1MDAxMt2UJt5cdTAwMGVkP0t1U1x1MDAwMapcZouHM/zW5fmxUdqpbbT2sr3NXGZ2N1x1MDAwZTK57av9evpV43vvOXBcIjy6t+hcdTAwMTJwMis6hFBuKCdcdTAwMTX0gmIhYLzGTalCoXgh5TTPzz1GSIUpg89Selx1MDAxMjSzj01cYjbxXHUwMDExnUJcdTAwMWKlmVx0XGZcYr5cdTAwMTWcrzNM2Xbnvt45Ov/x48f5Sf3i6vgwfXxcdTAwMWMwTH9O/9qHXHUwMDBm51t7lUIu5Z9095pH9lx1MDAxMCv1y+1cdTAwMWbjZ3k6f6Hbbd3MYcQ4XHUwMDAwhZRQi4qoSCNcdTAwMTY5vkMmjGhcdTAwMGWtiZ+aTO/MhFx1MDAxYrFcdTAwMTRcdTAwMTGpIOtDySBlXHUwMDA1aMLhJJin6bBloFxirnYh2cm0eJLW46QsmnJBy6ZGVcDEPI2MazJvxr5cdTAwMDPFXHUwMDBiSpjIi1x1MDAwNYj1dUaM49jRXHUwMDE5Rix/4/v9JfmwXHUwMDE3SD/sw0JNiWfD5Fx1MDAxYmyYgPDR5/FcdTAwMDLBXGYjb1x1MDAxMt+GVXnaNrYvdzqp6lx1MDAwNuxfyrPTn0onX+BcZnJcdTAwMDGGh+1cdTAwMTdcdTAwMWGPRIVcdTAwMDJEaGCaiVCbPsUteOu3mtlccr9/36hWz5pq/7qU7drcSXxcdTAwMTFcdTAwMDLFKVx1MDAwMdNq+aNcdTAwMDFcdTAwMDFWXGKrkLbcXHUwMDE4w+dcdTAwMTggm37VK6BCJD+KSWHleHYxxL1hXHUwMDFlJSXyzXnHylx1MDAwYpCYXHUwMDAzjMlcdTAwMTWgXHUwMDE3SPnDXHUwMDA1yLLw0ZEtVJJroUT8uen1k5udY5VLnzXRXFzdf9va6ejNXtJcdTAwMTWISN5cdTAwMDBcdC03XHUwMDFjJ4dcdTAwMDEkMOp24iVOIYtGvNFcdTAwMTT+5jqUXHUwMDFmXFyv5fa+X/LU/u3deW692Pl29X1OXHUwMDFkUjJQXHUwMDE4sixcdTAwMWTSkb5cdTAwMGKYkFxcQrBW5iXYT7/qxOuQ8JRigCBcdTAwMTXnlIKG0iHiYU9qXHUwMDEwlFx1MDAwYjFBXHUwMDE27Y1cdTAwMDbsU4xcdTAwMTIgRi/w80eLkTA8fPQpKlxySM1cdTAwMDH1aIzrpaBs9b5cdTAwMWSet9bWe2epn+1jW7lnJ7yQdC1SXHUwMDFlkTjprlx1MDAxNVO1SLteXHUwMDE3Slx1MDAxYm6QXHTFlydGU8ah5cTAc1JU5yB9WG9uXHUwMDFmVlx1MDAwN/5ajde+dUSnXdqZU3U0LF91Zk26kOxcdTAwMDCoOZKf6Vx1MDAxN71cdTAwMDKig0hcdTAwMTIvNJsqOkYsXHUwMDE04Z+qk1x1MDAwMNV5gYk/WnU4RqpcdTAwMGVcdTAwMDHEWUGr45codPZcdTAwMGWvXHUwMDFi6Hc6WXPon9rDwkb7eDPZsmOYp13dNVk9YVx0fCM6f5hlolBcdTAwMTGCXCKTQmk8Yl9cdTAwMTOSXHUwMDAwpuhPKywzXHUwMDFlc1Vj04ORXGLa01x1MDAxOJ6MfZz+ZHTcXG7BR/q0xFwiXHUwMDA1VFxc2PcpJlx1MDAwM1x1MDAxYpmkcCGlq+Ww8f3Qjl83379l6rtcdTAwMTfqqr2pdLfWbq4nXHUwMDFimK6aXGaBOdplXHUwMDE2UYcqXHUwMDFlldJcdTAwMWWlxXRYg5VcdTAwMTbeVvFooGS5P1x0zH9ENVmnWMzk8T6T7V3ft3WuslNax/R8XHUwMDBlytBcdTAwMWTgi4qLyLxcdTAwMWRlVEhoXHUwMDAzwDmzc1D11ItOuIPiWntcdTAwMTKtq8IgymMqVE2mUHiMoXtcdTAwMDe5TXzjXHUwMDEwcmQxmSTAWyvoNCBcZlx1MDAwNNK2hFx1MDAxNJMtyD/FLybbarX6Sysme4G6w1x1MDAwNirclqVcdTAwMTeTXHUwMDA1XHUwMDA3JENRKSkkiacxvk5cdTAwMWSfyduLTnFXZHbSm5mN3Lp/1j9KejFcdTAwMTnnnPyJtGjMw2zl6GtcdTAwMWVqySxFpXJPOHF8c43nSlaUTdelx6Gajla63vJ57fREXFyKrl/cuD1fWHVccmk2W5gwzTZsMnp2k9hcdTAwMWHcJEL86fxcdTAwMDNd6N521i94tre/0Thkx1x1MDAwN+e902RcdTAwMWI2QrdngFx1MDAxMftCuJZ5XHUwMDE4XHUwMDA2wDyrjEW7iDD47SdT1vKnu9vbP1x1MDAwZnY2+1x1MDAxOdk46lx1MDAxZmyd5W3cirXs9o+LU6wp1chu9ZrXR5XCzdXu4irW3NTg0s1cdTAwMWXMKP90s0RcdTAwMWGZjVx1MDAxZE7TezPpbo9SXHUwMDFjrYiOXHUwMDE5Ob6JXHUwMDA0SFxi4ZGlUlxcPZYwvy2cPlx1MDAwN8timr1lXHUwMDBllr3A+lx1MDAxZj1YJmT0xKlcIoiilvGHsEV3/fJ252fZ7MHZcVVV8VStdZOvcKRvJOVcdTAwMTiWsGFEalI4So0oXHUwMDAzs1x1MDAwYnjA7TdcdTAwMTe4/PptrWvWXHUwMDBlb1tcdTAwMWIoikff7zavXHUwMDBi+3M9wyZcdTAwMTRcYlxc+qhDcGooLEQgmUBleXzUT7/qxFx1MDAwYlx1MDAxMXpcdTAwMTZdMkdJ/6RcdTAwMTBcdTAwMTlcdTAwMTJcIvc4r3vY9+3FmqsqRK/AY3KF6Fx1MDAwNXL+cCGy0c8zUL5H2TaK+KtcdTAwMDGcsfNO6qJvMsfVtZ3Uxam6zlx1MDAwNzLQhCqR8lx1MDAxNFx1MDAwNzR0qUJPKJFcdTAwMDTwXGZl92RcdTAwMWQ1uHj5VKJcdTAwMTlKxLLp0rbMs61Byp5cdTAwMWOcXHUwMDE2JWtcdTAwMTVcdTAwMDbzjX9LYiZcdTAwMTbE/1KUKLpCXHUwMDA2NWHBTVx1MDAwYsVcdTAwMDb99IteXHUwMDAxIZJuXHUwMDEwmaDjVlx1MDAxYVx1MDAxOFx1MDAxN1wiKZTHkFx1MDAwMGdcZqi3V2uuqlx1MDAxMP1WXHUwMDE50Vx1MDAwYtz80UIkZ1RQk1WSXHUwMDA20cZcdTAwMTeiXvNO+er7fjs/yFxmWje8t7X5bTfpQiQ98r9aXHUwMDAyQ+VcbmdHXGb0ULQmSIiY0kYwMz5h9Y8uWjtcdTAwMTPrezc3crN4spMvpG4vq7uVg0pcdTAwMDIlR+jIKVeyIHS/jOXxNWf6Va+A5lxi4chcdTAwMWXc8LWRIatl1EJcdTAwMTG+4qozz9pcdTAwMDDJVZ1cdTAwMTeI+KNVh7xeVFSCMlxiwOJPNG01N3c7W6ksbMP5oLnm64vNb1FcdTAwMDNcdTAwMTJcdNFcdTAwMWNQzFOMXHUwMDFiTVx1MDAxZW+yXGJCXHUwMDAwp9RILWpJnZUuWVx1MDAxYq7Ywd6rZE1gdMlcdTAwMWFcdTAwMThB3oDJ+KVcdTAwMDCbOyeH7eyp3fyW/nFweqK+i3w/asWOxJRcdTAwMDLQZXqKYFx1MDAwMUrgtFx1MDAxMWLOPFSWhJPDm59r/u0qXHUwMDAxdvs3p8Wj01x1MDAxY5rMnbzoraW3blpnXHUwMDBim7WUYFx1MDAwMlx1MDAxNfhLrVx1MDAwNFx1MDAxMJFcdTAwMTUxnJO2U0Ns/JVrjptnxVRd18o1xnJZOEinipuphFx1MDAxM7QxRNBaI2pwSz2HaopcdTAwMDE8gr5WXGa5q/JXn/Mks1KFa57ezd3traV+yPtTuGfFjctKMW4hwPFdeW3ztsVcdTAwMGYzuWo5n8FBrnmbW1xcIYCVbOkpyMxVXHUwMDAztNKcXHUwMDFiXHUwMDFi/2nN6b2Z8Fx1MDAxNITMtSeFVdZcdTAwMTlszoJcdTAwMTHzUFx04HJw7Vx1MDAxYziGXHUwMDE20/hMQVY1XHUwMDA1eYH2PzpcdTAwMDVcdTAwMTFcInLgXHUwMDBiKE92i1xixlx1MDAxZvdK93aE/H7q31xcXHUwMDFmd1x1MDAwYmv3ftpcdTAwMWVVo8aiXHUwMDEzI3HaXHUwMDE1u1lheFjEXHUwMDFlSlx1MDAwMcAjkUelXHUwMDFmpkTDXHL7lLjg/b+6YoNGZuOuVsZu75JlZfv84GLO0TD3Z9lSXHUwMDA0PPpcdTAwMDFcdTAwMDRAVFx1MDAwNIb4XHUwMDBmIEy/6MQrkXElaVx1MDAxYbmRjPAvxlFvSIhIXHUwMDE4mISQ7/vUoVXVoVx1MDAxN7j5w3XIRJaJUrRcbuJnmGNcdJutVvU8ZVtHfnX/JNPM1lxuulHNJl+IXGJtQqCSROdhIZIgPWRMuVx1MDAxZFjQ2OC04KdcdTAwMTJNKtH29fqV8Sv5Sr3Fi6Zlv12y9LzzMu+iRFx1MDAxOF1cdTAwMDAjlVuxiFx1MDAwNYpcdTAwMDVegv30q15cdTAwMDEpcvVWyEBcdTAwMWJcdTAwMTGWXCIp0DPcjXxcdG5ccshFbIjxKUVcdTAwMWYrRS+w80dLkdSRUoSKvJLSc9Sk2SNbNlBLr/XyV6XizV1e/cgmfCVcdTAwMDEw6GlGcWCUskqHXHUwMDAzUitPUpxQsqRcdTAwMDAkLi8jWqlCgFx1MDAxMzi9zJTklrxs71c3dnZz+6U7lUDBXHUwMDEx0etkSPdwXHUwMDFiSX98vZl+0SugN1x1MDAxY6zbeskyJoIrRD/UXHUwMDAx6EXi+1NwXHUwMDEyIDgvkPBHXHUwMDBiXHUwMDBlRG+AqrVGXHUwMDAwruOnPt30UZrt7O83hTpJy8Za5uw+9zPZguOqnIGSXG5BXHUwMDA0NNysYywgXHUwMDA1uMdcdTAwMTJcdTAwMDJjcG9cbsjVL1x1MDAwM1x1MDAxMDqQXGIvtVxmQEfv+oRCgWVyjpVr9lmrv5nL+oY31/X6xe4mXjRcdTAwMTO/qix6buEowp52i5mFXHUwMDE28qNcdTAwMWXwrFvv+nFcdTAwMTW0t42SRVx1MDAwMVO6labdhlNuq1x1MDAwM/c1OFx0T5Bu116SXHUwMDExPVxcx1x1MDAwMyZcdTAwMGJcdTAwMDDeXHUwMDE3o1x1MDAxMoRcXPoq5DNYU7n1XHUwMDAyOJujiv460ylmXHUwMDFh9udm+eLozFx1MDAxN1x1MDAwNzlTgGbSnVxmXHUwMDExJlx1MDAwMVx1MDAxMNFwI1x1MDAxMVhgfYSHrSS14yxOyTORhjViedtGk5eR5ExcdTAwMTRcYlx1MDAwYqFAmbGUXGY1yY1wwLKt+9KLUlx1MDAxNuSV5linpnbrlyO8Ut2/6M9wSv1WO8omjTV3Yjma8VMuYjWa6MdjXCLHxITbbFCjiV8nUDXr+3fn1fIgla1s5Vx1MDAxYq3LuzxL/OxcZoWFR1xcL4XVhlEqMl58hpx0h6SXQKeMU55/dFxc58hcdTAwMTZcdTAwMTbyV2dcXF9cdTAwMWNuX36HUjrTb3/G9Vx1MDAwN8b1QzdPM5Qztlx1MDAxMVVuu2vkMv7I2mxCT2pkg/bc9ijKKKVItsdcdTAwMWQloPBcdTAwMDSS41x1MDAxNNxcIuCbxrpnXHUwMDA2tmWeNW5FVMtcdTAwMTRcdTAwMTnHXHUwMDEx31x1MDAwNi0loqA2MOOe0MNcdEvJiXtcdTAwMTSToOdcdTAwMTmMmFxcXGZcdTAwMTFcdTAwMWWPzFhcZnFG5Zog9tOv2my01y90++u1ZrnWrIw3zH849XZcZl84jOjSwLWSeaApYXLzXHUwMDE0bjhSju6s65hC2/G2Ry6c2lxmlj9MsE1cXLrfLL/cpNn11IEmpZhnXHUwMDE0pztkhit6XHUwMDE4hWKiTZR5o9vcW5I9dONQdqJN9UKvv9FqNGp96vmDVq3ZXHUwMDBm9/CwK9dcXNBX/cJcdTAwMDTh0DVcdTAwMDVfXHUwMDBis0PbfeM4yY9++zKKnuF/nn//959T352KwrX7mUD06Nv+XGL+Ozet2ehHq1xmUr9Solx1MDAxY3/4ZracJZXVXHUwMDE0o1x1MDAwNJQpUIbwL9n4g1x1MDAxY1xi3ONusFx1MDAwMJlcdTAwMTaISyyWt8JDbYZLW4xtXGY4mj/gXHUwMDFlXHUwMDEzlFxmW0roXHRcdTAwMWWT01x0XHUwMDFjKWWiTJnNY15cdTAwMTbOasxcYnzVY78xWW22KVx1MDAxZadcdTAwMTBiNNdXXGJARKLZiO6fOcR40m1cdTAwMDIurGTuwVFjZvPa+HWsXHUwMDE0u0yH1/C1XHRkzckukfM1NnIyUlx0NG5p8/iL1lx1MDAxY1X29zZQ9vzaXHUwMDFl+Nnvt53Dw42bpJNLSqJLd4hBrEIlJIyzXHUwMDBiV+5JXHUwMDFkrVxyMolumONNw3Czn8Ux2uNCXHUwMDE5lNHZ0MScXHK3lFxyKVx1MDAwN4x3SYfesMftn7O+91xyK1x1MDAxMc783iU+2OD2one9/8b0Lf5cdTAwMTRcdTAwMTahaNBo9r7811N29qVX6rbq9f9+35QuRjNcdTAwMTYxpVx1MDAxNemHzIxcdTAwMDc9lFtseq5nzWdcdTAwMDM6oZwljEdGXHUwMDEzrHFcdTAwMWJccihcdTAwMDXjXHUwMDAzOI5HXHUwMDE0XHUwMDEzRltcdTAwMTKS5Y7MkunVJM1cbshccmshg9tCjCa3QHiGh2r+nthcdTAwMGKcnVx1MDAxYptSeG8/5MLY7dm3RD80W1x1MDAxN8ezPNBkcyVKpa1Ukk2meZy6XFxcInPVkbONUFRrZu92PO7OXHUwMDE4Uk5jQIOWXG5IlfREczQhkfwsvW4lXHUwMDEycuVEo1bKg0VcdTAwMDPa/Vx1MDAwNKE8p1x1MDAwMYukM2TRdCY510zMs13BbFx1MDAxZE0onVnjaW5cdGSEXCJcdCZUmsxccvfc7CRcYiWXTGfaeJRmSLe/MFx1MDAxMavk09hMoEfo0I55hzvdXHUwMDA0nkp95LRhnVx1MDAwMVx1MDAwNFx1MDAxN194f05T1Ly5dtpYXHUwMDFhp1x1MDAxMYtI4G6tU1wiXG6plbTTSMRyx2jSXHUwMDBlf8zkMFEsZpu9LO5cdTAwMTjPckKVXHUwMDE0YIxcdTAwMDX6VUzSLHiUhJEyuM50T2/halx1MDAxM1skst3PJKZcdTAwMTdGbjxyXHUwMDEynVTOclxu9vjcNtvLJ5TbXGI4XHUwMDFlKkBKLCWA0eOT6NxQz1x1MDAxM945d0umkl1b3pg8R+5m891CXHUwMDBlklx1MDAwMlLBlEJcdTAwMGbKOSlYJcWhXHUwMDFlrvRcdTAwMTBMdlx1MDAxZshNKtRqrlxc8/d1a47ZyPRcZvdop0ZJXHUwMDE2XHUwMDFjXHUwMDEwfnJrZFx1MDAxOEjFXHTsZlg680rPNvsp33FmQ+ZG+MDtduVOOTko71x1MDAxNlx1MDAxNzFuSS73yFx1MDAwMzVcdHGiUatEbdHAXHUwMDFlvjpcdTAwMDHpObkter3nSNvGnfZqPUel88Fpo8GuQF9tnaX3i93SbvbnbtRcIueJoTaNnuSkXHUwMDE3XHUwMDA0fjfXKMeX70CiNiNcZqM/Llx1MDAwZmVLXHUwMDFjN1NIpt1cdTAwMWHlRuhcZuppS9hMKatcdTAwMTSMzLVcclxc5YruXvNYRvDWcaj4ZVx1MDAwNNVWt3bvxoueRn7ed/xpxumXWl5cdTAwMTCoXHUwMDFiXHUwMDBll1xyKcvA7VpcdTAwMTY73mfDIaHxzqX2iF6F21x1MDAwN5TcitLjhdSopVx1MDAwN4xcdTAwMWOkojcuN+Ct52qDrZ3Ykz7gZKwnnVx1MDAwMNqIkSdtXHUwMDE5XHSHlfOE/6In4ubedWpeKzNbVr6EJuKoP9zjXCJ0f8mowlx1MDAxNN/gJvNcdTAwMWZ2d32li5m5a9uYi2Hu4Vx1MDAxOMVcdTAwMDRnROtcdTAwMDKnjIOBoFx1MDAxYqxcdTAwMWTna5fIMTZ5L1bKxURh2v2kJuBcdTAwMWNlYv54PMHXQrt92Ce4Pd9cdTAwMGXCd638yPWjq/x6XfNv1qfUi19cZn+cXGZccvvTUZI/jIFff/z6f3dfuVAifQ== HeaderTweetTweetTweetTweetFooterTweetTweetTweetTweetTweetTweetTweetTweetFixedFixedColumns (vertical scroll)horizontal scroll

It's rough, but it's all we need.

"},{"location":"how-to/design-a-layout/#tip-2-work-outside-in","title":"Tip 2. Work outside in","text":"

Like a sculpture with a block of marble, it is best to work from the outside towards the center. If your design has fixed elements (like a header, footer, or sidebar), start with those first.

In our sketch we have a header and footer. Since these are the outermost widgets, we will begin by adding them.

Tip

Textual has builtin Header and Footer widgets which you could use in a real application.

The following example defines an app, a screen, and our header and footer widgets. Since we're starting from scratch and don't have any functionality for our widgets, we are going to use the Placeholder widget to help us visualize our design.

In a real app, we would replace these placeholders with more useful content.

layout01.pyOutput
from textual.app import App, ComposeResult\nfrom textual.screen import Screen\nfrom textual.widgets import Placeholder\nclass Header(Placeholder):  # (1)!\npass\nclass Footer(Placeholder):  # (2)!\npass\nclass TweetScreen(Screen):\ndef compose(self) -> ComposeResult:\nyield Header(id=\"Header\")  # (3)!\nyield Footer(id=\"Footer\")  # (4)!\nclass LayoutApp(App):\ndef on_mount(self) -> None:\nself.push_screen(TweetScreen())\nif __name__ == \"__main__\":\napp = LayoutApp()\napp.run()\n
  1. The Header widget extends Placeholder.
  2. The footer widget extends Placeholder.
  3. Creates the header widget (the id will be displayed within the placeholder widget).
  4. Creates the footer widget.

LayoutApp #Header

"},{"location":"how-to/design-a-layout/#tip-3-apply-docks","title":"Tip 3. Apply docks","text":"

This app works, but the header and footer don't behave as expected. We want both of these widgets to be fixed to an edge of the screen and limited in height. In Textual this is known as docking which you can apply with the dock rule.

We will dock the header and footer to the top and bottom edges of the screen respectively, by adding a little CSS to the widget classes:

layout02.pyOutput
from textual.app import App, ComposeResult\nfrom textual.screen import Screen\nfrom textual.widgets import Placeholder\nclass Header(Placeholder):\nDEFAULT_CSS = \"\"\"\n    Header {\n        height: 3;\n        dock: top;\n    }\n    \"\"\"\nclass Footer(Placeholder):\nDEFAULT_CSS = \"\"\"\n    Footer {\n        height: 3;\n        dock: bottom;\n    }\n    \"\"\"\nclass TweetScreen(Screen):\ndef compose(self) -> ComposeResult:\nyield Header(id=\"Header\")\nyield Footer(id=\"Footer\")\nclass LayoutApp(App):\ndef on_ready(self) -> None:\nself.push_screen(TweetScreen())\nif __name__ == \"__main__\":\napp = LayoutApp()\napp.run()\n

LayoutApp #Header #Footer

The DEFAULT_CSS class variable is used to set CSS directly in Python code. We could define these in an external CSS file, but writing the CSS inline like this can be convenient if it isn't too complex.

When you dock a widget, it reduces the available area for other widgets. This means that Textual will automatically compensate for the 6 additional lines reserved for the header and footer.

"},{"location":"how-to/design-a-layout/#tip-4-use-fr-units-for-flexible-things","title":"Tip 4. Use FR Units for flexible things","text":"

After we've added the header and footer, we want the remaining space to be used for the main interface, which will contain the columns in the sketch. This area is flexible (will change according to the size of the terminal), so how do we ensure that it takes up precisely the space needed?

The simplest way is to use fr units. By setting both the width and height to 1fr, we are telling Textual to divide the space equally amongst the remaining widgets. There is only a single widget, so that widget will fill all of the remaining space.

Let's make that change.

layout03.pyOutput
from textual.app import App, ComposeResult\nfrom textual.screen import Screen\nfrom textual.widgets import Placeholder\nclass Header(Placeholder):\nDEFAULT_CSS = \"\"\"\n    Header {\n        height: 3;\n        dock: top;\n    }\n    \"\"\"\nclass Footer(Placeholder):\nDEFAULT_CSS = \"\"\"\n    Footer {\n        height: 3;\n        dock: bottom;\n    }\n    \"\"\"\nclass ColumnsContainer(Placeholder):\nDEFAULT_CSS = \"\"\"\n    ColumnsContainer {\n        width: 1fr;\n        height: 1fr;\n        border: solid white;\n    }\n    \"\"\"  # (1)!\nclass TweetScreen(Screen):\ndef compose(self) -> ComposeResult:\nyield Header(id=\"Header\")\nyield Footer(id=\"Footer\")\nyield ColumnsContainer(id=\"Columns\")\nclass LayoutApp(App):\ndef on_ready(self) -> None:\nself.push_screen(TweetScreen())\nif __name__ == \"__main__\":\napp = LayoutApp()\napp.run()\n
  1. Here's where we set the width and height to 1fr. We also add a border just to illustrate the dimensions better.

LayoutApp #Header \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502#Columns\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 #Footer

As you can see, the central Columns area will resize with the terminal window.

"},{"location":"how-to/design-a-layout/#tip-5-use-containers","title":"Tip 5. Use containers","text":"

Before we add content to the Columns area, we have an opportunity to simplify. Rather than extend Placeholder for our ColumnsContainer widget, we can use one of the builtin containers. A container is simply a widget designed to contain other widgets. Containers are styled with fr units to fill the remaining space so we won't need to add any more CSS.

Let's replace the ColumnsContainer class in the previous example with a HorizontalScroll container, which also adds an automatic horizontal scrollbar.

layout04.pyOutput
from textual.app import App, ComposeResult\nfrom textual.containers import HorizontalScroll\nfrom textual.screen import Screen\nfrom textual.widgets import Placeholder\nclass Header(Placeholder):\nDEFAULT_CSS = \"\"\"\n    Header {\n        height: 3;\n        dock: top;\n    }\n    \"\"\"\nclass Footer(Placeholder):\nDEFAULT_CSS = \"\"\"\n    Footer {\n        height: 3;\n        dock: bottom;\n    }\n    \"\"\"\nclass TweetScreen(Screen):\ndef compose(self) -> ComposeResult:\nyield Header(id=\"Header\")\nyield Footer(id=\"Footer\")\nyield HorizontalScroll()  # (1)!\nclass LayoutApp(App):\ndef on_ready(self) -> None:\nself.push_screen(TweetScreen())\nif __name__ == \"__main__\":\napp = LayoutApp()\napp.run()\n
  1. The builtin container widget.

LayoutApp #Header #Footer

The container will appear as blank space until we add some widgets to it.

Let's add the columns to the HorizontalScroll. A column is itself a container which will have a vertical scrollbar, so we will define our Column by subclassing VerticalScroll. In a real app, these columns will likely be added dynamically from some kind of configuration, but let's add 4 to visualize the layout.

We will also define a Tweet placeholder and add a few to each column.

layout05.pyOutput
from textual.app import App, ComposeResult\nfrom textual.containers import HorizontalScroll, VerticalScroll\nfrom textual.screen import Screen\nfrom textual.widgets import Placeholder\nclass Header(Placeholder):\nDEFAULT_CSS = \"\"\"\n    Header {\n        height: 3;\n        dock: top;\n    }\n    \"\"\"\nclass Footer(Placeholder):\nDEFAULT_CSS = \"\"\"\n    Footer {\n        height: 3;\n        dock: bottom;\n    }\n    \"\"\"\nclass Tweet(Placeholder):\npass\nclass Column(VerticalScroll):\ndef compose(self) -> ComposeResult:\nfor tweet_no in range(1, 20):\nyield Tweet(id=f\"Tweet{tweet_no}\")\nclass TweetScreen(Screen):\ndef compose(self) -> ComposeResult:\nyield Header(id=\"Header\")\nyield Footer(id=\"Footer\")\nwith HorizontalScroll():\nyield Column()\nyield Column()\nyield Column()\nyield Column()\nclass LayoutApp(App):\ndef on_ready(self) -> None:\nself.push_screen(TweetScreen())\nif __name__ == \"__main__\":\napp = LayoutApp()\napp.run()\n

LayoutApp #Header #Tweet1#Tweet1#Tweet1#Tweet1 #Footer

Note from the output that each Column takes a quarter of the screen width. This happens because Column extends a container which has a width of 1fr.

It makes more sense for a column in a Twitter / Mastodon client to use a fixed width. Let's set the width of the columns to 32.

We also want to reduce the height of each \"tweet\". In the real app, you might set the height to \"auto\" so it fits the content, but lets set it to 5 lines for now.

Here's the final example and a reminder of the sketch.

layout06.pyOutputSketch
from textual.app import App, ComposeResult\nfrom textual.containers import HorizontalScroll, VerticalScroll\nfrom textual.screen import Screen\nfrom textual.widgets import Placeholder\nclass Header(Placeholder):\nDEFAULT_CSS = \"\"\"\n    Header {\n        height: 3;\n        dock: top;\n    }\n    \"\"\"\nclass Footer(Placeholder):\nDEFAULT_CSS = \"\"\"\n    Footer {\n        height: 3;\n        dock: bottom;\n    }\n    \"\"\"\nclass Tweet(Placeholder):\nDEFAULT_CSS = \"\"\"\n    Tweet {\n        height: 5;\n        width: 1fr;\n        border: tall $background;\n    }\n    \"\"\"\nclass Column(VerticalScroll):\nDEFAULT_CSS = \"\"\"\n    Column {\n        height: 1fr;\n        width: 32;\n        margin: 0 2;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nfor tweet_no in range(1, 20):\nyield Tweet(id=f\"Tweet{tweet_no}\")\nclass TweetScreen(Screen):\ndef compose(self) -> ComposeResult:\nyield Header(id=\"Header\")\nyield Footer(id=\"Footer\")\nwith HorizontalScroll():\nyield Column()\nyield Column()\nyield Column()\nyield Column()\nclass LayoutApp(App):\ndef on_ready(self) -> None:\nself.push_screen(TweetScreen())\nif __name__ == \"__main__\":\napp = LayoutApp()\napp.run()\n

LayoutApp #Header \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e\u258a\u258e\u258a\u258e \u258a#Tweet1\u258e\u258a#Tweet1\u258e\u258a#Tweet1\u258e \u258a\u258e\u258a\u258e\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e\u2583\u2583\u258a\u258e\u2583\u2583\u258a\u258e \u258a#Tweet2\u258e\u258a#Tweet2\u258e\u258a#Tweet2\u258e \u258a\u258e\u258a\u258e\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e\u258a\u258e\u258a\u258e \u258a#Tweet3\u258e\u258a#Tweet3\u258e\u258a#Tweet3\u258e \u258a\u258e\u258a\u258e\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e\u258a\u258e\u258a\u258e \u258a#Tweet4\u258e\u258a#Tweet4\u258e\u258a#Tweet4\u258e \u258a\u258e\u258a\u258e\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e\u258a\u258e\u258a\u258e \u258a#Tweet5\u258e\u258a#Tweet5\u258e\u258a#Tweet5\u258e \u258a\u258e\u258a\u258e\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258c #Footer

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1daW9cdTAwMWK7kv1+f0WQ92VcdTAwMDa46ktcdTAwMTbJXCJ5gcHAi1x1MDAxY1mOl8iKt8GDoaUtyda+eHvIf5+ivKjVUsstW7JbuTaQxGktzWafOudcdTAwMTRZTf7njy9fvvbv2v7Xv7989W9LhXqt3C3cfP3TXHUwMDFkv/a7vVqrSS/B8P+91qBbXHUwMDFhvrPa77d7f//1V6PQvfL77Xqh5HvXtd6gUO/1XHUwMDA35VrLK7VcdTAwMWF/1fp+o/e/7u+9QsP/n3arUe53vdFJUn651m91XHUwMDFmzuXX/Ybf7Pfo2/+P/v/ly3+Gf1x1MDAwN1rX9Uv9QrNS94dcdTAwMWZcdTAwMTi+NGogZ0aFXHUwMDBm77Waw9ZyLoxGxVxyPL+j1tukXHUwMDEz9v0yvXxBjfZHr7hDX1x1MDAwZnL9dT+3q/ay56WbXCK2djHdXHUwMDFkjM57UavXXHUwMDBm+3f1YbtK3Vavl6pcdTAwMTb6peroXHUwMDFkvX63deVcdTAwMWbXyv3qU/dcdTAwMDWOP3+216KuXHUwMDE4farbXHUwMDFhVKpNv+d6gT9cdTAwMWZttVx1MDAwYqVa/254lez56ENX/P1ldOSW/pdC4Vx0LY1VqLW1KtAr7lx1MDAwYkAoXHUwMDBmhFx1MDAxNKhcdTAwMDVarahfQi3baNXpjlDL/sWGP6O2XHUwMDE1XHUwMDBipatcbjWwWX5+T79baPbahS7dt9H7blx1MDAxZa9ZSemhcCdjinM+akfVr1WqfXqHQOZcdTAwMTnOrJQohLs9OGqMP7w1XHUwMDFjjNCopVx1MDAxNs+vuCa0t8tDnPw72HHN8mPHNVx1MDAwN/X6qNXuhXRcdTAwMDBbo89cZtrlwlx1MDAwM1x1MDAwMjhcdTAwMWFcdIpJ1Fxio1x1MDAwZanXmlfhr6u3Slcj0Fxmj/7681x1MDAxNXDVKlx1MDAxYa1WcVx1MDAwNIlcdTAwMThcdTAwMWKsuVLn5367sL63s3+Lza2fXHUwMDA3nZ3tTlx1MDAwNFhDgPtImFx1MDAxYcGVtIpxJjhBIIRT6Vx1MDAxOaUlgcJcdTAwMDJKi8vEqfBcdTAwMThHXHUwMDA1dD6urTaTQFx1MDAwNeMhMiFccjJAQaCeXHUwMDAwKt0zJYGuZvVw6tfrtXZvKkpxXHUwMDA2p1x1MDAxYZCcI1cmNkzbP86OS+lstssvc9f5+3TqrtG8fVxyTPn7wVRxz1xuhdTxQljOQIbolDjOMCMl11x1MDAwZcqgw1x1MDAxMTRcdTAwMTdM/3VRUKBgXHUwMDEyolxceJJcdTAwMDEoi0D/SGPkJEY5eFxuKVKISFx1MDAxOVxiXHUwMDAxUocxyql9ilNcdTAwMWL5b1x1MDAwNVItbFx1MDAxNEi1QXfNXHUwMDEwX/dP1jrpu8ZB7qC4tnXT2upvna3jTcIxXG7KY9TdyLnhlsBcdTAwMDEhiFxuT1x1MDAxObRkXG4kUZhV8o1cdTAwMTAtMqaWXHUwMDA1Uc1cdTAwMTXpgeC/XHUwMDE5Qq2IQijnjJSFS81iQ7TeSbdTV1x1MDAxNexs1upXWLrrb53UXHUwMDBlk1xyUVx1MDAxMljgXHUwMDAyQWjBmWRWh1x1MDAxMFxujmQl3Vx1MDAxNVxyxLVv41BcdTAwMGVFY3BZXHUwMDAwpcsgqkcuV1DoZ+dPKlx1MDAxMqPu1iihXHUwMDAzb3hcdKLr37ZcbrsldbLV2V1vnt3+OL6pXHUwMDFjNJJcctFcdTAwMTTlXCKk4lZcdTAwMTFFcnBJyThGXHUwMDExXHQ+hFxmTTTKJcOlYFRJ5oym4Vx1MDAxNCnaQiBOXHUwMDAyOZMnNaekytphO0CFIUquTJGLNYrNXHUwMDAx0SewjOBcIlx1MDAxZY/8ikbu82dGn1x1MDAwZcCt79+O3HZcdTAwMDBcdTAwMWPd9mG1cbB9tj/YK+VzlbXWgVx1MDAxOdx9fX7fr8ffosJCXGImjTLWLiosxtpcdTAwMTmMXGJcdTAwMTNcdTAwMTVcdTAwMTCczs5dvqBiR8T0i55cdTAwMWVcdTAwMTHVQqk66PpcdTAwMWZcdTAwMWZcdTAwMTNcXCO5W+JKQIK+YTLE21x1MDAxYTxcdTAwMDdB4nNcdTAwMTlcbpnXXHUwMDA0XHUwMDA1xVx1MDAxZbuYYn4p8oBcdTAwMTPaXHUwMDE5p1x1MDAwZSeRmJafTVx1MDAwNoFcdTAwMDWuyZ3PXHUwMDE1XHUwMDA0r+Pp11x1MDAwMHJ041vN/mHt3vU9sLGjW4VGrX43du+GSKWuyviFst/9OnZ8rV6rONR+LVF7gy9cdTAwMTJ2+7VSof78hkatXFxcdTAwMGVqQIlOVqg1/e52XHUwMDFj7m51a5Vas1DPR7SFrt3PPOupXHUwMDE3uDPFQs93r1x1MDAwZVx1MDAwM+j1Qlx1MDAwNVx1MDAxOJmTkns0QltcdTAwMTN/5GRtc6O+M2Bddn/XuK/YfL5/lc68LizfcexEXHUwMDE5j5NRkoxsfUhcdTAwMDPcN1xihp7ipGQ8XHUwMDE4XHUwMDE3XHUwMDBiXHUwMDFlNeGKuEFScvEkiKPrXHUwMDFmhSX5KbCGXHUwMDFisnXKqCA/PLkp0IZy6sDgz1x1MDAxMt2UJt5cdTAwMGVkP0t1U1x1MDAwMapcZouHM/zW5fmxUdqpbbT2sr3NXGZ2N1x1MDAwZTK57av9evpV43vvOXBcIjy6t+hcdTAwMTJwMis6hFBuKCdcdTAwMTX0gmIhYLzGTalCoXgh5TTPzz1GSIUpg89Selx1MDAxMjSzj01cYjbxXHUwMDExnUJcdTAwMWKlmVx0XGZcYr5cdTAwMTWcrzNM2Xbnvt45Ov/x48f5Sf3i6vgwfXxcdTAwMWMwTH9O/9qHXHUwMDBm51t7lUIu5Z9095pH9lx1MDAxMCv1y+1cdTAwMWbjZ3k6f6Hbbd3MYcQ4XHUwMDAwhZRQi4qoSCNcdTAwMTY5vkMmjGhcdTAwMGWtiZ+aTO/MhFx1MDAxYrFcdTAwMTRcdTAwMTGpIOtDySBlXHUwMDA1aMLhJJin6bBloFxirnYh2cm0eJLW46QsmnJBy6ZGVcDEPI2MazJvxr5cdTAwMDPFXHUwMDBiSpjIi1x1MDAwNYj1dUaM49jRXHUwMDE5Rix/4/v9JfmwXHUwMDE3SD/sw0JNiWfD5Fx1MDAxYmyYgPDR5/FcdTAwMDLBXGYjb1x1MDAxMt+GVXnaNrYvdzqp6lx1MDAwNuxfyrPTn0onX+BcZnJcdTAwMDGGh+1cdTAwMTdcdTAwMWGPRIVcdTAwMDJEaGCaiVCbPsUteOu3mtlccr9/36hWz5pq/7qU7drcSXxcdTAwMTFcdTAwMDLFKVx1MDAwMdNq+aNcdTAwMDFcdTAwMDFWXGKrkLbcXHUwMDE4w+dcdTAwMTggm37VK6BCJD+KSWHleHYxxL1hXHUwMDFlJSXyzXnHylx1MDAwYpCYXHUwMDAzjMlcdTAwMTWgXHUwMDE3SPnDXHUwMDA1yLLw0ZEtVJJroUT8uen1k5udY5VLnzXRXFzdf9va6ejNXtJcdTAwMTWISN5cdTAwMDBcdC03XHUwMDFjJ4dcdTAwMDEkMOp24iVOIYtGvNFcdTAwMTT+5jqUXHUwMDFmXFyv5fa+X/LU/u3deW692Pl29X1OXHUwMDFkUjJQXHUwMDE4sixcdTAwMWTSkb5cdTAwMGKYkFxcQrBW5iXYT7/qxOuQ8JRigCBcdTAwMTXnlIKG0iHiYU9qXHUwMDEwlFx1MDAwYjFBXHUwMDE27Y1cdTAwMDbsU4xcdTAwMTIgRi/w80eLkTA8fPQpKlxySM1cdTAwMDH1aIzrpaBs9b5cdTAwMWSet9bWe2epn+1jW7lnJ7yQdC1SXHUwMDFlkTjprlx1MDAxNVO1SLteXHUwMDE3Slx1MDAxYm6QXHTFlydGU8ah5cTAc1JU5yB9WG9uXHUwMDFmVlx1MDAwN/5ajde+dUSnXdqZU3U0LF91Zk26kOxcdTAwMDCoOZKf6Vx1MDAxN71cdTAwMDKig0hcdTAwMTIvNJsqOkYsXHUwMDE04Z+qk1x1MDAwMNV5gYk/WnU4RqpcdTAwMGVcdTAwMDHEWUGr45codPZcdTAwMGWvXHUwMDFi6Hc6WXPon9rDwkb7eDPZsmOYp13dNVk9YVx0fCM6f5hlolBcdTAwMTGCXCKTQmk8Yl9cdTAwMTOSXHUwMDAwpuhPKywzXHUwMDFlc1Vj04ORXGLa01x1MDAxOJ6MfZz+ZHTcXG7BR/q0xFwiXHUwMDA1VFxc2PcpJlx1MDAwM1x1MDAxYpmkcCGlq+Ww8f3Qjl83379l6rtcdTAwMTfqqr2pdLfWbq4nXHUwMDFimK6aXGaBOdplXHUwMDE2UYcqXHUwMDFlldJcdTAwMWWlxXRYg5VcdTAwMTbeVvFooGS5P1x0zH9ENVmnWMzk8T6T7V3ft3WuslNax/R8XHUwMDBlytBcdTAwMWTgi4qLyLxcdTAwMWRlVEhoXHUwMDAzwDmzc1D11ItOuIPiWntcdTAwMTKtq8IgymMqVE2mUHiMoXtcdTAwMDe5TXzjXHUwMDEwcmQxmSTAWyvoNCBcZlx1MDAwNNK2hFx1MDAxNJMtyD/FLybbarX6Sysme4G6w1x1MDAwNirclqVcdTAwMTeTXHUwMDA1XHUwMDA3JENRKSkkiacxvk5cdTAwMWSfyduLTnFXZHbSm5mN3Lp/1j9KejFcdTAwMTnnnPyJtGjMw2zl6GtcdTAwMWVqySxFpXJPOHF8c43nSlaUTdelx6Gajla63vJ57fREXFyKrl/cuD1fWHVccmk2W5gwzTZsMnp2k9hcdTAwMWHcJEL86fxcdTAwMDNd6N521i94tre/0Thkx1x1MDAwN+e902RcdTAwMWI2QrdngFx1MDAxMftCuJZ5XHUwMDE4XHUwMDA2wDyrjEW7iDD47SdT1vKnu9vbP1x1MDAwZnY2+1x1MDAxOdk46lx1MDAxZmyd5W3cirXs9o+LU6wp1chu9ZrXR5XCzdXu4irW3NTg0s1cdTAwMWXMKP90s0RcdTAwMWGZjVx1MDAxZE7TezPpbo9SXHUwMDFjrYiOXHUwMDE5Ob6JXHUwMDA0SFxi4ZGlUlxcPZYwvy2cPlx1MDAwN8timr1lXHUwMDBllr3A+lx1MDAxZj1YJmT0xKlcIoiilvGHsEV3/fJ252fZ7MHZcVVV8VStdZOvcKRvJOVcdTAwMTiWsGFEalI4So0oXHUwMDAzs1x1MDAwYnjA7TdcdTAwMTe4/PptrWvWXHUwMDBlb1tcdTAwMWIoikff7zavXHUwMDBi+3M9wyZcdTAwMTRcYlxc+qhDcGooLEQgmUBleXzUT7/qxFx1MDAwYlx1MDAxMXpcdTAwMTZdMkdJ/6RcdTAwMTBcdTAwMTlcdTAwMTJcIvc4r3vY9+3FmqsqRK/AY3KF6Fx1MDAwNXL+cCGy0c8zUL5H2TaK+KtcdTAwMDGcsfNO6qJvMsfVtZ3Uxam6zlx1MDAwNzLQhCqR8lx1MDAxNFx1MDAwNzR0qUJPKJFcdTAwMDTwXGZl92RcdTAwMWQ1uHj5VKJcdTAwMTlKxLLp0rbMs61Byp5cdTAwMWOcXHUwMDE2JWtcdTAwMTVcdTAwMDbzjX9LYiZcdTAwMTbE/1KUKLpCXHUwMDA2NWHBTVx1MDAwYsVcdTAwMDb99IteXHUwMDAxIZJuXHUwMDEwmaDjVlx1MDAxYVx1MDAxOFx1MDAxN1wiKZTHkFx1MDAwMGdcZqi3V2uuqlx1MDAxMP1WXHUwMDE50Vx1MDAwYtz80UIkZ1RQk1WSXHUwMDA20cZcdTAwMTeiXvNO+er7fjs/yFxmWje8t7X5bTfpQiQ98r9aXHUwMDAyQ+VcbmdHXGb0ULQmSIiY0kYwMz5h9Y8uWjtcdTAwMTPrezc3crN4spMvpG4vq7uVg0pcdTAwMDIlR+jIKVeyIHS/jOXxNWf6Va+A5lxi4chcdTAwMWXc8LWRIatl1EJcdTAwMTG+4qozz9pcdTAwMDDJVZ1cdTAwMTeI+KNVh7xeVFSCMlxiwOJPNG01N3c7W6ksbMP5oLnm64vNb1FcdTAwMDNcdTAwMTJcdNFcdTAwMWNQzFOMXHUwMDFiTVx1MDAxZW+yXGJCXHUwMDAwp9RILWpJnZUuWVx1MDAxYq7Ywd6rZE1gdMlcdTAwMWFcdTAwMThB3oDJ+KVcdTAwMDCbOyeH7eyp3fyW/nFweqK+i3w/asWOxJRcdTAwMDLQZXqKYFx1MDAwMUrgtFx1MDAxMWLOPFSWhJPDm59r/u0qXHUwMDAxdvs3p8Wj01x1MDAxY5rMnbzoraW3blpnXHUwMDBim7WUYFx1MDAwMlx1MDAxNfhLrVx1MDAwNFx1MDAxMJFcdTAwMTUxnJO2U0Ns/JVrjptnxVRd18o1xnJZOEinipuphFx1MDAxM7QxRNBaI2pwSz2HaopcdTAwMDE8gr5WXGa5q/JXn/Mks1KFa57ezd3traV+yPtTuGfFjctKMW4hwPFdeW3ztsVcdTAwMGYzuWo5n8FBrnmbW1xcIYCVbOkpyMxVXHUwMDAztNKcXHUwMDFiXHUwMDFi/2nN6b2Z8Fx1MDAxNITMtSeFVdZcdTAwMTlszoJcdTAwMTHzUFx04HJw7Vx1MDAxYziGXHUwMDE20/hMQVY1XHUwMDA1eYH2PzpcdTAwMDVcdTAwMTFcInLgXHUwMDBiKE92i1xixlx1MDAxZvdK93aE/H7q31xcXHUwMDFmd1x1MDAwYmv3ftpcdTAwMWVVo8aiXHUwMDEzI3HaXHUwMDE1u1lheFjEXHUwMDFlSlx1MDAwMcAjkUelXHUwMDFmpkTDXHL7lLjg/b+6YoNGZuOuVsZu75JlZfv84GLO0TD3Z9lSXHUwMDA0PPpcdTAwMDFcdTAwMDRAVFx1MDAwNIb4XHUwMDBmIEy/6MQrkXElaVx1MDAxYbmRjPAvxlFvSIhIXHUwMDE4mISQ7/vUoVXVoVx1MDAxN7j5w3XIRJaJUrRcbuJnmGNcdJutVvU8ZVtHfnX/JNPM1lxuulHNJl+IXGJtQqCSROdhIZIgPWRMuVx1MDAxZFjQ2OC04KdcdTAwMTJNKtH29fqV8Sv5Sr3Fi6Zlv12y9LzzMu+iRFx1MDAxOF1cdTAwMDAjlVuxiFx1MDAwNYpcdTAwMDVegv30q15cdTAwMDEpcvVWyEBcdTAwMWJcdTAwMTGWXCIp0DPcjXxcdG5ccshFbIjxKUVcdTAwMWYrRS+w80dLkdSRUoSKvJLSc9Sk2SNbNlBLr/XyV6XizV1e/cgmfCVcdTAwMDEw6GlGcWCUskqHXHUwMDAzUitPUpxQsqRcdTAwMDAkLi8jWqlCgFx1MDAxMzi9zJTklrxs71c3dnZz+6U7lUDBXHUwMDEx0etkSPdwXHUwMDFiSX98vZl+0SugN1x1MDAxY6zbeskyJoIrRD/UXHUwMDAx6EXi+1NwXHUwMDEyIDgvkPBHXHUwMDBiXHUwMDBlRG+AqrVGXHUwMDAwruOnPt30UZrt7O83hTpJy8Za5uw+9zPZguOqnIGSXG5BXHUwMDA0NNysYywgXHUwMDA1uMdcdTAwMTJcdTAwMDJjcG9cbsjVL1x1MDAwM1x1MDAxMDqQXGIvtVxmQEfv+oRCgWVyjpVr9lmrv5nL+oY31/X6xe4mXjRcdTAwMTO/qix6buEowp52i5mFXHUwMDE28qNcdTAwMWXwrFvv+nFcdTAwMTW0t42SRVx1MDAwMVO6labdhlNuq1x1MDAwM/c1OFx0T5Bu116SXHUwMDExPVxcx1x1MDAwMyZcdTAwMGJcdTAwMDDeXHUwMDE3o1x1MDAxMoRcXPoq5DNYU7n1XHUwMDAyOJujiv460ylmXHUwMDFh9udm+eLozFx1MDAxN1x1MDAwNzlTgGbSnVxmXHUwMDExJlx1MDAwMVx1MDAxMNFwI1x1MDAxMVhgfYSHrSS14yxOyTORhjViedtGk5eR5ExcdTAwMTRcYlx1MDAwYqFAmbGUXGY1yY1wwLKt+9KLUlx1MDAxNuSV5linpnbrlyO8Ut2/6M9wSv1WO8omjTV3Yjma8VMuYjWa6MdjXCLHxITbbFCjiV8nUDXr+3fn1fIgla1s5Vx1MDAxYq3LuzxL/OxcZoWFR1xcL4XVhlEqMl58hpx0h6SXQKeMU55/dFxc58hcdTAwMTZcdTAwMTbyV2dcXF9cdTAwMWNuX36HUjrTb3/G9Vx1MDAwN8b1QzdPM5Qztlx1MDAxMVVuu2vkMv7I2mxCT2pkg/bc9ijKKKVItsdcdTAwMWQloPBcdTAwMDSS41x1MDAxNNxcIuCbxrpnXHUwMDA2tmWeNW5FVMtcdTAwMTRcdTAwMTnHXHUwMDEx31x1MDAwNi0loqA2MOOe0MNcdEvJiXtcdTAwMTSToOdcdTAwMTmMmFxcXGZcdTAwMTFcdTAwMWWPzFhcZnFG5Zog9tOv2my01y90++u1ZrnWrIw3zH849XZcZl84jOjSwLWSeaApYXLzXHUwMDE0bjhSju6s65hC2/G2Ry6c2lxmlj9MsE1cXLrfLL/cpNn11IEmpZhnXHUwMDE0pztkhit6XHUwMDE4hWKiTZR5o9vcW5I9dONQdqJN9UKvv9FqNGp96vmDVq3ZXHUwMDBm9/CwK9dcXNBX/cJcdTAwMDTh0DVcdTAwMDVfXHUwMDBis0PbfeM4yY9++zKKnuF/nn//959T352KwrX7mUD06Nv+XGL+Ozet2ehHq1xmUr9Solx1MDAxY3/4ZracJZXVXHUwMDE0o1x1MDAwNJQpUIbwL9n4g1x1MDAxY1xi3ONusFx1MDAwMJlcdTAwMTaISyyWt8JDbYZLW4xtXGY4mj/gXHUwMDFlXHUwMDEzlFxmW0roXHRcdTAwMWWT01x0XHUwMDFjKWWiTJnNY15cdTAwMTbOasxcYnzVY78xWW22KVx1MDAxZadcdTAwMTBiNNdXXGJARKLZiO6fOcR40m1cdTAwMDIurGTuwVFjZvPa+HWsXHUwMDE0u0yH1/C1XHRkzckukfM1NnIyUlx0NG5p8/iL1lx1MDAxY1X29zZQ9vzaXHUwMDFl+Nnvt53Dw42bpJNLSqJLd4hBrEIlJIyzXHUwMDBiV+5JXHUwMDFkrVxyMolumONNw3Czn8Ux2uNCXHUwMDE5lNHZ0MScXHK3lFxyKVx1MDAwN4x3SYfesMftn7O+91xyK1x1MDAxMc783iU+2OD2one9/8b0Lf5cdTAwMTRcdTAwMTahaNBo9r7811N29qVX6rbq9f9+35QuRjNcdTAwMTYxpVx1MDAxNemHzIxcdTAwMDc9lFtseq5nzWdcdTAwMDM6oZwljEdGXHUwMDEzrHFcdTAwMWJccihcdTAwMDXjXHUwMDAzOI5HXHUwMDE0XHUwMDEzRltcdTAwMTKS5Y7MkunVJM1cbshccmshg9tCjCa3QHiGh2r+nthcdTAwMGKcnVx1MDAxYptSeG8/5MLY7dm3RD80W1x1MDAxN8ezPNBkcyVKpa1Ukk2meZy6XFxcInPVkbONUFRrZu92PO7OXHUwMDE4Uk5jQIOWXG5IlfREczQhkfwsvW4lXHUwMDEycuVEo1bKg0VcdTAwMDPa/Vx1MDAwNKE8p1x1MDAwMYukM2TRdCY510zMs13BbFx1MDAxZE0onVnjaW5cdGSEXCJcdCZUmsxccvfc7CRcYiWXTGfaeJRmSLe/MFx1MDAxMavk09hMoEfo0I55hzvdXHUwMDA0nkp95LRhnVx1MDAwMVx1MDAwNFx1MDAxN194f05T1Ly5dtpYXHUwMDFhp1x1MDAxMYtI4G6tU1wiXG6plbTTSMRyx2jSXHUwMDBlf8zkMFEsZpu9LO5cdTAwMTjPckKVXHUwMDE0YIxcdTAwMDX6VUzSLHiUhJEyuM50T2/halx1MDAxM1skst3PJKZcdTAwMTdGbjxyXHUwMDEynVTOclxu9vjcNtvLJ5TbXGI4XHUwMDFlKkBKLCWA0eOT6NxQz1x1MDAxM945d0umkl1b3pg8R+5m891CXHUwMDBlklx1MDAwMlLBlEJcdTAwMGbKOSlYJcWhXHUwMDFlrvRcdTAwMTBMdlx1MDAxZshNKtRqrlxc8/d1a47ZyPRcZvdop0ZJXHUwMDE2XHUwMDFjXHUwMDEwfnJrZFx1MDAxOEjFXHTsZlg680rPNvsp33FmQ+ZG+MDtduVOOTko71x1MDAxNlx1MDAxNzFuSS73yFx1MDAwMzVcdHGiUatEbdHAXHUwMDFlvjpcdTAwMDHpObkter3nSNvGnfZqPUel88Fpo8GuQF9tnaX3i93SbvbnbtRcIueJoTaNnuSkXHUwMDE3XHUwMDA0fjfXKMeX70CiNiNcZqM/Llx1MDAwZmVLXHUwMDFjN1NIpt1cdTAwMWHlRuhcZuppS9hMKatcdTAwMTSMzLVcclxc5YruXvNYRvDWcaj4ZVx1MDAwNNVWt3bvxoueRn7ed/xpxumXWl5cdTAwMTCoXHUwMDFiXHUwMDBll1xyKcvA7VpcdTAwMTY73mfDIaHxzqX2iF6F21x1MDAwN5TcitLjhdSopVx1MDAwN4xcdTAwMWOkojcuN+Ct52qDrZ3Ykz7gZKwnnVx1MDAwMNqIkSdtXHUwMDE5XHSHlfOE/6In4ubedWpeKzNbVr6EJuKoP9zjXCJ0f8mowlx1MDAxNN/gJvNcdTAwMWZ2d32li5m5a9uYi2Hu4Vx1MDAxOMVcdTAwMDRnROtcdTAwMDKnjIOBoFx1MDAxYqxcdTAwMWTna5fIMTZ5L1bKxURh2v2kJuBcdTAwMWNlYv54PMHXQrt92Ce4Pd9cdTAwMGXCd638yPWjq/x6XfNv1qfUi19cZn+cXGZccvvTUZI/jIFff/z6f3dfuVAifQ== HeaderTweetTweetTweetTweetFooterTweetTweetTweetTweetTweetTweetTweetTweetFixedFixedColumns (vertical scroll)horizontal scroll

A layout like this is a great starting point. In a real app, you would start replacing each of the placeholders with builtin or custom widgets.

"},{"location":"how-to/design-a-layout/#summary","title":"Summary","text":"

Layout is the first thing you will tackle when building a Textual app. The following tips will help you get started.

  1. Make a sketch (pen and paper is fine).
  2. Work outside in. Start with the entire space of the terminal, add the outermost content first.
  3. Dock fixed widgets. If the content doesn't move or scroll, you probably want to dock it.
  4. Make use of fr for flexible space within layouts.
  5. Use containers to contain other widgets, particularly if they scroll!

If you need further help, we are here to help.

"},{"location":"reference/","title":"Reference","text":"

Welcome to the Textual Reference.

  • CSS Types

    CSS Types are the data types that CSS styles accept in their rules.

    CSS Types Reference

  • Events

    Events are how Textual communicates with your application.

    Events Reference

  • Styles

    All the styles you can use to take your Textual app to the next level.

    Styles Reference

  • Widgets

    How to use the many widgets builtin to Textual.

    Widgets Reference

"},{"location":"styles/","title":"Styles","text":"

A reference to Widget styles.

See the links to the left of the page, or in the hamburger menu (three horizontal bars, top left).

"},{"location":"styles/align/","title":"Align","text":"

The align style aligns children within a container.

"},{"location":"styles/align/#syntax","title":"Syntax","text":"
\nalign: <horizontal> <vertical>;\n\nalign-horizontal: <horizontal>;\nalign-vertical: <vertical>;\n

The align style takes a <horizontal> followed by a <vertical>.

You can also set the alignment for each axis individually with align-horizontal and align-vertical.

"},{"location":"styles/align/#examples","title":"Examples","text":""},{"location":"styles/align/#basic-usage","title":"Basic usage","text":"

This example contains a simple app with two labels centered on the screen with align: center middle;:

Outputalign.pyalign.tcss

AlignApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503Vertical\u00a0alignment\u00a0with\u00a0Textual\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503Take\u00a0note,\u00a0browsers.\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

from textual.app import App\nfrom textual.widgets import Label\nclass AlignApp(App):\ndef compose(self):\nyield Label(\"Vertical alignment with [b]Textual[/]\", classes=\"box\")\nyield Label(\"Take note, browsers.\", classes=\"box\")\napp = AlignApp(css_path=\"align.tcss\")\n
Screen {\nalign: center middle;\n}\n.box {\nwidth: 40;\nheight: 5;\nmargin: 1;\npadding: 1;\nbackground: green;\ncolor: white 90%;\nborder: heavy white;\n}\n
"},{"location":"styles/align/#all-alignments","title":"All alignments","text":"

The next example shows a 3 by 3 grid of containers with text labels. Each label has been aligned differently inside its container, and its text shows its align: ... value.

Outputalign_all.pyalign_all.tcss

AlignAllApp \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502left\u00a0top\u2502\u2502center\u00a0top\u2502\u2502right\u00a0top\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502left\u00a0middle\u2502\u2502center\u00a0middle\u2502\u2502right\u00a0middle\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502left\u00a0bottom\u2502\u2502center\u00a0bottom\u2502\u2502right\u00a0bottom\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

from textual.app import App, ComposeResult\nfrom textual.containers import Container\nfrom textual.widgets import Label\nclass AlignAllApp(App):\n\"\"\"App that illustrates all alignments.\"\"\"\nCSS_PATH = \"align_all.tcss\"\ndef compose(self) -> ComposeResult:\nyield Container(Label(\"left top\"), id=\"left-top\")\nyield Container(Label(\"center top\"), id=\"center-top\")\nyield Container(Label(\"right top\"), id=\"right-top\")\nyield Container(Label(\"left middle\"), id=\"left-middle\")\nyield Container(Label(\"center middle\"), id=\"center-middle\")\nyield Container(Label(\"right middle\"), id=\"right-middle\")\nyield Container(Label(\"left bottom\"), id=\"left-bottom\")\nyield Container(Label(\"center bottom\"), id=\"center-bottom\")\nyield Container(Label(\"right bottom\"), id=\"right-bottom\")\n
#left-top {\n/* align: left top; this is the default value and is implied. */\n}\n#center-top {\nalign: center top;\n}\n#right-top {\nalign: right top;\n}\n#left-middle {\nalign: left middle;\n}\n#center-middle {\nalign: center middle;\n}\n#right-middle {\nalign: right middle;\n}\n#left-bottom {\nalign: left bottom;\n}\n#center-bottom {\nalign: center bottom;\n}\n#right-bottom {\nalign: right bottom;\n}\nScreen {\nlayout: grid;\ngrid-size: 3 3;\ngrid-gutter: 1;\n}\nContainer {\nbackground: $boost;\nborder: solid gray;\nheight: 100%;\n}\nLabel {\nwidth: auto;\nheight: 1;\nbackground: $accent;\n}\n
"},{"location":"styles/align/#css","title":"CSS","text":"
/* Align child widgets to the center. */\nalign: center middle;\n/* Align child widget to the top right */\nalign: right top;\n/* Change the horizontal alignment of the children of a widget */\nalign-horizontal: right;\n/* Change the vertical alignment of the children of a widget */\nalign-vertical: middle;\n
"},{"location":"styles/align/#python","title":"Python","text":"
# Align child widgets to the center\nwidget.styles.align = (\"center\", \"middle\")\n# Align child widgets to the top right\nwidget.styles.align = (\"right\", \"top\")\n# Change the horizontal alignment of the children of a widget\nwidget.styles.align_horizontal = \"right\"\n# Change the vertical alignment of the children of a widget\nwidget.styles.align_vertical = \"middle\"\n
"},{"location":"styles/align/#see-also","title":"See also","text":"
  • content-align to set the alignment of content inside a widget.
  • text-align to set the alignment of text in a widget.
"},{"location":"styles/background/","title":"Background","text":"

The background style sets the background color of a widget.

"},{"location":"styles/background/#syntax","title":"Syntax","text":"
\nbackground: <color> [<percentage>];\n

The background style requires a <color> optionally followed by <percentage> to specify the color's opacity (clamped between 0% and 100%).

"},{"location":"styles/background/#examples","title":"Examples","text":""},{"location":"styles/background/#basic-usage","title":"Basic usage","text":"

This example creates three widgets and applies a different background to each.

Outputbackground.pybackground.tcss

BackgroundApp Widget\u00a01 Widget\u00a02 Widget\u00a03

from textual.app import App\nfrom textual.widgets import Label\nclass BackgroundApp(App):\ndef compose(self):\nyield Label(\"Widget 1\", id=\"static1\")\nyield Label(\"Widget 2\", id=\"static2\")\nyield Label(\"Widget 3\", id=\"static3\")\napp = BackgroundApp(css_path=\"background.tcss\")\n
Label {\nwidth: 100%;\nheight: 1fr;\ncontent-align: center middle;\ncolor: white;\n}\n#static1 {\nbackground: red;\n}\n#static2 {\nbackground: rgb(0, 255, 0);\n}\n#static3 {\nbackground: hsl(240, 100%, 50%);\n}\n
"},{"location":"styles/background/#different-opacity-settings","title":"Different opacity settings","text":"

The next example creates ten widgets laid out side by side to show the effect of setting different percentages for the background color's opacity.

Outputbackground_transparency.pybackground_transparency.tcss

BackgroundTransparencyApp 10%20%30%40%50%60%70%80%90%100%

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass BackgroundTransparencyApp(App):\n\"\"\"Simple app to exemplify different transparency settings.\"\"\"\ndef compose(self) -> ComposeResult:\nyield Static(\"10%\", id=\"t10\")\nyield Static(\"20%\", id=\"t20\")\nyield Static(\"30%\", id=\"t30\")\nyield Static(\"40%\", id=\"t40\")\nyield Static(\"50%\", id=\"t50\")\nyield Static(\"60%\", id=\"t60\")\nyield Static(\"70%\", id=\"t70\")\nyield Static(\"80%\", id=\"t80\")\nyield Static(\"90%\", id=\"t90\")\nyield Static(\"100%\", id=\"t100\")\napp = BackgroundTransparencyApp(css_path=\"background_transparency.tcss\")\n
#t10 {\nbackground: red 10%;\n}\n#t20 {\nbackground: red 20%;\n}\n#t30 {\nbackground: red 30%;\n}\n#t40 {\nbackground: red 40%;\n}\n#t50 {\nbackground: red 50%;\n}\n#t60 {\nbackground: red 60%;\n}\n#t70 {\nbackground: red 70%;\n}\n#t80 {\nbackground: red 80%;\n}\n#t90 {\nbackground: red 90%;\n}\n#t100 {\nbackground: red 100%;\n}\nScreen {\nlayout: horizontal;\n}\nStatic {\nheight: 100%;\nwidth: 1fr;\ncontent-align: center middle;\n}\n
"},{"location":"styles/background/#css","title":"CSS","text":"
/* Blue background */\nbackground: blue;\n/* 20% red background */\nbackground: red 20%;\n/* RGB color */\nbackground: rgb(100, 120, 200);\n/* HSL color */\nbackground: hsl(290, 70%, 80%);\n
"},{"location":"styles/background/#python","title":"Python","text":"

You can use the same syntax as CSS, or explicitly set a Color object for finer-grained control.

# Set blue background\nwidget.styles.background = \"blue\"\n# Set through HSL model\nwidget.styles.background = \"hsl(351,32%,89%)\"\nfrom textual.color import Color\n# Set with a color object by parsing a string\nwidget.styles.background = Color.parse(\"pink\")\nwidget.styles.background = Color.parse(\"#FF00FF\")\n# Set with a color object instantiated directly\nwidget.styles.background = Color(120, 60, 100)\n
"},{"location":"styles/background/#see-also","title":"See also","text":"
  • color to set the color of text in a widget.
"},{"location":"styles/border/","title":"Border","text":"

The border style enables the drawing of a box around a widget.

A border style may also be applied to individual edges with border-top, border-right, border-bottom, and border-left.

Note

border and outline cannot coexist in the same edge of a widget.

"},{"location":"styles/border/#syntax","title":"Syntax","text":"
\nborder: [<border>] [<color>] [<percentage>];\n\nborder-top: [<border>] [<color>] [<percentage>];\nborder-right: [<border>] [<color> [<percentage>]];\nborder-bottom: [<border>] [<color> [<percentage>]];\nborder-left: [<border>] [<color> [<percentage>]];\n

In CSS, the border is set with a border style and a color. Both are optional. An optional percentage may be added to blend the border with the background color.

In Python, the border is set with a tuple of border style and a color.

"},{"location":"styles/border/#border-command","title":"Border command","text":"

The textual CLI has a subcommand which will let you explore the various border types interactively:

textual borders\n

Alternatively, you can see the examples below.

"},{"location":"styles/border/#examples","title":"Examples","text":""},{"location":"styles/border/#basic-usage","title":"Basic usage","text":"

This examples shows three widgets with different border styles.

Outputborder.pyborder.tcss

BorderApp \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502 \u2502My\u00a0border\u00a0is\u00a0solid\u00a0red\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250f\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u2513 \u254f\u254f \u254fMy\u00a0border\u00a0is\u00a0dashed\u00a0green\u254f \u254f\u254f \u2517\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u251b \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258aMy\u00a0border\u00a0is\u00a0tall\u00a0blue\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

from textual.app import App\nfrom textual.widgets import Label\nclass BorderApp(App):\ndef compose(self):\nyield Label(\"My border is solid red\", id=\"label1\")\nyield Label(\"My border is dashed green\", id=\"label2\")\nyield Label(\"My border is tall blue\", id=\"label3\")\napp = BorderApp(css_path=\"border.tcss\")\n
#label1 {\nbackground: red 20%;\ncolor: red;\nborder: solid red;\n}\n#label2 {\nbackground: green 20%;\ncolor: green;\nborder: dashed green;\n}\n#label3 {\nbackground: blue 20%;\ncolor: blue;\nborder: tall blue;\n}\nScreen {\nbackground: white;\n}\nScreen > Label {\nwidth: 100%;\nheight: 5;\ncontent-align: center middle;\ncolor: white;\nmargin: 1;\nbox-sizing: border-box;\n}\n
"},{"location":"styles/border/#all-border-types","title":"All border types","text":"

The next example shows a grid with all the available border types.

Outputborder_all.pyborder_all.tcss

AllBordersApp +------------------+\u250f\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u2513 |ascii|blank\u254fdashed\u254f +------------------+\u2517\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u251b \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2551double\u2551\u2503heavy\u2503hidden/none \u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d\u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2597\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2596\u259b\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u259c hkey\u2590inner\u258c\u258couter\u2590 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u259d\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2598\u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u2502round\u2502\u2502solid\u2502\u258atall\u258e \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u2588\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2588\u258f\u2595\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2588thick\u2588\u258fvkey\u2595\u258ewide\u258a \u2588\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2588\u258f\u2595\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Label\nclass AllBordersApp(App):\ndef compose(self):\nyield Grid(\nLabel(\"ascii\", id=\"ascii\"),\nLabel(\"blank\", id=\"blank\"),\nLabel(\"dashed\", id=\"dashed\"),\nLabel(\"double\", id=\"double\"),\nLabel(\"heavy\", id=\"heavy\"),\nLabel(\"hidden/none\", id=\"hidden\"),\nLabel(\"hkey\", id=\"hkey\"),\nLabel(\"inner\", id=\"inner\"),\nLabel(\"outer\", id=\"outer\"),\nLabel(\"round\", id=\"round\"),\nLabel(\"solid\", id=\"solid\"),\nLabel(\"tall\", id=\"tall\"),\nLabel(\"thick\", id=\"thick\"),\nLabel(\"vkey\", id=\"vkey\"),\nLabel(\"wide\", id=\"wide\"),\n)\napp = AllBordersApp(css_path=\"border_all.tcss\")\n
#ascii {\nborder: ascii $accent;\n}\n#blank {\nborder: blank $accent;\n}\n#dashed {\nborder: dashed $accent;\n}\n#double {\nborder: double $accent;\n}\n#heavy {\nborder: heavy $accent;\n}\n#hidden {\nborder: hidden $accent;\n}\n#hkey {\nborder: hkey $accent;\n}\n#inner {\nborder: inner $accent;\n}\n#outer {\nborder: outer $accent;\n}\n#round {\nborder: round $accent;\n}\n#solid {\nborder: solid $accent;\n}\n#tall {\nborder: tall $accent;\n}\n#thick {\nborder: thick $accent;\n}\n#vkey {\nborder: vkey $accent;\n}\n#wide {\nborder: wide $accent;\n}\nGrid {\ngrid-size: 3 5;\nalign: center middle;\ngrid-gutter: 1 2;\n}\nLabel {\nwidth: 20;\nheight: 3;\ncontent-align: center middle;\n}\n
"},{"location":"styles/border/#borders-and-outlines","title":"Borders and outlines","text":"

The next example makes the difference between border and outline clearer by having three labels side-by-side. They contain the same text, have the same width and height, and are styled exactly the same up to their border and outline styles.

This example also shows that a widget cannot contain both a border and an outline:

Outputoutline_vs_border.pyoutline_vs_border.tcss

OutlineBorderApp \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502ear\u00a0is\u00a0the\u00a0mind-killer.\u2502 \u2502ear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2502 \u2502\u00a0will\u00a0face\u00a0my\u00a0fear.\u2502 \u2502\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2502 \u2502nd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path\u2502 \u2502here\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503I\u00a0must\u00a0not\u00a0fear.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0mind-killer.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2503 \u2503I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2503 \u2503I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2503 \u2503And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502I\u00a0must\u00a0not\u00a0fear.\u2502 \u2502Fear\u00a0is\u00a0the\u00a0mind-killer.\u2502 \u2502Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2502 \u2502I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2502 \u2502I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2502 \u2502And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

from textual.app import App\nfrom textual.widgets import Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass OutlineBorderApp(App):\ndef compose(self):\nyield Label(TEXT, classes=\"outline\")\nyield Label(TEXT, classes=\"border\")\nyield Label(TEXT, classes=\"outline border\")\napp = OutlineBorderApp(css_path=\"outline_vs_border.tcss\")\n
Label {\nheight: 8;\n}\n.outline {\noutline: $error round;\n}\n.border {\nborder: $success heavy;\n}\n
"},{"location":"styles/border/#css","title":"CSS","text":"
/* Set a heavy white border */\nborder: heavy white;\n/* Set a red border on the left */\nborder-left: outer red;\n/* Set a rounded orange border, 50% opacity. */\nborder: round orange 50%;\n
"},{"location":"styles/border/#python","title":"Python","text":"
# Set a heavy white border\nwidget.styles.border = (\"heavy\", \"white\")\n# Set a red border on the left\nwidget.styles.border_left = (\"outer\", \"red\")\n
"},{"location":"styles/border/#see-also","title":"See also","text":"
  • box-sizing to specify how to account for the border in a widget's dimensions.
  • outline to add an outline around the content of a widget.
  • border-title-align to set the title's alignment.
  • border-title-color to set the title's color.
  • border-title-background to set the title's background color.
  • border-title-style to set the title's text style.

  • border-subtitle-align to set the sub-title's alignment.

  • border-subtitle-color to set the sub-title's color.
  • border-subtitle-background to set the sub-title's background color.
  • border-subtitle-style to set the sub-title's text style.
"},{"location":"styles/border_subtitle_align/","title":"Border-subtitle-align","text":"

The border-subtitle-align style sets the horizontal alignment for the border subtitle.

"},{"location":"styles/border_subtitle_align/#syntax","title":"Syntax","text":"
\nborder-subtitle-align: <horizontal>;\n

The border-subtitle-align style takes a <horizontal> that determines where the border subtitle is aligned along the top edge of the border. This means that the border corners are always visible.

"},{"location":"styles/border_subtitle_align/#default","title":"Default","text":"

The default alignment is right.

"},{"location":"styles/border_subtitle_align/#examples","title":"Examples","text":""},{"location":"styles/border_subtitle_align/#basic-usage","title":"Basic usage","text":"

This example shows three labels, each with a different border subtitle alignment:

Outputborder_subtitle_align.pyborder_subtitle_align.tcss

BorderSubtitleAlignApp \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502 \u2502My\u00a0subtitle\u00a0is\u00a0on\u00a0the\u00a0left.\u2502 \u2502\u2502 \u2514\u2500\u00a0<\u00a0Left\u00a0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250f\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u2513 \u254f\u254f \u254fMy\u00a0subtitle\u00a0is\u00a0centered\u254f \u254f\u254f \u2517\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u00a0Centered!\u00a0\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u251b \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258aMy\u00a0subtitle\u00a0is\u00a0on\u00a0the\u00a0right\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u00a0Right\u00a0>\u00a0\u2581\u258e

from textual.app import App\nfrom textual.widgets import Label\nclass BorderSubtitleAlignApp(App):\ndef compose(self):\nlbl = Label(\"My subtitle is on the left.\", id=\"label1\")\nlbl.border_subtitle = \"< Left\"\nyield lbl\nlbl = Label(\"My subtitle is centered\", id=\"label2\")\nlbl.border_subtitle = \"Centered!\"\nyield lbl\nlbl = Label(\"My subtitle is on the right\", id=\"label3\")\nlbl.border_subtitle = \"Right >\"\nyield lbl\napp = BorderSubtitleAlignApp(css_path=\"border_subtitle_align.tcss\")\n
#label1 {\nborder: solid $secondary;\nborder-subtitle-align: left;\n}\n#label2 {\nborder: dashed $secondary;\nborder-subtitle-align: center;\n}\n#label3 {\nborder: tall $secondary;\nborder-subtitle-align: right;\n}\nScreen > Label {\nwidth: 100%;\nheight: 5;\ncontent-align: center middle;\ncolor: white;\nmargin: 1;\nbox-sizing: border-box;\n}\n
"},{"location":"styles/border_subtitle_align/#complete-usage-reference","title":"Complete usage reference","text":"

This example shows all border title and subtitle alignments, together with some examples of how (sub)titles can have custom markup. Open the code tabs to see the details of the code examples.

Outputborder_sub_title_align_all.pyborder_sub_title_align_all.tcss

BorderSubTitleAlignAll \u258fBorder\u00a0title\u2595\u256d\u2500Lef\u2026\u2500\u256e\u2581\u2581\u2581\u2581\u2581Left\u2581\u2581\u2581\u2581\u2581 \u258fThis\u00a0is\u00a0the\u00a0story\u00a0of\u2595\u2502a\u00a0Python\u2502\u258edeveloper\u00a0that\u258a \u258fBorder\u00a0subtitle\u2595\u2570\u2500Cen\u2026\u2500\u256f\u2594\u2594\u2594\u2594\u2594@@@\u2594\u2594\u2594\u2594\u2594\u2594 +--------------+\u2500Title\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 |had\u00a0to\u00a0fill\u00a0up|nine\u00a0labelsand\u00a0ended\u00a0up\u00a0redoing\u00a0it +-Left-------+\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500Subtitle\u2500 \u2500Title,\u00a0but\u00a0really\u00a0looo\u2026\u2500 \u2500Title,\u00a0but\u00a0r\u2026\u2500\u2500Title,\u00a0but\u00a0reall\u2026\u2500 because\u00a0the\u00a0first\u00a0tryhad\u00a0some\u00a0labelsthat\u00a0were\u00a0too\u00a0long. \u2500Subtitle,\u00a0bu\u2026\u2500\u2500Subtitle,\u00a0but\u00a0re\u2026\u2500 \u2500Subtitle,\u00a0but\u00a0really\u00a0l\u2026\u2500

from textual.app import App\nfrom textual.containers import Container, Grid\nfrom textual.widgets import Label\ndef make_label_container(  # (11)!\ntext: str, id: str, border_title: str, border_subtitle: str\n) -> Container:\nlbl = Label(text, id=id)\nlbl.border_title = border_title\nlbl.border_subtitle = border_subtitle\nreturn Container(lbl)\nclass BorderSubTitleAlignAll(App[None]):\ndef compose(self):\nwith Grid():\nyield make_label_container(  # (1)!\n\"This is the story of\",\n\"lbl1\",\n\"[b]Border [i]title[/i][/]\",\n\"[u][r]Border[/r] subtitle[/]\",\n)\nyield make_label_container(  # (2)!\n\"a Python\",\n\"lbl2\",\n\"[b red]Left, but it's loooooooooooong\",\n\"[reverse]Center, but it's loooooooooooong\",\n)\nyield make_label_container(  # (3)!\n\"developer that\",\n\"lbl3\",\n\"[b i on purple]Left[/]\",\n\"[r u white on black]@@@[/]\",\n)\nyield make_label_container(\n\"had to fill up\",\n\"lbl4\",\n\"\",  # (4)!\n\"[link=https://textual.textualize.io]Left[/]\",  # (5)!\n)\nyield make_label_container(  # (6)!\n\"nine labels\", \"lbl5\", \"Title\", \"Subtitle\"\n)\nyield make_label_container(  # (7)!\n\"and ended up redoing it\",\n\"lbl6\",\n\"Title\",\n\"Subtitle\",\n)\nyield make_label_container(  # (8)!\n\"because the first try\",\n\"lbl7\",\n\"Title, but really loooooooooong!\",\n\"Subtitle, but really loooooooooong!\",\n)\nyield make_label_container(  # (9)!\n\"had some labels\",\n\"lbl8\",\n\"Title, but really loooooooooong!\",\n\"Subtitle, but really loooooooooong!\",\n)\nyield make_label_container(  # (10)!\n\"that were too long.\",\n\"lbl9\",\n\"Title, but really loooooooooong!\",\n\"Subtitle, but really loooooooooong!\",\n)\napp = BorderSubTitleAlignAll(css_path=\"border_sub_title_align_all.tcss\")\nif __name__ == \"__main__\":\napp.run()\n
  1. Border (sub)titles can contain nested markup.
  2. Long (sub)titles get truncated and occupy as much space as possible.
  3. (Sub)titles can be stylised with Rich markup.
  4. An empty (sub)title isn't displayed.
  5. The markup can even contain Rich links.
  6. If the widget does not have a border, the title and subtitle are not shown.
  7. When the side borders are not set, the (sub)title will align with the edge of the widget.
  8. The title and subtitle are aligned on the left and very long, so they get truncated and we can still see the rightmost character of the border edge.
  9. The title and subtitle are centered and very long, so they get truncated and are centered with one character of padding on each side.
  10. The title and subtitle are aligned on the right and very long, so they get truncated and we can still see the leftmost character of the border edge.
  11. An auxiliary function to create labels with border title and subtitle.
Grid {\ngrid-size: 3 3;\nalign: center middle;\n}\nContainer {\nwidth: 100%;\nheight: 100%;\nalign: center middle;\n}\n#lbl1 {  /* (1)! */\nborder: vkey $secondary;\n}\n#lbl2 {  /* (2)! */\nborder: round $secondary;\nborder-title-align: right;\nborder-subtitle-align: right;\n}\n#lbl3 {\nborder: wide $secondary;\nborder-title-align: center;\nborder-subtitle-align: center;\n}\n#lbl4 {\nborder: ascii $success;\nborder-title-align: center;  /* (3)! */\nborder-subtitle-align: left;\n}\n#lbl5 {  /* (4)! */\n/* No border = no (sub)title. */\nborder: none $success;\nborder-title-align: center;\nborder-subtitle-align: center;\n}\n#lbl6 {  /* (5)! */\nborder-top: solid $success;\nborder-bottom: solid $success;\n}\n#lbl7 {  /* (6)! */\nborder-top: solid $error;\nborder-bottom: solid $error;\npadding: 1 2;\nborder-subtitle-align: left;\n}\n#lbl8 {\nborder-top: solid $error;\nborder-bottom: solid $error;\nborder-title-align: center;\nborder-subtitle-align: center;\n}\n#lbl9 {\nborder-top: solid $error;\nborder-bottom: solid $error;\nborder-title-align: right;\n}\n
  1. The default alignment for the title is left and the default alignment for the subtitle is right.
  2. Specifying an alignment when the (sub)title is too long has no effect. (Although, it will have an effect if the (sub)title is shortened or if the widget is widened.)
  3. Setting the alignment does not affect empty (sub)titles.
  4. If the border is not set, or set to none/hidden, the (sub)title is not shown.
  5. If the (sub)title alignment is on a side which does not have a border edge, the (sub)title will be flush to that side.
  6. Naturally, (sub)title positioning is affected by padding.
"},{"location":"styles/border_subtitle_align/#css","title":"CSS","text":"
border-subtitle-align: left;\nborder-subtitle-align: center;\nborder-subtitle-align: right;\n
"},{"location":"styles/border_subtitle_align/#python","title":"Python","text":"
widget.styles.border_subtitle_align = \"left\"\nwidget.styles.border_subtitle_align = \"center\"\nwidget.styles.border_subtitle_align = \"right\"\n
"},{"location":"styles/border_subtitle_align/#see-also","title":"See also","text":"
  • border-title-align to set the title's alignment.
  • border-title-color to set the title's color.
  • border-title-background to set the title's background color.
  • border-title-style to set the title's text style.

  • border-subtitle-align to set the sub-title's alignment.

  • border-subtitle-color to set the sub-title's color.
  • border-subtitle-background to set the sub-title's background color.
  • border-subtitle-style to set the sub-title's text style.
"},{"location":"styles/border_subtitle_background/","title":"Border-subtitle-background","text":"

The border-subtitle-background style sets the background color of the border_subtitle.

"},{"location":"styles/border_subtitle_background/#syntax","title":"Syntax","text":"
\nborder-subtitle-background: (<color> | auto) [<percentage>];\n
"},{"location":"styles/border_subtitle_background/#example","title":"Example","text":"

The following examples demonstrates customization of the border color and text style rules.

Outputborder_title_colors.pyborder_title_colors.tcss

BorderTitleApp \u250f\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503Hello,\u00a0World!\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u251b

from textual.app import App, ComposeResult\nfrom textual.widgets import Label\nclass BorderTitleApp(App):\nCSS_PATH = \"border_title_colors.tcss\"\ndef compose(self) -> ComposeResult:\nyield Label(\"Hello, World!\")\ndef on_mount(self) -> None:\nlabel = self.query_one(Label)\nlabel.border_title = \"Textual Rocks\"\nlabel.border_subtitle = \"Textual Rocks\"\nif __name__ == \"__main__\":\napp = BorderTitleApp()\napp.run()\n
Screen {\nalign: center middle;\n}\nLabel {\npadding: 4 8;\nborder: heavy red;\nborder-title-color: green;\nborder-title-background: white;\nborder-title-style: bold;\nborder-subtitle-color: magenta;\nborder-subtitle-background: yellow;\nborder-subtitle-style: italic;\n}\n
"},{"location":"styles/border_subtitle_background/#css","title":"CSS","text":"
border-subtitle-background: blue;\n
"},{"location":"styles/border_subtitle_background/#python","title":"Python","text":"
widget.styles.border_subtitle_background = \"blue\"\n
"},{"location":"styles/border_subtitle_background/#see-also","title":"See also","text":"
  • border-title-align to set the title's alignment.
  • border-title-color to set the title's color.
  • border-title-background to set the title's background color.
  • border-title-style to set the title's text style.

  • border-subtitle-align to set the sub-title's alignment.

  • border-subtitle-color to set the sub-title's color.
  • border-subtitle-background to set the sub-title's background color.
  • border-subtitle-style to set the sub-title's text style.
"},{"location":"styles/border_subtitle_color/","title":"Border-subtitle-color","text":"

The border-subtitle-color style sets the color of the border_subtitle.

"},{"location":"styles/border_subtitle_color/#syntax","title":"Syntax","text":"
\nborder-subtitle-color: (<color> | auto) [<percentage>];\n
"},{"location":"styles/border_subtitle_color/#example","title":"Example","text":"

The following examples demonstrates customization of the border color and text style rules.

Outputborder_title_colors.pyborder_title_colors.tcss

BorderTitleApp \u250f\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503Hello,\u00a0World!\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u251b

from textual.app import App, ComposeResult\nfrom textual.widgets import Label\nclass BorderTitleApp(App):\nCSS_PATH = \"border_title_colors.tcss\"\ndef compose(self) -> ComposeResult:\nyield Label(\"Hello, World!\")\ndef on_mount(self) -> None:\nlabel = self.query_one(Label)\nlabel.border_title = \"Textual Rocks\"\nlabel.border_subtitle = \"Textual Rocks\"\nif __name__ == \"__main__\":\napp = BorderTitleApp()\napp.run()\n
Screen {\nalign: center middle;\n}\nLabel {\npadding: 4 8;\nborder: heavy red;\nborder-title-color: green;\nborder-title-background: white;\nborder-title-style: bold;\nborder-subtitle-color: magenta;\nborder-subtitle-background: yellow;\nborder-subtitle-style: italic;\n}\n
"},{"location":"styles/border_subtitle_color/#css","title":"CSS","text":"
border-subtitle-color: red;\n
"},{"location":"styles/border_subtitle_color/#python","title":"Python","text":"
widget.styles.border_subtitle_color = \"red\"\n
"},{"location":"styles/border_subtitle_color/#see-also","title":"See also","text":"
  • border-title-align to set the title's alignment.
  • border-title-color to set the title's color.
  • border-title-background to set the title's background color.
  • border-title-style to set the title's text style.

  • border-subtitle-align to set the sub-title's alignment.

  • border-subtitle-color to set the sub-title's color.
  • border-subtitle-background to set the sub-title's background color.
  • border-subtitle-style to set the sub-title's text style.
"},{"location":"styles/border_subtitle_style/","title":"Border-subtitle-style","text":"

The border-subtitle-style style sets the text style of the border_subtitle.

"},{"location":"styles/border_subtitle_style/#syntax","title":"Syntax","text":"
\nborder-subtitle-style: <text-style>;\n
"},{"location":"styles/border_subtitle_style/#example","title":"Example","text":"

The following examples demonstrates customization of the border color and text style rules.

Outputborder_title_colors.pyborder_title_colors.tcss

BorderTitleApp \u250f\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503Hello,\u00a0World!\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u251b

from textual.app import App, ComposeResult\nfrom textual.widgets import Label\nclass BorderTitleApp(App):\nCSS_PATH = \"border_title_colors.tcss\"\ndef compose(self) -> ComposeResult:\nyield Label(\"Hello, World!\")\ndef on_mount(self) -> None:\nlabel = self.query_one(Label)\nlabel.border_title = \"Textual Rocks\"\nlabel.border_subtitle = \"Textual Rocks\"\nif __name__ == \"__main__\":\napp = BorderTitleApp()\napp.run()\n
Screen {\nalign: center middle;\n}\nLabel {\npadding: 4 8;\nborder: heavy red;\nborder-title-color: green;\nborder-title-background: white;\nborder-title-style: bold;\nborder-subtitle-color: magenta;\nborder-subtitle-background: yellow;\nborder-subtitle-style: italic;\n}\n
"},{"location":"styles/border_subtitle_style/#css","title":"CSS","text":"
border-subtitle-style: bold underline;\n
"},{"location":"styles/border_subtitle_style/#python","title":"Python","text":"
widget.styles.border_subtitle_style = \"bold underline\"\n
"},{"location":"styles/border_subtitle_style/#see-also","title":"See also","text":"
  • border-title-align to set the title's alignment.
  • border-title-color to set the title's color.
  • border-title-background to set the title's background color.
  • border-title-style to set the title's text style.

  • border-subtitle-align to set the sub-title's alignment.

  • border-subtitle-color to set the sub-title's color.
  • border-subtitle-background to set the sub-title's background color.
  • border-subtitle-style to set the sub-title's text style.
"},{"location":"styles/border_title_align/","title":"Border-title-align","text":"

The border-title-align style sets the horizontal alignment for the border title.

"},{"location":"styles/border_title_align/#syntax","title":"Syntax","text":"
\nborder-title-align: <horizontal>;\n

The border-title-align style takes a <horizontal> that determines where the border title is aligned along the top edge of the border. This means that the border corners are always visible.

"},{"location":"styles/border_title_align/#default","title":"Default","text":"

The default alignment is left.

"},{"location":"styles/border_title_align/#examples","title":"Examples","text":""},{"location":"styles/border_title_align/#basic-usage","title":"Basic usage","text":"

This example shows three labels, each with a different border title alignment:

Outputborder_title_align.pyborder_title_align.tcss

BorderTitleAlignApp \u250c\u2500\u00a0<\u00a0Left\u00a0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502 \u2502My\u00a0title\u00a0is\u00a0on\u00a0the\u00a0left.\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250f\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u00a0Centered!\u00a0\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u2513 \u254f\u254f \u254fMy\u00a0title\u00a0is\u00a0centered\u254f \u254f\u254f \u2517\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u251b \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u00a0Right\u00a0>\u00a0\u2594\u258e \u258a\u258e \u258aMy\u00a0title\u00a0is\u00a0on\u00a0the\u00a0right\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

from textual.app import App\nfrom textual.widgets import Label\nclass BorderTitleAlignApp(App):\ndef compose(self):\nlbl = Label(\"My title is on the left.\", id=\"label1\")\nlbl.border_title = \"< Left\"\nyield lbl\nlbl = Label(\"My title is centered\", id=\"label2\")\nlbl.border_title = \"Centered!\"\nyield lbl\nlbl = Label(\"My title is on the right\", id=\"label3\")\nlbl.border_title = \"Right >\"\nyield lbl\napp = BorderTitleAlignApp(css_path=\"border_title_align.tcss\")\n
#label1 {\nborder: solid $secondary;\nborder-title-align: left;\n}\n#label2 {\nborder: dashed $secondary;\nborder-title-align: center;\n}\n#label3 {\nborder: tall $secondary;\nborder-title-align: right;\n}\nScreen > Label {\nwidth: 100%;\nheight: 5;\ncontent-align: center middle;\ncolor: white;\nmargin: 1;\nbox-sizing: border-box;\n}\n
"},{"location":"styles/border_title_align/#complete-usage-reference","title":"Complete usage reference","text":"

This example shows all border title and subtitle alignments, together with some examples of how (sub)titles can have custom markup. Open the code tabs to see the details of the code examples.

Outputborder_sub_title_align_all.pyborder_sub_title_align_all.tcss

BorderSubTitleAlignAll \u258fBorder\u00a0title\u2595\u256d\u2500Lef\u2026\u2500\u256e\u2581\u2581\u2581\u2581\u2581Left\u2581\u2581\u2581\u2581\u2581 \u258fThis\u00a0is\u00a0the\u00a0story\u00a0of\u2595\u2502a\u00a0Python\u2502\u258edeveloper\u00a0that\u258a \u258fBorder\u00a0subtitle\u2595\u2570\u2500Cen\u2026\u2500\u256f\u2594\u2594\u2594\u2594\u2594@@@\u2594\u2594\u2594\u2594\u2594\u2594 +--------------+\u2500Title\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 |had\u00a0to\u00a0fill\u00a0up|nine\u00a0labelsand\u00a0ended\u00a0up\u00a0redoing\u00a0it +-Left-------+\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500Subtitle\u2500 \u2500Title,\u00a0but\u00a0really\u00a0looo\u2026\u2500 \u2500Title,\u00a0but\u00a0r\u2026\u2500\u2500Title,\u00a0but\u00a0reall\u2026\u2500 because\u00a0the\u00a0first\u00a0tryhad\u00a0some\u00a0labelsthat\u00a0were\u00a0too\u00a0long. \u2500Subtitle,\u00a0bu\u2026\u2500\u2500Subtitle,\u00a0but\u00a0re\u2026\u2500 \u2500Subtitle,\u00a0but\u00a0really\u00a0l\u2026\u2500

from textual.app import App\nfrom textual.containers import Container, Grid\nfrom textual.widgets import Label\ndef make_label_container(  # (11)!\ntext: str, id: str, border_title: str, border_subtitle: str\n) -> Container:\nlbl = Label(text, id=id)\nlbl.border_title = border_title\nlbl.border_subtitle = border_subtitle\nreturn Container(lbl)\nclass BorderSubTitleAlignAll(App[None]):\ndef compose(self):\nwith Grid():\nyield make_label_container(  # (1)!\n\"This is the story of\",\n\"lbl1\",\n\"[b]Border [i]title[/i][/]\",\n\"[u][r]Border[/r] subtitle[/]\",\n)\nyield make_label_container(  # (2)!\n\"a Python\",\n\"lbl2\",\n\"[b red]Left, but it's loooooooooooong\",\n\"[reverse]Center, but it's loooooooooooong\",\n)\nyield make_label_container(  # (3)!\n\"developer that\",\n\"lbl3\",\n\"[b i on purple]Left[/]\",\n\"[r u white on black]@@@[/]\",\n)\nyield make_label_container(\n\"had to fill up\",\n\"lbl4\",\n\"\",  # (4)!\n\"[link=https://textual.textualize.io]Left[/]\",  # (5)!\n)\nyield make_label_container(  # (6)!\n\"nine labels\", \"lbl5\", \"Title\", \"Subtitle\"\n)\nyield make_label_container(  # (7)!\n\"and ended up redoing it\",\n\"lbl6\",\n\"Title\",\n\"Subtitle\",\n)\nyield make_label_container(  # (8)!\n\"because the first try\",\n\"lbl7\",\n\"Title, but really loooooooooong!\",\n\"Subtitle, but really loooooooooong!\",\n)\nyield make_label_container(  # (9)!\n\"had some labels\",\n\"lbl8\",\n\"Title, but really loooooooooong!\",\n\"Subtitle, but really loooooooooong!\",\n)\nyield make_label_container(  # (10)!\n\"that were too long.\",\n\"lbl9\",\n\"Title, but really loooooooooong!\",\n\"Subtitle, but really loooooooooong!\",\n)\napp = BorderSubTitleAlignAll(css_path=\"border_sub_title_align_all.tcss\")\nif __name__ == \"__main__\":\napp.run()\n
  1. Border (sub)titles can contain nested markup.
  2. Long (sub)titles get truncated and occupy as much space as possible.
  3. (Sub)titles can be stylised with Rich markup.
  4. An empty (sub)title isn't displayed.
  5. The markup can even contain Rich links.
  6. If the widget does not have a border, the title and subtitle are not shown.
  7. When the side borders are not set, the (sub)title will align with the edge of the widget.
  8. The title and subtitle are aligned on the left and very long, so they get truncated and we can still see the rightmost character of the border edge.
  9. The title and subtitle are centered and very long, so they get truncated and are centered with one character of padding on each side.
  10. The title and subtitle are aligned on the right and very long, so they get truncated and we can still see the leftmost character of the border edge.
  11. An auxiliary function to create labels with border title and subtitle.
Grid {\ngrid-size: 3 3;\nalign: center middle;\n}\nContainer {\nwidth: 100%;\nheight: 100%;\nalign: center middle;\n}\n#lbl1 {  /* (1)! */\nborder: vkey $secondary;\n}\n#lbl2 {  /* (2)! */\nborder: round $secondary;\nborder-title-align: right;\nborder-subtitle-align: right;\n}\n#lbl3 {\nborder: wide $secondary;\nborder-title-align: center;\nborder-subtitle-align: center;\n}\n#lbl4 {\nborder: ascii $success;\nborder-title-align: center;  /* (3)! */\nborder-subtitle-align: left;\n}\n#lbl5 {  /* (4)! */\n/* No border = no (sub)title. */\nborder: none $success;\nborder-title-align: center;\nborder-subtitle-align: center;\n}\n#lbl6 {  /* (5)! */\nborder-top: solid $success;\nborder-bottom: solid $success;\n}\n#lbl7 {  /* (6)! */\nborder-top: solid $error;\nborder-bottom: solid $error;\npadding: 1 2;\nborder-subtitle-align: left;\n}\n#lbl8 {\nborder-top: solid $error;\nborder-bottom: solid $error;\nborder-title-align: center;\nborder-subtitle-align: center;\n}\n#lbl9 {\nborder-top: solid $error;\nborder-bottom: solid $error;\nborder-title-align: right;\n}\n
  1. The default alignment for the title is left and the default alignment for the subtitle is right.
  2. Specifying an alignment when the (sub)title is too long has no effect. (Although, it will have an effect if the (sub)title is shortened or if the widget is widened.)
  3. Setting the alignment does not affect empty (sub)titles.
  4. If the border is not set, or set to none/hidden, the (sub)title is not shown.
  5. If the (sub)title alignment is on a side which does not have a border edge, the (sub)title will be flush to that side.
  6. Naturally, (sub)title positioning is affected by padding.
"},{"location":"styles/border_title_align/#css","title":"CSS","text":"
border-title-align: left;\nborder-title-align: center;\nborder-title-align: right;\n
"},{"location":"styles/border_title_align/#python","title":"Python","text":"
widget.styles.border_title_align = \"left\"\nwidget.styles.border_title_align = \"center\"\nwidget.styles.border_title_align = \"right\"\n
"},{"location":"styles/border_title_align/#see-also","title":"See also","text":"
  • border-title-align to set the title's alignment.
  • border-title-color to set the title's color.
  • border-title-background to set the title's background color.
  • border-title-style to set the title's text style.

  • border-subtitle-align to set the sub-title's alignment.

  • border-subtitle-color to set the sub-title's color.
  • border-subtitle-background to set the sub-title's background color.
  • border-subtitle-style to set the sub-title's text style.
"},{"location":"styles/border_title_background/","title":"Border-title-background","text":"

The border-title-background style sets the background color of the border_title.

"},{"location":"styles/border_title_background/#syntax","title":"Syntax","text":"
\nborder-title-background: (<color> | auto) [<percentage>];\n
"},{"location":"styles/border_title_background/#example","title":"Example","text":"

The following examples demonstrates customization of the border color and text style rules.

Outputborder_title_colors.pyborder_title_colors.tcss

BorderTitleApp \u250f\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503Hello,\u00a0World!\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u251b

from textual.app import App, ComposeResult\nfrom textual.widgets import Label\nclass BorderTitleApp(App):\nCSS_PATH = \"border_title_colors.tcss\"\ndef compose(self) -> ComposeResult:\nyield Label(\"Hello, World!\")\ndef on_mount(self) -> None:\nlabel = self.query_one(Label)\nlabel.border_title = \"Textual Rocks\"\nlabel.border_subtitle = \"Textual Rocks\"\nif __name__ == \"__main__\":\napp = BorderTitleApp()\napp.run()\n
Screen {\nalign: center middle;\n}\nLabel {\npadding: 4 8;\nborder: heavy red;\nborder-title-color: green;\nborder-title-background: white;\nborder-title-style: bold;\nborder-subtitle-color: magenta;\nborder-subtitle-background: yellow;\nborder-subtitle-style: italic;\n}\n
"},{"location":"styles/border_title_background/#css","title":"CSS","text":"
border-title-background: blue;\n
"},{"location":"styles/border_title_background/#python","title":"Python","text":"
widget.styles.border_title_background = \"blue\"\n
"},{"location":"styles/border_title_background/#see-also","title":"See also","text":"
  • border-title-align to set the title's alignment.
  • border-title-color to set the title's color.
  • border-title-background to set the title's background color.
  • border-title-style to set the title's text style.

  • border-subtitle-align to set the sub-title's alignment.

  • border-subtitle-color to set the sub-title's color.
  • border-subtitle-background to set the sub-title's background color.
  • border-subtitle-style to set the sub-title's text style.
"},{"location":"styles/border_title_color/","title":"Border-title-color","text":"

The border-title-color style sets the color of the border_title.

"},{"location":"styles/border_title_color/#syntax","title":"Syntax","text":"
\nborder-title-color: (<color> | auto) [<percentage>];\n
"},{"location":"styles/border_title_color/#example","title":"Example","text":"

The following examples demonstrates customization of the border color and text style rules.

Outputborder_title_colors.pyborder_title_colors.tcss

BorderTitleApp \u250f\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503Hello,\u00a0World!\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u251b

from textual.app import App, ComposeResult\nfrom textual.widgets import Label\nclass BorderTitleApp(App):\nCSS_PATH = \"border_title_colors.tcss\"\ndef compose(self) -> ComposeResult:\nyield Label(\"Hello, World!\")\ndef on_mount(self) -> None:\nlabel = self.query_one(Label)\nlabel.border_title = \"Textual Rocks\"\nlabel.border_subtitle = \"Textual Rocks\"\nif __name__ == \"__main__\":\napp = BorderTitleApp()\napp.run()\n
Screen {\nalign: center middle;\n}\nLabel {\npadding: 4 8;\nborder: heavy red;\nborder-title-color: green;\nborder-title-background: white;\nborder-title-style: bold;\nborder-subtitle-color: magenta;\nborder-subtitle-background: yellow;\nborder-subtitle-style: italic;\n}\n
"},{"location":"styles/border_title_color/#css","title":"CSS","text":"
border-title-color: red;\n
"},{"location":"styles/border_title_color/#python","title":"Python","text":"
widget.styles.border_title_color = \"red\"\n
"},{"location":"styles/border_title_color/#see-also","title":"See also","text":"
  • border-title-align to set the title's alignment.
  • border-title-color to set the title's color.
  • border-title-background to set the title's background color.
  • border-title-style to set the title's text style.

  • border-subtitle-align to set the sub-title's alignment.

  • border-subtitle-color to set the sub-title's color.
  • border-subtitle-background to set the sub-title's background color.
  • border-subtitle-style to set the sub-title's text style.
"},{"location":"styles/border_title_style/","title":"Border-title-style","text":"

The border-title-style style sets the text style of the border_title.

"},{"location":"styles/border_title_style/#syntax","title":"Syntax","text":"
\nborder-title-style: <text-style>;\n
"},{"location":"styles/border_title_style/#example","title":"Example","text":"

The following examples demonstrates customization of the border color and text style rules.

Outputborder_title_colors.pyborder_title_colors.tcss

BorderTitleApp \u250f\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503Hello,\u00a0World!\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u251b

from textual.app import App, ComposeResult\nfrom textual.widgets import Label\nclass BorderTitleApp(App):\nCSS_PATH = \"border_title_colors.tcss\"\ndef compose(self) -> ComposeResult:\nyield Label(\"Hello, World!\")\ndef on_mount(self) -> None:\nlabel = self.query_one(Label)\nlabel.border_title = \"Textual Rocks\"\nlabel.border_subtitle = \"Textual Rocks\"\nif __name__ == \"__main__\":\napp = BorderTitleApp()\napp.run()\n
Screen {\nalign: center middle;\n}\nLabel {\npadding: 4 8;\nborder: heavy red;\nborder-title-color: green;\nborder-title-background: white;\nborder-title-style: bold;\nborder-subtitle-color: magenta;\nborder-subtitle-background: yellow;\nborder-subtitle-style: italic;\n}\n
"},{"location":"styles/border_title_style/#css","title":"CSS","text":"
border-title-style: bold underline;\n
"},{"location":"styles/border_title_style/#python","title":"Python","text":"
widget.styles.border_title_style = \"bold underline\"\n
"},{"location":"styles/border_title_style/#see-also","title":"See also","text":"
  • border-title-align to set the title's alignment.
  • border-title-color to set the title's color.
  • border-title-background to set the title's background color.
  • border-title-style to set the title's text style.

  • border-subtitle-align to set the sub-title's alignment.

  • border-subtitle-color to set the sub-title's color.
  • border-subtitle-background to set the sub-title's background color.
  • border-subtitle-style to set the sub-title's text style.
"},{"location":"styles/box_sizing/","title":"Box-sizing","text":"

The box-sizing style determines how the width and height of a widget are calculated.

"},{"location":"styles/box_sizing/#syntax","title":"Syntax","text":"
box-sizing: border-box | content-box;\n
"},{"location":"styles/box_sizing/#values","title":"Values","text":"Value Description border-box (default) Padding and border are included in the width and height. If you add padding and/or border the widget will not change in size, but you will have less space for content. content-box Padding and border will increase the size of the widget, leaving the content area unaffected."},{"location":"styles/box_sizing/#example","title":"Example","text":"

Both widgets in this example have the same height (5). The top widget has box-sizing: border-box which means that padding and border reduce the space for content. The bottom widget has box-sizing: content-box which increases the size of the widget to compensate for padding and border.

Outputbox_sizing.pybox_sizing.tcss

BoxSizingApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eI'm\u00a0using\u00a0border-box!\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eI'm\u00a0using\u00a0content-box!\u258a \u258e\u258a \u258e\u258a \u258e\u258a \u258e\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

from textual.app import App\nfrom textual.widgets import Static\nclass BoxSizingApp(App):\ndef compose(self):\nyield Static(\"I'm using border-box!\", id=\"static1\")\nyield Static(\"I'm using content-box!\", id=\"static2\")\napp = BoxSizingApp(css_path=\"box_sizing.tcss\")\n
#static1 {\nbox-sizing: border-box;\n}\n#static2 {\nbox-sizing: content-box;\n}\nScreen {\nbackground: white;\ncolor: black;\n}\nApp Static {\nbackground: blue 20%;\nheight: 5;\nmargin: 2;\npadding: 1;\nborder: wide black;\n}\n
"},{"location":"styles/box_sizing/#css","title":"CSS","text":"
/* Set box sizing to border-box (default) */\nbox-sizing: border-box;\n/* Set box sizing to content-box */\nbox-sizing: content-box;\n
"},{"location":"styles/box_sizing/#python","title":"Python","text":"
# Set box sizing to border-box (default)\nwidget.box_sizing = \"border-box\"\n# Set box sizing to content-box\nwidget.box_sizing = \"content-box\"\n
"},{"location":"styles/box_sizing/#see-also","title":"See also","text":"
  • border to add a border around a widget.
  • padding to add spacing around the content of a widget.
"},{"location":"styles/color/","title":"Color","text":"

The color style sets the text color of a widget.

"},{"location":"styles/color/#syntax","title":"Syntax","text":"
\ncolor: (<color> | auto) [<percentage>];\n

The color style requires a <color> followed by an optional <percentage> to specify the color's opacity.

You can also use the special value of \"auto\" in place of a color. This tells Textual to automatically select either white or black text for best contrast against the background.

"},{"location":"styles/color/#examples","title":"Examples","text":""},{"location":"styles/color/#basic-usage","title":"Basic usage","text":"

This example sets a different text color for each of three different widgets.

Outputcolor.pycolor.tcss

ColorApp I'm\u00a0red! I'm\u00a0rgb(0,\u00a0255,\u00a00)! I'm\u00a0hsl(240,\u00a0100%,\u00a050%)!

from textual.app import App\nfrom textual.widgets import Label\nclass ColorApp(App):\ndef compose(self):\nyield Label(\"I'm red!\", id=\"label1\")\nyield Label(\"I'm rgb(0, 255, 0)!\", id=\"label2\")\nyield Label(\"I'm hsl(240, 100%, 50%)!\", id=\"label3\")\napp = ColorApp(css_path=\"color.tcss\")\n
Label {\nheight: 1fr;\ncontent-align: center middle;\nwidth: 100%;\n}\n#label1 {\ncolor: red;\n}\n#label2 {\ncolor: rgb(0, 255, 0);\n}\n#label3 {\ncolor: hsl(240, 100%, 50%);\n}\n
"},{"location":"styles/color/#auto","title":"Auto","text":"

The next example shows how auto chooses between a lighter or a darker text color to increase the contrast and improve readability.

Outputcolor_auto.pycolor_auto.tcss

ColorApp The\u00a0quick\u00a0brown\u00a0fox\u00a0jumps\u00a0over\u00a0the\u00a0lazy\u00a0dog! The\u00a0quick\u00a0brown\u00a0fox\u00a0jumps\u00a0over\u00a0the\u00a0lazy\u00a0dog! The\u00a0quick\u00a0brown\u00a0fox\u00a0jumps\u00a0over\u00a0the\u00a0lazy\u00a0dog! The\u00a0quick\u00a0brown\u00a0fox\u00a0jumps\u00a0over\u00a0the\u00a0lazy\u00a0dog! The\u00a0quick\u00a0brown\u00a0fox\u00a0jumps\u00a0over\u00a0the\u00a0lazy\u00a0dog!

from textual.app import App\nfrom textual.widgets import Label\nclass ColorApp(App):\ndef compose(self):\nyield Label(\"The quick brown fox jumps over the lazy dog!\", id=\"lbl1\")\nyield Label(\"The quick brown fox jumps over the lazy dog!\", id=\"lbl2\")\nyield Label(\"The quick brown fox jumps over the lazy dog!\", id=\"lbl3\")\nyield Label(\"The quick brown fox jumps over the lazy dog!\", id=\"lbl4\")\nyield Label(\"The quick brown fox jumps over the lazy dog!\", id=\"lbl5\")\napp = ColorApp(css_path=\"color_auto.tcss\")\n
Label {\ncolor: auto 80%;\ncontent-align: center middle;\nheight: 1fr;\nwidth: 100%;\n}\n#lbl1 {\nbackground: red 80%;\n}\n#lbl2 {\nbackground: yellow 80%;\n}\n#lbl3 {\nbackground: blue 80%;\n}\n#lbl4 {\nbackground: pink 80%;\n}\n#lbl5 {\nbackground: green 80%;\n}\n
"},{"location":"styles/color/#css","title":"CSS","text":"
/* Blue text */\ncolor: blue;\n/* 20% red text */\ncolor: red 20%;\n/* RGB color */\ncolor: rgb(100, 120, 200);\n/* Automatically choose color with suitable contrast for readability */\ncolor: auto;\n
"},{"location":"styles/color/#python","title":"Python","text":"

You can use the same syntax as CSS, or explicitly set a Color object.

# Set blue text\nwidget.styles.color = \"blue\"\nfrom textual.color import Color\n# Set with a color object\nwidget.styles.color = Color.parse(\"pink\")\n
"},{"location":"styles/color/#see-also","title":"See also","text":"
  • background to set the background color in a widget.
"},{"location":"styles/content_align/","title":"Content-align","text":"

The content-align style aligns content inside a widget.

"},{"location":"styles/content_align/#syntax","title":"Syntax","text":"
\ncontent-align: <horizontal> <vertical>;\n\ncontent-align-horizontal: <horizontal>;\ncontent-align-vertical: <vertical>;\n

The content-align style takes a <horizontal> followed by a <vertical>.

You can specify the alignment of content on both the horizontal and vertical axes at the same time, or on each of the axis separately. To specify content alignment on a single axis, use the respective style and type:

  • content-align-horizontal takes a <horizontal> and does alignment along the horizontal axis; and
  • content-align-vertical takes a <vertical> and does alignment along the vertical axis.
"},{"location":"styles/content_align/#examples","title":"Examples","text":""},{"location":"styles/content_align/#basic-usage","title":"Basic usage","text":"

This first example shows three labels stacked vertically, each with different content alignments.

Outputcontent_align.pycontent_align.tcss

ContentAlignApp With\u00a0content-align\u00a0you\u00a0can... ...Easily\u00a0align\u00a0content... ...Horizontally\u00a0and\u00a0vertically!

from textual.app import App\nfrom textual.widgets import Label\nclass ContentAlignApp(App):\ndef compose(self):\nyield Label(\"With [i]content-align[/] you can...\", id=\"box1\")\nyield Label(\"...[b]Easily align content[/]...\", id=\"box2\")\nyield Label(\"...Horizontally [i]and[/] vertically!\", id=\"box3\")\napp = ContentAlignApp(css_path=\"content_align.tcss\")\n
#box1 {\ncontent-align: left top;\nbackground: red;\n}\n#box2 {\ncontent-align-horizontal: center;\ncontent-align-vertical: middle;\nbackground: green;\n}\n#box3 {\ncontent-align: right bottom;\nbackground: blue;\n}\nLabel {\nwidth: 100%;\nheight: 1fr;\npadding: 1;\ncolor: white;\n}\n
"},{"location":"styles/content_align/#all-content-alignments","title":"All content alignments","text":"

The next example shows a 3 by 3 grid of labels. Each label has its text aligned differently.

Outputcontent_align_all.pycontent_align_all.tcss

AllContentAlignApp left\u00a0topcenter\u00a0topright\u00a0top left\u00a0middlecenter\u00a0middleright\u00a0middle left\u00a0bottomcenter\u00a0bottomright\u00a0bottom

from textual.app import App\nfrom textual.widgets import Label\nclass AllContentAlignApp(App):\ndef compose(self):\nyield Label(\"left top\", id=\"left-top\")\nyield Label(\"center top\", id=\"center-top\")\nyield Label(\"right top\", id=\"right-top\")\nyield Label(\"left middle\", id=\"left-middle\")\nyield Label(\"center middle\", id=\"center-middle\")\nyield Label(\"right middle\", id=\"right-middle\")\nyield Label(\"left bottom\", id=\"left-bottom\")\nyield Label(\"center bottom\", id=\"center-bottom\")\nyield Label(\"right bottom\", id=\"right-bottom\")\napp = AllContentAlignApp(css_path=\"content_align_all.tcss\")\n
#left-top {\n/* content-align: left top; this is the default implied value. */\n}\n#center-top {\ncontent-align: center top;\n}\n#right-top {\ncontent-align: right top;\n}\n#left-middle {\ncontent-align: left middle;\n}\n#center-middle {\ncontent-align: center middle;\n}\n#right-middle {\ncontent-align: right middle;\n}\n#left-bottom {\ncontent-align: left bottom;\n}\n#center-bottom {\ncontent-align: center bottom;\n}\n#right-bottom {\ncontent-align: right bottom;\n}\nScreen {\nlayout: grid;\ngrid-size: 3 3;\ngrid-gutter: 1;\n}\nLabel {\nwidth: 100%;\nheight: 100%;\nbackground: $primary;\n}\n
"},{"location":"styles/content_align/#css","title":"CSS","text":"
/* Align content in the very center of a widget */\ncontent-align: center middle;\n/* Align content at the top right of a widget */\ncontent-align: right top;\n/* Change the horizontal alignment of the content of a widget */\ncontent-align-horizontal: right;\n/* Change the vertical alignment of the content of a widget */\ncontent-align-vertical: middle;\n
"},{"location":"styles/content_align/#python","title":"Python","text":"
# Align content in the very center of a widget\nwidget.styles.content_align = (\"center\", \"middle\")\n# Align content at the top right of a widget\nwidget.styles.content_align = (\"right\", \"top\")\n# Change the horizontal alignment of the content of a widget\nwidget.styles.content_align_horizontal = \"right\"\n# Change the vertical alignment of the content of a widget\nwidget.styles.content_align_vertical = \"middle\"\n
"},{"location":"styles/content_align/#see-also","title":"See also","text":"
  • align to set the alignment of children widgets inside a container.
  • text-align to set the alignment of text in a widget.
"},{"location":"styles/display/","title":"Display","text":"

The display style defines whether a widget is displayed or not.

"},{"location":"styles/display/#syntax","title":"Syntax","text":"
display: block | none;\n
"},{"location":"styles/display/#values","title":"Values","text":"Value Description block (default) Display the widget as normal. none The widget is not displayed and space will no longer be reserved for it."},{"location":"styles/display/#example","title":"Example","text":"

Note that the second widget is hidden by adding the \"remove\" class which sets the display style to none.

Outputdisplay.pydisplay.tcss

DisplayApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Widget\u00a01\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Widget\u00a03\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

from textual.app import App\nfrom textual.widgets import Static\nclass DisplayApp(App):\ndef compose(self):\nyield Static(\"Widget 1\")\nyield Static(\"Widget 2\", classes=\"remove\")\nyield Static(\"Widget 3\")\napp = DisplayApp(css_path=\"display.tcss\")\n
Screen {\nbackground: green;\n}\nStatic {\nheight: 5;\nbackground: white;\ncolor: blue;\nborder: heavy blue;\n}\nStatic.remove {\ndisplay: none;\n}\n
"},{"location":"styles/display/#css","title":"CSS","text":"
/* Widget is shown */\ndisplay: block;\n/* Widget is not shown */\ndisplay: none;\n
"},{"location":"styles/display/#python","title":"Python","text":"
# Hide the widget\nself.styles.display = \"none\"\n# Show the widget again\nself.styles.display = \"block\"\n

There is also a shortcut to show / hide a widget. The display property on Widget may be set to True or False to show or hide the widget.

# Hide the widget\nwidget.display = False\n# Show the widget\nwidget.display = True\n
"},{"location":"styles/display/#see-also","title":"See also","text":"
  • visibility to specify whether a widget is visible or not.
"},{"location":"styles/dock/","title":"Dock","text":"

The dock style is used to fix a widget to the edge of a container (which may be the entire terminal window).

"},{"location":"styles/dock/#syntax","title":"Syntax","text":"
dock: bottom | left | right | top;\n

The option chosen determines the edge to which the widget is docked.

"},{"location":"styles/dock/#examples","title":"Examples","text":""},{"location":"styles/dock/#basic-usage","title":"Basic usage","text":"

The example below shows a left docked sidebar. Notice that even though the content is scrolled, the sidebar remains fixed.

Outputdock_layout1_sidebar.pydock_layout1_sidebar.tcss

DockLayoutExample SidebarDocking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0 edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0making\u00a0them\u00a0ideal\u00a0 for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0\u2587\u2587 edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0making\u00a0them\u00a0ideal\u00a0 for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0 edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0making\u00a0them\u00a0ideal\u00a0 for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0 edges\u00a0of\u00a0a\u00a0container.

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nTEXT = \"\"\"\\\nDocking a widget removes it from the layout and fixes its position, aligned to either the top, right, bottom, or left edges of a container.\nDocked widgets will not scroll out of view, making them ideal for sticky headers, footers, and sidebars.\n\"\"\"\nclass DockLayoutExample(App):\nCSS_PATH = \"dock_layout1_sidebar.tcss\"\ndef compose(self) -> ComposeResult:\nyield Static(\"Sidebar\", id=\"sidebar\")\nyield Static(TEXT * 10, id=\"body\")\nif __name__ == \"__main__\":\napp = DockLayoutExample()\napp.run()\n
#sidebar {\ndock: left;\nwidth: 15;\nheight: 100%;\ncolor: #0f2b41;\nbackground: dodgerblue;\n}\n
"},{"location":"styles/dock/#advanced-usage","title":"Advanced usage","text":"

The second example shows how one can use full-width or full-height containers to dock labels to the edges of a larger container. The labels will remain in that position (docked) even if the container they are in scrolls horizontally and/or vertically.

Outputdock_all.pydock_all.tcss

DockAllApp \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502top\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502leftright\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502bottom\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

from textual.app import App\nfrom textual.containers import Container\nfrom textual.widgets import Label\nclass DockAllApp(App):\ndef compose(self):\nyield Container(\nContainer(Label(\"left\"), id=\"left\"),\nContainer(Label(\"top\"), id=\"top\"),\nContainer(Label(\"right\"), id=\"right\"),\nContainer(Label(\"bottom\"), id=\"bottom\"),\nid=\"big_container\",\n)\napp = DockAllApp(css_path=\"dock_all.tcss\")\n
#left {\ndock: left;\nheight: 100%;\nwidth: auto;\nalign-vertical: middle;\n}\n#top {\ndock: top;\nheight: auto;\nwidth: 100%;\nalign-horizontal: center;\n}\n#right {\ndock: right;\nheight: 100%;\nwidth: auto;\nalign-vertical: middle;\n}\n#bottom {\ndock: bottom;\nheight: auto;\nwidth: 100%;\nalign-horizontal: center;\n}\nScreen {\nalign: center middle;\n}\n#big_container {\nwidth: 75%;\nheight: 75%;\nborder: round white;\n}\n
"},{"location":"styles/dock/#css","title":"CSS","text":"
dock: bottom;  /* Docks on the bottom edge of the parent container. */\ndock: left;    /* Docks on the   left edge of the parent container. */\ndock: right;   /* Docks on the  right edge of the parent container. */\ndock: top;     /* Docks on the    top edge of the parent container. */\n
"},{"location":"styles/dock/#python","title":"Python","text":"
widget.styles.dock = \"bottom\"  # Dock bottom.\nwidget.styles.dock = \"left\"    # Dock   left.\nwidget.styles.dock = \"right\"   # Dock  right.\nwidget.styles.dock = \"top\"     # Dock    top.\n
"},{"location":"styles/dock/#see-also","title":"See also","text":"
  • The layout guide section on docking.
"},{"location":"styles/height/","title":"Height","text":"

The height style sets a widget's height.

"},{"location":"styles/height/#syntax","title":"Syntax","text":"
\nheight: <scalar>;\n

The height style needs a <scalar> to determine the vertical length of the widget. By default, it sets the height of the content area, but if box-sizing is set to border-box it sets the height of the border area.

"},{"location":"styles/height/#examples","title":"Examples","text":""},{"location":"styles/height/#basic-usage","title":"Basic usage","text":"

This examples creates a widget with a height of 50% of the screen.

Outputheight.pyheight.tcss

HeightApp Widget

from textual.app import App\nfrom textual.widget import Widget\nclass HeightApp(App):\ndef compose(self):\nyield Widget()\napp = HeightApp(css_path=\"height.tcss\")\n
Screen > Widget {\nbackground: green;\nheight: 50%;\ncolor: white;\n}\n
"},{"location":"styles/height/#all-height-formats","title":"All height formats","text":"

The next example creates a series of wide widgets with heights set with different units. Open the CSS file tab to see the comments that explain how each height is computed. (The output includes a vertical ruler on the right to make it easier to check the height of each widget.)

Outputheight_comparison.pyheight_comparison.tcss

HeightComparisonApp #cells\u00b7 \u00b7 \u00b7 #percent\u00b7 \u2022 \u00b7 #w\u00b7 \u00b7 \u00b7 \u2022 #h\u00b7 \u00b7 \u00b7 \u00b7 #vw\u2022 \u00b7 \u00b7 \u00b7 #vh\u00b7 \u2022 #auto\u00b7 #fr1\u00b7 #fr2\u00b7 \u00b7

from textual.app import App\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Label, Placeholder, Static\nclass Ruler(Static):\ndef compose(self):\nruler_text = \"\u00b7\\n\u00b7\\n\u00b7\\n\u00b7\\n\u2022\\n\" * 100\nyield Label(ruler_text)\nclass HeightComparisonApp(App):\ndef compose(self):\nyield VerticalScroll(\nPlaceholder(id=\"cells\"),  # (1)!\nPlaceholder(id=\"percent\"),\nPlaceholder(id=\"w\"),\nPlaceholder(id=\"h\"),\nPlaceholder(id=\"vw\"),\nPlaceholder(id=\"vh\"),\nPlaceholder(id=\"auto\"),\nPlaceholder(id=\"fr1\"),\nPlaceholder(id=\"fr2\"),\n)\nyield Ruler()\napp = HeightComparisonApp(css_path=\"height_comparison.tcss\")\n
  1. The id of the placeholder identifies which unit will be used to set the height of the widget.
#cells {\nheight: 2;       /* (1)! */\n}\n#percent {\nheight: 12.5%;   /* (2)! */\n}\n#w {\nheight: 5w;      /* (3)! */\n}\n#h {\nheight: 12.5h;   /* (4)! */\n}\n#vw {\nheight: 6.25vw;  /* (5)! */\n}\n#vh {\nheight: 12.5vh;  /* (6)! */\n}\n#auto {\nheight: auto;    /* (7)! */\n}\n#fr1 {\nheight: 1fr;     /* (8)! */\n}\n#fr2 {\nheight: 2fr;     /* (9)! */\n}\nScreen {\nlayers: ruler;\noverflow: hidden;\n}\nRuler {\nlayer: ruler;\ndock: right;\nwidth: 1;\nbackground: $accent;\n}\n
  1. This sets the height to 2 lines.
  2. This sets the height to 12.5% of the space made available by the container. The container is 24 lines tall, so 12.5% of 24 is 3.
  3. This sets the height to 5% of the width of the direct container, which is the VerticalScroll container. Because it expands to fit all of the terminal, the width of the VerticalScroll is 80 and 5% of 80 is 4.
  4. This sets the height to 12.5% of the height of the direct container, which is the VerticalScroll container. Because it expands to fit all of the terminal, the height of the VerticalScroll is 24 and 12.5% of 24 is 3.
  5. This sets the height to 6.25% of the viewport width, which is 80. 6.25% of 80 is 5.
  6. This sets the height to 12.5% of the viewport height, which is 24. 12.5% of 24 is 3.
  7. This sets the height of the placeholder to be the optimal size that fits the content without scrolling. Because the content only spans one line, the placeholder has its height set to 1.
  8. This sets the height to 1fr, which means this placeholder will have half the height of a placeholder with 2fr.
  9. This sets the height to 2fr, which means this placeholder will have twice the height of a placeholder with 1fr.
"},{"location":"styles/height/#css","title":"CSS","text":"
/* Explicit cell height */\nheight: 10;\n/* Percentage height */\nheight: 50%;\n/* Automatic height */\nheight: auto\n
"},{"location":"styles/height/#python","title":"Python","text":"
self.styles.height = 10  # Explicit cell height can be an int\nself.styles.height = \"50%\"\nself.styles.height = \"auto\"\n
"},{"location":"styles/height/#see-also","title":"See also","text":"
  • max-height and min-height to limit the height of a widget.
  • width to set the width of a widget.
"},{"location":"styles/layer/","title":"Layer","text":"

The layer style defines the layer a widget belongs to.

"},{"location":"styles/layer/#syntax","title":"Syntax","text":"
\nlayer: <name>;\n

The layer style accepts a <name> that defines the layer this widget belongs to. This <name> must correspond to a <name> that has been defined in a layers style by an ancestor of this widget.

More information on layers can be found in the guide.

Warning

Using a <name> that hasn't been defined in a layers declaration of an ancestor of this widget has no effect.

"},{"location":"styles/layer/#example","title":"Example","text":"

In the example below, #box1 is yielded before #box2. However, since #box1 is on the higher layer, it is drawn on top of #box2.

Outputlayers.pylayers.tcss

LayersExample box1\u00a0(layer\u00a0=\u00a0above) box2\u00a0(layer\u00a0=\u00a0below)

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass LayersExample(App):\nCSS_PATH = \"layers.tcss\"\ndef compose(self) -> ComposeResult:\nyield Static(\"box1 (layer = above)\", id=\"box1\")\nyield Static(\"box2 (layer = below)\", id=\"box2\")\nif __name__ == \"__main__\":\napp = LayersExample()\napp.run()\n
Screen {\nalign: center middle;\nlayers: below above;\n}\nStatic {\nwidth: 28;\nheight: 8;\ncolor: auto;\ncontent-align: center middle;\n}\n#box1 {\nlayer: above;\nbackground: darkcyan;\n}\n#box2 {\nlayer: below;\nbackground: orange;\noffset: 12 6;\n}\n
"},{"location":"styles/layer/#css","title":"CSS","text":"
/* Draw the widget on the layer called 'below' */\nlayer: below;\n
"},{"location":"styles/layer/#python","title":"Python","text":"
# Draw the widget on the layer called 'below'\nwidget.styles.layer = \"below\"\n
"},{"location":"styles/layer/#see-also","title":"See also","text":"
  • The layout guide section on layers.
  • layers to define an ordered set of layers.
"},{"location":"styles/layers/","title":"Layers","text":"

The layers style allows you to define an ordered set of layers.

"},{"location":"styles/layers/#syntax","title":"Syntax","text":"
\nlayers: <name>+;\n

The layers style accepts one or more <name> that define the layers that the widget is aware of, and the order in which they will be painted on the screen.

The values used here can later be referenced using the layer property. The layers defined first in the list are drawn under the layers that are defined later in the list.

More information on layers can be found in the guide.

"},{"location":"styles/layers/#example","title":"Example","text":"

In the example below, #box1 is yielded before #box2. However, since #box1 is on the higher layer, it is drawn on top of #box2.

Outputlayers.pylayers.tcss

LayersExample box1\u00a0(layer\u00a0=\u00a0above) box2\u00a0(layer\u00a0=\u00a0below)

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass LayersExample(App):\nCSS_PATH = \"layers.tcss\"\ndef compose(self) -> ComposeResult:\nyield Static(\"box1 (layer = above)\", id=\"box1\")\nyield Static(\"box2 (layer = below)\", id=\"box2\")\nif __name__ == \"__main__\":\napp = LayersExample()\napp.run()\n
Screen {\nalign: center middle;\nlayers: below above;\n}\nStatic {\nwidth: 28;\nheight: 8;\ncolor: auto;\ncontent-align: center middle;\n}\n#box1 {\nlayer: above;\nbackground: darkcyan;\n}\n#box2 {\nlayer: below;\nbackground: orange;\noffset: 12 6;\n}\n
"},{"location":"styles/layers/#css","title":"CSS","text":"
/* Bottom layer is called 'below', layer above it is called 'above' */\nlayers: below above;\n
"},{"location":"styles/layers/#python","title":"Python","text":"
# Bottom layer is called 'below', layer above it is called 'above'\nwidget.style.layers = (\"below\", \"above\")\n
"},{"location":"styles/layers/#see-also","title":"See also","text":"
  • The layout guide section on layers.
  • layer to set the layer a widget belongs to.
"},{"location":"styles/layout/","title":"Layout","text":"

The layout style defines how a widget arranges its children.

"},{"location":"styles/layout/#syntax","title":"Syntax","text":"
\nlayout: grid | horizontal | vertical;\n

The layout style takes an option that defines how child widgets will be arranged, as per the table shown below.

"},{"location":"styles/layout/#values","title":"Values","text":"Value Description grid Child widgets will be arranged in a grid. horizontal Child widgets will be arranged along the horizontal axis, from left to right. vertical (default) Child widgets will be arranged along the vertical axis, from top to bottom.

See the layout guide for more information.

"},{"location":"styles/layout/#example","title":"Example","text":"

Note how the layout style affects the arrangement of widgets in the example below. To learn more about the grid layout, you can see the layout guide or the grid reference.

Outputlayout.pylayout.tcss

LayoutApp Layout Is Vertical LayoutIsHorizontal

from textual.app import App\nfrom textual.containers import Container\nfrom textual.widgets import Label\nclass LayoutApp(App):\ndef compose(self):\nyield Container(\nLabel(\"Layout\"),\nLabel(\"Is\"),\nLabel(\"Vertical\"),\nid=\"vertical-layout\",\n)\nyield Container(\nLabel(\"Layout\"),\nLabel(\"Is\"),\nLabel(\"Horizontal\"),\nid=\"horizontal-layout\",\n)\napp = LayoutApp(css_path=\"layout.tcss\")\n
#vertical-layout {\nlayout: vertical;\nbackground: darkmagenta;\nheight: auto;\n}\n#horizontal-layout {\nlayout: horizontal;\nbackground: darkcyan;\nheight: auto;\n}\nLabel {\nmargin: 1;\nwidth: 12;\ncolor: black;\nbackground: yellowgreen;\n}\n
"},{"location":"styles/layout/#css","title":"CSS","text":"
layout: horizontal;\n
"},{"location":"styles/layout/#python","title":"Python","text":"
widget.styles.layout = \"horizontal\"\n
"},{"location":"styles/layout/#see-also","title":"See also","text":"
  • Layout guide.
  • Grid reference.
"},{"location":"styles/margin/","title":"Margin","text":"

The margin style specifies spacing around a widget.

"},{"location":"styles/margin/#syntax","title":"Syntax","text":"
\nmargin: <integer>\n      # one value for all edges\n      | <integer> <integer>\n      # top/bot   left/right\n      | <integer> <integer> <integer> <integer>;\n      # top       right     bot       left\n\nmargin-top: <integer>;\nmargin-right: <integer>;\nmargin-bottom: <integer>;\nmargin-left: <integer>;\n

The margin specifies spacing around the four edges of the widget equal to the <integer> specified. The number of values given defines what edges get what margin:

  • 1 <integer> sets the same margin for the four edges of the widget;
  • 2 <integer> set margin for top/bottom and left/right edges, respectively.
  • 4 <integer> set margin for the top, right, bottom, and left edges, respectively.

Tip

To remember the order of the edges affected by the rule margin when it has 4 values, think of a clock. Its hand starts at the top and the goes clockwise: top, right, bottom, left.

Alternatively, margin can be set for each edge individually through the styles margin-top, margin-right, margin-bottom, and margin-left, respectively.

"},{"location":"styles/margin/#examples","title":"Examples","text":""},{"location":"styles/margin/#basic-usage","title":"Basic usage","text":"

In the example below we add a large margin to a label, which makes it move away from the top-left corner of the screen.

Outputmargin.pymargin.tcss

MarginApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eI\u00a0must\u00a0not\u00a0fear.\u258a \u258eFear\u00a0is\u00a0the\u00a0mind-killer.\u258a \u258eFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u258a \u258eI\u00a0will\u00a0face\u00a0my\u00a0fear.\u258a \u258eI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u258a \u258eAnd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0\u258a \u258eits\u00a0path.\u258a \u258eWhere\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0\u258a \u258eremain.\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

from textual.app import App\nfrom textual.widgets import Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass MarginApp(App):\ndef compose(self):\nyield Label(TEXT)\napp = MarginApp(css_path=\"margin.tcss\")\n
Screen {\nbackground: white;\ncolor: black;\n}\nLabel {\nmargin: 4 8;\nbackground: blue 20%;\nborder: blue wide;\nwidth: 100%;\n}\n
"},{"location":"styles/margin/#all-margin-settings","title":"All margin settings","text":"

The next example shows a grid. In each cell, we have a placeholder that has its margins set in different ways.

Outputmargin_all.pymargin_all.tcss

MarginAllApp \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502margin\u2502\u2502margin:\u00a01\u00a0\u2502 \u2502no\u00a0margin\u2502\u2502margin:\u00a01\u2502\u2502:\u00a01\u00a05\u2502\u25021\u00a02\u00a06\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502margin-bottom:\u00a04\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502margin-right:\u00a0\u2502\u2502\u2502\u2502margin-left:\u00a03\u2502 \u2502\u2502\u25023\u2502\u2502\u2502\u2502\u2502 \u2502margin-top:\u00a04\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

from textual.app import App\nfrom textual.containers import Container, Grid\nfrom textual.widgets import Placeholder\nclass MarginAllApp(App):\ndef compose(self):\nyield Grid(\nContainer(Placeholder(\"no margin\", id=\"p1\"), classes=\"bordered\"),\nContainer(Placeholder(\"margin: 1\", id=\"p2\"), classes=\"bordered\"),\nContainer(Placeholder(\"margin: 1 5\", id=\"p3\"), classes=\"bordered\"),\nContainer(Placeholder(\"margin: 1 1 2 6\", id=\"p4\"), classes=\"bordered\"),\nContainer(Placeholder(\"margin-top: 4\", id=\"p5\"), classes=\"bordered\"),\nContainer(Placeholder(\"margin-right: 3\", id=\"p6\"), classes=\"bordered\"),\nContainer(Placeholder(\"margin-bottom: 4\", id=\"p7\"), classes=\"bordered\"),\nContainer(Placeholder(\"margin-left: 3\", id=\"p8\"), classes=\"bordered\"),\n)\napp = MarginAllApp(css_path=\"margin_all.tcss\")\n
Screen {\nbackground: $background;\n}\nGrid {\ngrid-size: 4;\ngrid-gutter: 1 2;\n}\nPlaceholder {\nwidth: 100%;\nheight: 100%;\n}\nContainer {\nwidth: 100%;\nheight: 100%;\n}\n.bordered {\nborder: white round;\n}\n#p1 {\n/* default is no margin */\n}\n#p2 {\nmargin: 1;\n}\n#p3 {\nmargin: 1 5;\n}\n#p4 {\nmargin: 1 1 2 6;\n}\n#p5 {\nmargin-top: 4;\n}\n#p6 {\nmargin-right: 3;\n}\n#p7 {\nmargin-bottom: 4;\n}\n#p8 {\nmargin-left: 3;\n}\n
"},{"location":"styles/margin/#css","title":"CSS","text":"
/* Set margin of 1 around all edges */\nmargin: 1;\n/* Set margin of 2 on the top and bottom edges, and 4 on the left and right */\nmargin: 2 4;\n/* Set margin of 1 on the top, 2 on the right,\n3 on the bottom, and 4 on the left */\nmargin: 1 2 3 4;\nmargin-top: 1;\nmargin-right: 2;\nmargin-bottom: 3;\nmargin-left: 4;\n
"},{"location":"styles/margin/#python","title":"Python","text":"

Python does not provide the properties margin-top, margin-right, margin-bottom, and margin-left. However, you can set the margin to a single integer, a tuple of 2 integers, or a tuple of 4 integers:

# Set margin of 1 around all edges\nwidget.styles.margin = 1\n# Set margin of 2 on the top and bottom edges, and 4 on the left and right\nwidget.styles.margin = (2, 4)\n# Set margin of 1 on top, 2 on the right, 3 on the bottom, and 4 on the left\nwidget.styles.margin = (1, 2, 3, 4)\n
"},{"location":"styles/margin/#see-also","title":"See also","text":"
  • padding to add spacing around the content of a widget.
"},{"location":"styles/max_height/","title":"Max-height","text":"

The max-height style sets a maximum height for a widget.

"},{"location":"styles/max_height/#syntax","title":"Syntax","text":"
\nmax-height: <scalar>;\n

The max-height style accepts a <scalar> that defines an upper bound for the height of a widget. That is, the height of a widget is never allowed to exceed max-height.

"},{"location":"styles/max_height/#example","title":"Example","text":"

The example below shows some placeholders that were defined to span vertically from the top edge of the terminal to the bottom edge. Then, we set max-height individually on each placeholder.

Outputmax_height.pymax_height.tcss

MaxHeightApp max-height:\u00a010w max-height:\u00a010 max-height:\u00a050% max-height:\u00a0999

from textual.app import App\nfrom textual.containers import Horizontal\nfrom textual.widgets import Placeholder\nclass MaxHeightApp(App):\ndef compose(self):\nyield Horizontal(\nPlaceholder(\"max-height: 10w\", id=\"p1\"),\nPlaceholder(\"max-height: 999\", id=\"p2\"),\nPlaceholder(\"max-height: 50%\", id=\"p3\"),\nPlaceholder(\"max-height: 10\", id=\"p4\"),\n)\napp = MaxHeightApp(css_path=\"max_height.tcss\")\n
Horizontal {\nheight: 100%;\nwidth: 100%;\n}\nPlaceholder {\nheight: 100%;\nwidth: 1fr;\n}\n#p1 {\nmax-height: 10w;\n}\n#p2 {\nmax-height: 999;  /* (1)! */\n}\n#p3 {\nmax-height: 50%;\n}\n#p4 {\nmax-height: 10;\n}\n
  1. This won't affect the placeholder because its height is less than the maximum height.
"},{"location":"styles/max_height/#css","title":"CSS","text":"
/* Set the maximum height to 10 rows */\nmax-height: 10;\n/* Set the maximum height to 25% of the viewport height */\nmax-height: 25vh;\n
"},{"location":"styles/max_height/#python","title":"Python","text":"
# Set the maximum height to 10 rows\nwidget.styles.max_height = 10\n# Set the maximum height to 25% of the viewport height\nwidget.styles.max_height = \"25vh\"\n
"},{"location":"styles/max_height/#see-also","title":"See also","text":"
  • min-height to set a lower bound on the height of a widget.
  • height to set the height of a widget.
"},{"location":"styles/max_width/","title":"Max-width","text":"

The max-width style sets a maximum width for a widget.

"},{"location":"styles/max_width/#syntax","title":"Syntax","text":"
\nmax-width: <scalar>;\n

The max-width style accepts a <scalar> that defines an upper bound for the width of a widget. That is, the width of a widget is never allowed to exceed max-width.

"},{"location":"styles/max_width/#example","title":"Example","text":"

The example below shows some placeholders that were defined to span horizontally from the left edge of the terminal to the right edge. Then, we set max-width individually on each placeholder.

Outputmax_width.pymax_width.tcss

MaxWidthApp max-width:\u00a0 50h max-width:\u00a0999 max-width:\u00a050% max-width:\u00a030

from textual.app import App\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Placeholder\nclass MaxWidthApp(App):\ndef compose(self):\nyield VerticalScroll(\nPlaceholder(\"max-width: 50h\", id=\"p1\"),\nPlaceholder(\"max-width: 999\", id=\"p2\"),\nPlaceholder(\"max-width: 50%\", id=\"p3\"),\nPlaceholder(\"max-width: 30\", id=\"p4\"),\n)\napp = MaxWidthApp(css_path=\"max_width.tcss\")\n
Horizontal {\nheight: 100%;\nwidth: 100%;\n}\nPlaceholder {\nwidth: 100%;\nheight: 1fr;\n}\n#p1 {\nmax-width: 50h;\n}\n#p2 {\nmax-width: 999;  /* (1)! */\n}\n#p3 {\nmax-width: 50%;\n}\n#p4 {\nmax-width: 30;\n}\n
  1. This won't affect the placeholder because its width is less than the maximum width.
"},{"location":"styles/max_width/#css","title":"CSS","text":"
/* Set the maximum width to 10 rows */\nmax-width: 10;\n/* Set the maximum width to 25% of the viewport width */\nmax-width: 25vw;\n
"},{"location":"styles/max_width/#python","title":"Python","text":"
# Set the maximum width to 10 rows\nwidget.styles.max_width = 10\n# Set the maximum width to 25% of the viewport width\nwidget.styles.max_width = \"25vw\"\n
"},{"location":"styles/max_width/#see-also","title":"See also","text":"
  • min-width to set a lower bound on the width of a widget.
  • width to set the width of a widget.
"},{"location":"styles/min_height/","title":"Min-height","text":"

The min-height style sets a minimum height for a widget.

"},{"location":"styles/min_height/#syntax","title":"Syntax","text":"
\nmin-height: <scalar>;\n

The min-height style accepts a <scalar> that defines a lower bound for the height of a widget. That is, the height of a widget is never allowed to be under min-height.

"},{"location":"styles/min_height/#example","title":"Example","text":"

The example below shows some placeholders with their height set to 50%. Then, we set min-height individually on each placeholder.

Outputmin_height.pymin_height.tcss

MinHeightApp min-height:\u00a025% min-height:\u00a075% min-height:\u00a030 min-height:\u00a040w \u2583\u2583

from textual.app import App\nfrom textual.containers import Horizontal\nfrom textual.widgets import Placeholder\nclass MinHeightApp(App):\ndef compose(self):\nyield Horizontal(\nPlaceholder(\"min-height: 25%\", id=\"p1\"),\nPlaceholder(\"min-height: 75%\", id=\"p2\"),\nPlaceholder(\"min-height: 30\", id=\"p3\"),\nPlaceholder(\"min-height: 40w\", id=\"p4\"),\n)\napp = MinHeightApp(css_path=\"min_height.tcss\")\n
Horizontal {\nheight: 100%;\nwidth: 100%;\noverflow-y: auto;\n}\nPlaceholder {\nwidth: 1fr;\nheight: 50%;\n}\n#p1 {\nmin-height: 25%;  /* (1)! */\n}\n#p2 {\nmin-height: 75%;\n}\n#p3 {\nmin-height: 30;\n}\n#p4 {\nmin-height: 40w;\n}\n
  1. This won't affect the placeholder because its height is larger than the minimum height.
"},{"location":"styles/min_height/#css","title":"CSS","text":"
/* Set the minimum height to 10 rows */\nmin-height: 10;\n/* Set the minimum height to 25% of the viewport height */\nmin-height: 25vh;\n
"},{"location":"styles/min_height/#python","title":"Python","text":"
# Set the minimum height to 10 rows\nwidget.styles.min_height = 10\n# Set the minimum height to 25% of the viewport height\nwidget.styles.min_height = \"25vh\"\n
"},{"location":"styles/min_height/#see-also","title":"See also","text":"
  • max-height to set an upper bound on the height of a widget.
  • height to set the height of a widget.
"},{"location":"styles/min_width/","title":"Min-width","text":"

The min-width style sets a minimum width for a widget.

"},{"location":"styles/min_width/#syntax","title":"Syntax","text":"
\nmin-width: <scalar>;\n

The min-width style accepts a <scalar> that defines a lower bound for the width of a widget. That is, the width of a widget is never allowed to be under min-width.

"},{"location":"styles/min_width/#example","title":"Example","text":"

The example below shows some placeholders with their width set to 50%. Then, we set min-width individually on each placeholder.

Outputmin_width.pymin_width.tcss

MinWidthApp min-width:\u00a025% min-width:\u00a075% min-width:\u00a0100 min-width:\u00a0400h

from textual.app import App\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Placeholder\nclass MinWidthApp(App):\ndef compose(self):\nyield VerticalScroll(\nPlaceholder(\"min-width: 25%\", id=\"p1\"),\nPlaceholder(\"min-width: 75%\", id=\"p2\"),\nPlaceholder(\"min-width: 100\", id=\"p3\"),\nPlaceholder(\"min-width: 400h\", id=\"p4\"),\n)\napp = MinWidthApp(css_path=\"min_width.tcss\")\n
VerticalScroll {\nheight: 100%;\nwidth: 100%;\noverflow-x: auto;\n}\nPlaceholder {\nheight: 1fr;\nwidth: 50%;\n}\n#p1 {\nmin-width: 25%;\n/* (1)! */\n}\n#p2 {\nmin-width: 75%;\n}\n#p3 {\nmin-width: 100;\n}\n#p4 {\nmin-width: 400h;\n}\n
  1. This won't affect the placeholder because its width is larger than the minimum width.
"},{"location":"styles/min_width/#css","title":"CSS","text":"
/* Set the minimum width to 10 rows */\nmin-width: 10;\n/* Set the minimum width to 25% of the viewport width */\nmin-width: 25vw;\n
"},{"location":"styles/min_width/#python","title":"Python","text":"
# Set the minimum width to 10 rows\nwidget.styles.min_width = 10\n# Set the minimum width to 25% of the viewport width\nwidget.styles.min_width = \"25vw\"\n
"},{"location":"styles/min_width/#see-also","title":"See also","text":"
  • max-width to set an upper bound on the width of a widget.
  • width to set the width of a widget.
"},{"location":"styles/offset/","title":"Offset","text":"

The offset style defines an offset for the position of the widget.

"},{"location":"styles/offset/#syntax","title":"Syntax","text":"
\noffset: <scalar> <scalar>;\n\noffset-x: <scalar>;\noffset-y: <scalar>\n

The two <scalar> in the offset define, respectively, the offsets in the horizontal and vertical axes for the widget.

To specify an offset along a single axis, you can use offset-x and offset-y.

"},{"location":"styles/offset/#example","title":"Example","text":"

In this example, we have 3 widgets with differing offsets.

Outputoffset.pyoffset.tcss

OffsetApp \u258c\u2590 \u258cChani\u00a0(offset\u00a00\u00a0\u2590 \u259b\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u259c\u258c-3)\u2590 \u258c\u2590\u258c\u2590 \u258c\u2590\u258c\u2590 \u258c\u2590\u258c\u2590 \u258cPaul\u00a0(offset\u00a08\u00a02)\u2590\u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259f \u258c\u2590 \u258c\u2590 \u258c\u2590 \u258c\u259b\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u259c \u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u258c\u2590 \u258c\u2590 \u258c\u2590 \u258cDuncan\u00a0(offset\u00a04\u00a0\u2590 \u258c10)\u2590 \u258c\u2590 \u258c\u2590 \u258c\u2590 \u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259f

from textual.app import App\nfrom textual.widgets import Label\nclass OffsetApp(App):\ndef compose(self):\nyield Label(\"Paul (offset 8 2)\", classes=\"paul\")\nyield Label(\"Duncan (offset 4 10)\", classes=\"duncan\")\nyield Label(\"Chani (offset 0 -3)\", classes=\"chani\")\napp = OffsetApp(css_path=\"offset.tcss\")\n
Screen {\nbackground: white;\ncolor: black;\nlayout: horizontal;\n}\nLabel {\nwidth: 20;\nheight: 10;\ncontent-align: center middle;\n}\n.paul {\noffset: 8 2;\nbackground: red 20%;\nborder: outer red;\ncolor: red;\n}\n.duncan {\noffset: 4 10;\nbackground: green 20%;\nborder: outer green;\ncolor: green;\n}\n.chani {\noffset: 0 -3;\nbackground: blue 20%;\nborder: outer blue;\ncolor: blue;\n}\n
"},{"location":"styles/offset/#css","title":"CSS","text":"
/* Move the widget 8 cells in the x direction and 2 in the y direction */\noffset: 8 2;\n/* Move the widget 4 cells in the x direction\noffset-x: 4;\n/* Move the widget -3 cells in the y direction\noffset-y: -3;\n
"},{"location":"styles/offset/#python","title":"Python","text":"

You cannot change programmatically the offset for a single axis. You have to set the two axes at the same time.

# Move the widget 2 cells in the x direction, and 4 in the y direction.\nwidget.styles.offset = (2, 4)\n
"},{"location":"styles/offset/#see-also","title":"See also","text":"
  • The layout guide section on offsets.
"},{"location":"styles/opacity/","title":"Opacity","text":"

The opacity style sets the opacity of a widget.

While terminals are not capable of true opacity, Textual can create an approximation by blending widgets with their background color.

"},{"location":"styles/opacity/#syntax","title":"Syntax","text":"
\nopacity: <number> | <percentage>;\n

The opacity of a widget can be set as a <number> or a <percentage>. If given as a number, then opacity should be a value between 0 and 1, where 0 is the background color and 1 is fully opaque. If given as a percentage, 0% is the background color and 100% is fully opaque.

Typically, if you set this value it would be somewhere between the two extremes. For instance, setting the opacity of a widget to 70% will make it appear dimmer than surrounding widgets, which could be used to display a disabled state.

"},{"location":"styles/opacity/#example","title":"Example","text":"

This example shows, from top to bottom, increasing opacity values for a label with a border and some text. When the opacity is zero, all we see is the (black) background.

Outputopacity.pyopacity.tcss

OpacityApp \u259b\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u259c \u258copacity:\u00a00%\u2590 \u258c\u2590 \u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259f \u259b\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u259c \u258c\u2590 \u258copacity:\u00a025%\u2590 \u258c\u2590 \u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259f \u259b\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u259c \u258c\u2590 \u258copacity:\u00a050%\u2590 \u258c\u2590 \u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259f \u259b\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u259c \u258c\u2590 \u258copacity:\u00a075%\u2590 \u258c\u2590 \u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259f \u259b\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u259c \u258c\u2590 \u258copacity:\u00a0100%\u2590 \u258c\u2590 \u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259f

from textual.app import App\nfrom textual.widgets import Label\nclass OpacityApp(App):\ndef compose(self):\nyield Label(\"opacity: 0%\", id=\"zero-opacity\")\nyield Label(\"opacity: 25%\", id=\"quarter-opacity\")\nyield Label(\"opacity: 50%\", id=\"half-opacity\")\nyield Label(\"opacity: 75%\", id=\"three-quarter-opacity\")\nyield Label(\"opacity: 100%\", id=\"full-opacity\")\napp = OpacityApp(css_path=\"opacity.tcss\")\n
#zero-opacity {\nopacity: 0%;\n}\n#quarter-opacity {\nopacity: 25%;\n}\n#half-opacity {\nopacity: 50%;\n}\n#three-quarter-opacity {\nopacity: 75%;\n}\n#full-opacity {\nopacity: 100%;\n}\nScreen {\nbackground: black;\n}\nLabel {\nwidth: 100%;\nheight: 1fr;\nborder: outer dodgerblue;\nbackground: lightseagreen;\ncontent-align: center middle;\ntext-style: bold;\n}\n
"},{"location":"styles/opacity/#css","title":"CSS","text":"
/* Fade the widget to 50% against its parent's background */\nopacity: 50%;\n
"},{"location":"styles/opacity/#python","title":"Python","text":"
# Fade the widget to 50% against its parent's background\nwidget.styles.opacity = \"50%\"\n
"},{"location":"styles/opacity/#see-also","title":"See also","text":"
  • text-opacity to blend the color of a widget's content with its background color.
"},{"location":"styles/outline/","title":"Outline","text":"

The outline style enables the drawing of a box around the content of a widget, which means the outline is drawn over the content area.

Note

border and outline cannot coexist in the same edge of a widget.

"},{"location":"styles/outline/#syntax","title":"Syntax","text":"
\noutline: [<border>] [<color>];\n\noutline-top: [<border>] [<color>];\noutline-right: [<border>] [<color>];\noutline-bottom: [<border>] [<color>];\noutline-left: [<border>] [<color>];\n

The style outline accepts an optional <border> that sets the visual style of the widget outline and an optional <color> to set the color of the outline.

Unlike the style border, the frame of the outline is drawn over the content area of the widget. This rule can be useful to add temporary emphasis on the content of a widget, if you want to draw the user's attention to it.

"},{"location":"styles/outline/#border-command","title":"Border command","text":"

The textual CLI has a subcommand which will let you explore the various border types interactively, when applied to the CSS rule border:

textual borders\n
"},{"location":"styles/outline/#examples","title":"Examples","text":""},{"location":"styles/outline/#basic-usage","title":"Basic usage","text":"

This example shows a widget with an outline. Note how the outline occludes the text area.

Outputoutline.pyoutline.tcss

OutlineApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eear\u00a0is\u00a0the\u00a0mind-killer.\u258a \u258eear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u258a \u258e\u00a0will\u00a0face\u00a0my\u00a0fear.\u258a \u258e\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u258a \u258end\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u258a \u258eath.\u258a \u258ehere\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

from textual.app import App\nfrom textual.widgets import Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass OutlineApp(App):\ndef compose(self):\nyield Label(TEXT)\napp = OutlineApp(css_path=\"outline.tcss\")\n
Screen {\nbackground: white;\ncolor: black;\n}\nLabel {\nmargin: 4 8;\nbackground: green 20%;\noutline: wide green;\nwidth: 100%;\n}\n
"},{"location":"styles/outline/#all-outline-types","title":"All outline types","text":"

The next example shows a grid with all the available outline types.

Outputoutline_all.pyoutline_all.tcss

AllOutlinesApp +------------------+\u250f\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u2513 |ascii|blank\u254fdashed\u254f +------------------+\u2517\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u251b \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2551double\u2551\u2503heavy\u2503hidden/none \u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d\u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2597\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2596 hkey\u2590inner\u258cnone \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u259d\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2598 \u259b\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u259c\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u258couter\u2590\u2502round\u2502\u2502solid\u2502 \u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258f\u2595\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258atall\u258e\u258fvkey\u2595\u258ewide\u258a \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258f\u2595\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Label\nclass AllOutlinesApp(App):\ndef compose(self):\nyield Grid(\nLabel(\"ascii\", id=\"ascii\"),\nLabel(\"blank\", id=\"blank\"),\nLabel(\"dashed\", id=\"dashed\"),\nLabel(\"double\", id=\"double\"),\nLabel(\"heavy\", id=\"heavy\"),\nLabel(\"hidden/none\", id=\"hidden\"),\nLabel(\"hkey\", id=\"hkey\"),\nLabel(\"inner\", id=\"inner\"),\nLabel(\"none\", id=\"none\"),\nLabel(\"outer\", id=\"outer\"),\nLabel(\"round\", id=\"round\"),\nLabel(\"solid\", id=\"solid\"),\nLabel(\"tall\", id=\"tall\"),\nLabel(\"vkey\", id=\"vkey\"),\nLabel(\"wide\", id=\"wide\"),\n)\napp = AllOutlinesApp(css_path=\"outline_all.tcss\")\n
#ascii {\noutline: ascii $accent;\n}\n#blank {\noutline: blank $accent;\n}\n#dashed {\noutline: dashed $accent;\n}\n#double {\noutline: double $accent;\n}\n#heavy {\noutline: heavy $accent;\n}\n#hidden {\noutline: hidden $accent;\n}\n#hkey {\noutline: hkey $accent;\n}\n#inner {\noutline: inner $accent;\n}\n#none {\noutline: none $accent;\n}\n#outer {\noutline: outer $accent;\n}\n#round {\noutline: round $accent;\n}\n#solid {\noutline: solid $accent;\n}\n#tall {\noutline: tall $accent;\n}\n#vkey {\noutline: vkey $accent;\n}\n#wide {\noutline: wide $accent;\n}\nGrid {\ngrid-size: 3 5;\nalign: center middle;\ngrid-gutter: 1 2;\n}\nLabel {\nwidth: 20;\nheight: 3;\ncontent-align: center middle;\n}\n
"},{"location":"styles/outline/#borders-and-outlines","title":"Borders and outlines","text":"

The next example makes the difference between border and outline clearer by having three labels side-by-side. They contain the same text, have the same width and height, and are styled exactly the same up to their border and outline styles.

This example also shows that a widget cannot contain both a border and an outline:

Outputoutline_vs_border.pyoutline_vs_border.tcss

OutlineBorderApp \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502ear\u00a0is\u00a0the\u00a0mind-killer.\u2502 \u2502ear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2502 \u2502\u00a0will\u00a0face\u00a0my\u00a0fear.\u2502 \u2502\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2502 \u2502nd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path\u2502 \u2502here\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503I\u00a0must\u00a0not\u00a0fear.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0mind-killer.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2503 \u2503I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2503 \u2503I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2503 \u2503And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502I\u00a0must\u00a0not\u00a0fear.\u2502 \u2502Fear\u00a0is\u00a0the\u00a0mind-killer.\u2502 \u2502Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2502 \u2502I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2502 \u2502I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2502 \u2502And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

from textual.app import App\nfrom textual.widgets import Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass OutlineBorderApp(App):\ndef compose(self):\nyield Label(TEXT, classes=\"outline\")\nyield Label(TEXT, classes=\"border\")\nyield Label(TEXT, classes=\"outline border\")\napp = OutlineBorderApp(css_path=\"outline_vs_border.tcss\")\n
Label {\nheight: 8;\n}\n.outline {\noutline: $error round;\n}\n.border {\nborder: $success heavy;\n}\n
"},{"location":"styles/outline/#css","title":"CSS","text":"
/* Set a heavy white outline */\noutline:heavy white;\n/* set a red outline on the left */\noutline-left:outer red;\n
"},{"location":"styles/outline/#python","title":"Python","text":"
# Set a heavy white outline\nwidget.outline = (\"heavy\", \"white\")\n# Set a red outline on the left\nwidget.outline_left = (\"outer\", \"red\")\n
"},{"location":"styles/outline/#see-also","title":"See also","text":"
  • border to add a border around a widget.
"},{"location":"styles/overflow/","title":"Overflow","text":"

The overflow style specifies if and when scrollbars should be displayed.

"},{"location":"styles/overflow/#syntax","title":"Syntax","text":"
\noverflow: <overflow> <overflow>;\n\noverflow-x: <overflow>;\noverflow-y: <overflow>;\n

The style overflow accepts two values that determine when to display scrollbars in a container widget. The two values set the overflow for the horizontal and vertical axes, respectively.

Overflow may also be set individually for each axis:

  • overflow-x sets the overflow for the horizontal axis; and
  • overflow-y sets the overflow for the vertical axis.
"},{"location":"styles/overflow/#defaults","title":"Defaults","text":"

The default setting for containers is overflow: auto auto.

Warning

Some built-in containers like Horizontal and VerticalScroll override these defaults.

"},{"location":"styles/overflow/#example","title":"Example","text":"

Here we split the screen into left and right sections, each with three vertically scrolling widgets that do not fit into the height of the terminal.

The left side has overflow-y: auto (the default) and will automatically show a scrollbar. The right side has overflow-y: hidden which will prevent a scrollbar from being shown.

Outputoverflow.pyoverflow.tcss

OverflowApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eI\u00a0must\u00a0not\u00a0fear.\u258a\u258eI\u00a0must\u00a0not\u00a0fear.\u258a \u258eFear\u00a0is\u00a0the\u00a0mind-killer.\u258a\u258eFear\u00a0is\u00a0the\u00a0mind-killer.\u258a \u258eFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0\u258a\u258eFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0\u258a \u258ebrings\u00a0total\u00a0obliteration.\u258a\u258ebrings\u00a0total\u00a0obliteration.\u258a \u258eI\u00a0will\u00a0face\u00a0my\u00a0fear.\u258a\u258eI\u00a0will\u00a0face\u00a0my\u00a0fear.\u258a \u258eI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u258a\u258eI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0\u258a \u258eand\u00a0through\u00a0me.\u258a\u258eand\u00a0through\u00a0me.\u258a \u258eAnd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0\u258a\u258eAnd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0\u258a \u258ewill\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0\u258a\u258eturn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0\u258a \u258eits\u00a0path.\u258a\u2581\u2581\u258epath.\u258a \u258eWhere\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0\u258a\u258eWhere\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u258a \u258ewill\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0\u258a\u258ebe\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\u258a \u258eremain.\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258eI\u00a0must\u00a0not\u00a0fear.\u258a \u258eI\u00a0must\u00a0not\u00a0fear.\u258a\u258eFear\u00a0is\u00a0the\u00a0mind-killer.\u258a \u258eFear\u00a0is\u00a0the\u00a0mind-killer.\u258a\u258eFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0\u258a \u258eFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0\u258a\u258ebrings\u00a0total\u00a0obliteration.\u258a \u258ebrings\u00a0total\u00a0obliteration.\u258a\u258eI\u00a0will\u00a0face\u00a0my\u00a0fear.\u258a \u258eI\u00a0will\u00a0face\u00a0my\u00a0fear.\u258a\u258eI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0\u258a \u258eI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u258a\u258eand\u00a0through\u00a0me.\u258a

from textual.app import App\nfrom textual.containers import Horizontal, VerticalScroll\nfrom textual.widgets import Static\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass OverflowApp(App):\ndef compose(self):\nyield Horizontal(\nVerticalScroll(Static(TEXT), Static(TEXT), Static(TEXT), id=\"left\"),\nVerticalScroll(Static(TEXT), Static(TEXT), Static(TEXT), id=\"right\"),\n)\napp = OverflowApp(css_path=\"overflow.tcss\")\n
Screen {\nbackground: $background;\ncolor: black;\n}\nVerticalScroll {\nwidth: 1fr;\n}\nStatic {\nmargin: 1 2;\nbackground: green 80%;\nborder: green wide;\ncolor: white 90%;\nheight: auto;\n}\n#right {\noverflow-y: hidden;\n}\n
"},{"location":"styles/overflow/#css","title":"CSS","text":"
/* Automatic scrollbars on both axes (the default) */\noverflow: auto auto;\n/* Hide the vertical scrollbar */\noverflow-y: hidden;\n/* Always show the horizontal scrollbar */\noverflow-x: scroll;\n
"},{"location":"styles/overflow/#python","title":"Python","text":"

Overflow cannot be programmatically set for both axes at the same time.

# Hide the vertical scrollbar\nwidget.styles.overflow_y = \"hidden\"\n# Always show the horizontal scrollbar\nwidget.styles.overflow_x = \"scroll\"\n
"},{"location":"styles/padding/","title":"Padding","text":"

The padding style specifies spacing around the content of a widget.

"},{"location":"styles/padding/#syntax","title":"Syntax","text":"
\npadding: <integer> # one value for all edges\n       | <integer> <integer>\n       # top/bot   left/right\n       | <integer> <integer> <integer> <integer>;\n       # top       right     bot       left\n\npadding-top: <integer>;\npadding-right: <integer>;\npadding-bottom: <integer>;\npadding-left: <integer>;\n

The padding specifies spacing around the content of a widget, thus this spacing is added inside the widget. The values of the <integer> determine how much spacing is added and the number of values define what edges get what padding:

  • 1 <integer> sets the same padding for the four edges of the widget;
  • 2 <integer> set padding for top/bottom and left/right edges, respectively.
  • 4 <integer> set padding for the top, right, bottom, and left edges, respectively.

Tip

To remember the order of the edges affected by the rule padding when it has 4 values, think of a clock. Its hand starts at the top and then goes clockwise: top, right, bottom, left.

Alternatively, padding can be set for each edge individually through the rules padding-top, padding-right, padding-bottom, and padding-left, respectively.

"},{"location":"styles/padding/#example","title":"Example","text":""},{"location":"styles/padding/#basic-usage","title":"Basic usage","text":"

This example adds padding around some text.

Outputpadding.pypadding.tcss

PaddingApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0 path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0 remain.

from textual.app import App\nfrom textual.widgets import Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass PaddingApp(App):\ndef compose(self):\nyield Label(TEXT)\napp = PaddingApp(css_path=\"padding.tcss\")\n
Screen {\nbackground: white;\ncolor: blue;\n}\nLabel {\npadding: 4 8;\nbackground: blue 20%;\nwidth: 100%;\n}\n
"},{"location":"styles/padding/#all-padding-settings","title":"All padding settings","text":"

The next example shows a grid. In each cell, we have a placeholder that has its padding set in different ways. The effect of each padding setting is noticeable in the colored background around the text of each placeholder.

Outputpadding_all.pypadding_all.tcss

PaddingAllApp no\u00a0padding padding:\u00a01padding:padding:\u00a01\u00a01 1\u00a052\u00a06 padding-right:\u00a03padding-bottom:\u00a04padding-left:\u00a03 padding-top:\u00a04

from textual.app import App\nfrom textual.containers import Container, Grid\nfrom textual.widgets import Placeholder\nclass PaddingAllApp(App):\ndef compose(self):\nyield Grid(\nPlaceholder(\"no padding\", id=\"p1\"),\nPlaceholder(\"padding: 1\", id=\"p2\"),\nPlaceholder(\"padding: 1 5\", id=\"p3\"),\nPlaceholder(\"padding: 1 1 2 6\", id=\"p4\"),\nPlaceholder(\"padding-top: 4\", id=\"p5\"),\nPlaceholder(\"padding-right: 3\", id=\"p6\"),\nPlaceholder(\"padding-bottom: 4\", id=\"p7\"),\nPlaceholder(\"padding-left: 3\", id=\"p8\"),\n)\napp = PaddingAllApp(css_path=\"padding_all.tcss\")\n
Screen {\nbackground: $background;\n}\nGrid {\ngrid-size: 4;\ngrid-gutter: 1 2;\n}\nPlaceholder {\nwidth: auto;\nheight: auto;\n}\n#p1 {\n/* default is no padding */\n}\n#p2 {\npadding: 1;\n}\n#p3 {\npadding: 1 5;\n}\n#p4 {\npadding: 1 1 2 6;\n}\n#p5 {\npadding-top: 4;\n}\n#p6 {\npadding-right: 3;\n}\n#p7 {\npadding-bottom: 4;\n}\n#p8 {\npadding-left: 3;\n}\n
"},{"location":"styles/padding/#css","title":"CSS","text":"
/* Set padding of 1 around all edges */\npadding: 1;\n/* Set padding of 2 on the top and bottom edges, and 4 on the left and right */\npadding: 2 4;\n/* Set padding of 1 on the top, 2 on the right,\n3 on the bottom, and 4 on the left */\npadding: 1 2 3 4;\npadding-top: 1;\npadding-right: 2;\npadding-bottom: 3;\npadding-left: 4;\n
"},{"location":"styles/padding/#python","title":"Python","text":"

In Python, you cannot set any of the individual padding styles padding-top, padding-right, padding-bottom, and padding-left.

However, you can set padding to a single integer, a tuple of 2 integers, or a tuple of 4 integers:

# Set padding of 1 around all edges\nwidget.styles.padding = 1\n# Set padding of 2 on the top and bottom edges, and 4 on the left and right\nwidget.styles.padding = (2, 4)\n# Set padding of 1 on top, 2 on the right, 3 on the bottom, and 4 on the left\nwidget.styles.padding = (1, 2, 3, 4)\n
"},{"location":"styles/padding/#see-also","title":"See also","text":"
  • box-sizing to specify how to account for padding in a widget's dimensions.
  • padding to add spacing around a widget.
"},{"location":"styles/scrollbar_gutter/","title":"Scrollbar-gutter","text":"

The scrollbar-gutter style allows reserving space for a vertical scrollbar.

"},{"location":"styles/scrollbar_gutter/#syntax","title":"Syntax","text":"
\nscrollbar-gutter: auto | stable;\n
"},{"location":"styles/scrollbar_gutter/#values","title":"Values","text":"Value Description auto (default) No space is reserved for a vertical scrollbar. stable Space is reserved for a vertical scrollbar.

Setting the value to stable prevents unwanted layout changes when the scrollbar becomes visible, whereas the default value of auto means that the layout of your application is recomputed when a vertical scrollbar becomes needed.

"},{"location":"styles/scrollbar_gutter/#example","title":"Example","text":"

In the example below, notice the gap reserved for the scrollbar on the right side of the terminal window.

Outputscrollbar_gutter.pyscrollbar_gutter.tcss

ScrollbarGutterApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.

from textual.app import App\nfrom textual.widgets import Static\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass ScrollbarGutterApp(App):\ndef compose(self):\nyield Static(TEXT, id=\"text-box\")\napp = ScrollbarGutterApp(css_path=\"scrollbar_gutter.tcss\")\n
Screen {\nscrollbar-gutter: stable;\n}\n#text-box {\ncolor: floralwhite;\nbackground: darkmagenta;\n}\n
"},{"location":"styles/scrollbar_gutter/#css","title":"CSS","text":"
scrollbar-gutter: auto;    /* Don't reserve space for a vertical scrollbar. */\nscrollbar-gutter: stable;  /* Reserve space for a vertical scrollbar. */\n
"},{"location":"styles/scrollbar_gutter/#python","title":"Python","text":"
self.styles.scrollbar_gutter = \"auto\"    # Don't reserve space for a vertical scrollbar.\nself.styles.scrollbar_gutter = \"stable\"  # Reserve space for a vertical scrollbar.\n
"},{"location":"styles/scrollbar_size/","title":"Scrollbar-size","text":"

The scrollbar-size style defines the width of the scrollbars.

"},{"location":"styles/scrollbar_size/#syntax","title":"Syntax","text":"
\nscrollbar-size: <integer> <integer>;\n              # horizontal vertical\n\nscrollbar-size-horizontal: <integer>;\nscrollbar-size-vertical: <integer>;\n

The scrollbar-size style takes two <integer> to set the horizontal and vertical scrollbar sizes, respectively. This customisable size is the width of the scrollbar, given that its length will always be 100% of the container.

The scrollbar widths may also be set individually with scrollbar-size-horizontal and scrollbar-size-vertical.

"},{"location":"styles/scrollbar_size/#examples","title":"Examples","text":""},{"location":"styles/scrollbar_size/#basic-usage","title":"Basic usage","text":"

In this example we modify the size of the widget's scrollbar to be much larger than usual.

Outputscrollbar_size.pyscrollbar_size.tcss

ScrollbarApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2581\u2581\u2581\u2581 I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain. I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear.

from textual.app import App\nfrom textual.containers import ScrollableContainer\nfrom textual.widgets import Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\nclass ScrollbarApp(App):\ndef compose(self):\nyield ScrollableContainer(Label(TEXT * 5), classes=\"panel\")\napp = ScrollbarApp(css_path=\"scrollbar_size.tcss\")\n
Screen {\nbackground: white;\ncolor: blue 80%;\nlayout: horizontal;\n}\nLabel {\npadding: 1 2;\nwidth: 200;\n}\n.panel {\nscrollbar-size: 10 4;\npadding: 1 2;\n}\n
"},{"location":"styles/scrollbar_size/#scrollbar-sizes-comparison","title":"Scrollbar sizes comparison","text":"

In the next example we show three containers with differently sized scrollbars.

Outputscrollbar_size2.pyscrollbar_size2.tcss

ScrollbarApp I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0Fear\u00a0is\u00a0the\u00a0little-death\u00a0tFear\u00a0is\u00a0the\u00a0little-death\u00a0 I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0oI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0pastAnd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0tWhere\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0thWhere\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0t I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0Fear\u00a0is\u00a0the\u00a0little-death\u00a0tFear\u00a0is\u00a0the\u00a0little-death\u00a0 I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2587I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2587\u2587 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0oI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0pastAnd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0tWhere\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0thWhere\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0t I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear.\u2582I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0Fear\u00a0is\u00a0the\u00a0little-death\u00a0tFear\u00a0is\u00a0the\u00a0little-death\u00a0 I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0oI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0 \u258fAnd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u258f \u258fWhere\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0th\u258f \u258fI\u00a0must\u00a0not\u00a0fear.\u258f \u258fFear\u00a0is\u00a0the\u00a0mind-killer.\u258f \u258f\u2589\u258f

from textual.app import App\nfrom textual.containers import Horizontal, ScrollableContainer\nfrom textual.widgets import Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\nclass ScrollbarApp(App):\ndef compose(self):\nyield Horizontal(\nScrollableContainer(Label(TEXT * 5), id=\"v1\"),\nScrollableContainer(Label(TEXT * 5), id=\"v2\"),\nScrollableContainer(Label(TEXT * 5), id=\"v3\"),\n)\napp = ScrollbarApp(css_path=\"scrollbar_size2.tcss\")\nif __name__ == \"__main__\":\napp.run()\n
ScrollableContainer {\nwidth: 1fr;\n}\n#v1 {\nscrollbar-size: 5 1;\nbackground: red 20%;\n}\n#v2 {\nscrollbar-size-vertical: 1;\nbackground: green 20%;\n}\n#v3 {\nscrollbar-size-horizontal: 5;\nbackground: blue 20%;\n}\n
"},{"location":"styles/scrollbar_size/#css","title":"CSS","text":"
/* Set horizontal scrollbar to 10, and vertical scrollbar to 4 */\nscrollbar-size: 10 4;\n/* Set horizontal scrollbar to 10 */\nscrollbar-size-horizontal: 10;\n/* Set vertical scrollbar to 4 */\nscrollbar-size-vertical: 4;\n
"},{"location":"styles/scrollbar_size/#python","title":"Python","text":"

The style scrollbar-size has no Python equivalent. The scrollbar sizes must be set independently:

# Set horizontal scrollbar to 10:\nwidget.styles.scrollbar_size_horizontal = 10\n# Set vertical scrollbar to 4:\nwidget.styles.scrollbar_size_vertical = 4\n
"},{"location":"styles/text_align/","title":"Text-align","text":"

The text-align style sets the text alignment in a widget.

"},{"location":"styles/text_align/#syntax","title":"Syntax","text":"
\ntext-align: <text-align>;\n

The text-align style accepts a value of the type <text-align> that defines how text is aligned inside the widget.

"},{"location":"styles/text_align/#defaults","title":"Defaults","text":"

The default value is start.

"},{"location":"styles/text_align/#example","title":"Example","text":"

This example shows, from top to bottom: left, center, right, and justify text alignments.

Outputtext_align.pytext_align.tcss

TextAlign Left\u00a0alignedCenter\u00a0aligned I\u00a0must\u00a0not\u00a0fear.\u00a0Fear\u00a0is\u00a0the\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0I\u00a0must\u00a0not\u00a0fear.\u00a0Fear\u00a0is\u00a0the\u00a0\u00a0\u00a0\u00a0 mind-killer.\u00a0Fear\u00a0is\u00a0the\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0mind-killer.\u00a0Fear\u00a0is\u00a0the\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 little-death\u00a0that\u00a0brings\u00a0total\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0\u00a0\u00a0 obliteration.\u00a0I\u00a0will\u00a0face\u00a0my\u00a0fear.\u00a0Iobliteration.\u00a0I\u00a0will\u00a0face\u00a0my\u00a0fear.\u00a0I will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0\u00a0\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0 through\u00a0me.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0through\u00a0me.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 Right\u00a0alignedJustified \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0I\u00a0must\u00a0not\u00a0fear.\u00a0Fear\u00a0is\u00a0theI\u00a0\u00a0must\u00a0\u00a0not\u00a0\u00a0fear.\u00a0\u00a0Fear\u00a0\u00a0\u00a0is\u00a0\u00a0\u00a0the \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0mind-killer.\u00a0Fear\u00a0is\u00a0themind-killer.\u00a0\u00a0\u00a0\u00a0\u00a0Fear\u00a0\u00a0\u00a0\u00a0\u00a0is\u00a0\u00a0\u00a0\u00a0\u00a0the \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0little-death\u00a0that\u00a0brings\u00a0totallittle-death\u00a0\u00a0\u00a0that\u00a0\u00a0\u00a0brings\u00a0\u00a0\u00a0total obliteration.\u00a0I\u00a0will\u00a0face\u00a0my\u00a0fear.\u00a0Iobliteration.\u00a0I\u00a0will\u00a0face\u00a0my\u00a0fear.\u00a0I \u00a0\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0andwill\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0\u00a0me\u00a0\u00a0and \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0through\u00a0me.through\u00a0me.

from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Label\nTEXT = (\n\"I must not fear. Fear is the mind-killer. Fear is the little-death that \"\n\"brings total obliteration. I will face my fear. I will permit it to pass over \"\n\"me and through me.\"\n)\nclass TextAlign(App):\ndef compose(self):\nyield Grid(\nLabel(\"[b]Left aligned[/]\\n\" + TEXT, id=\"one\"),\nLabel(\"[b]Center aligned[/]\\n\" + TEXT, id=\"two\"),\nLabel(\"[b]Right aligned[/]\\n\" + TEXT, id=\"three\"),\nLabel(\"[b]Justified[/]\\n\" + TEXT, id=\"four\"),\n)\napp = TextAlign(css_path=\"text_align.tcss\")\n
#one {\ntext-align: left;\nbackground: lightblue;\n}\n#two {\ntext-align: center;\nbackground: indianred;\n}\n#three {\ntext-align: right;\nbackground: palegreen;\n}\n#four {\ntext-align: justify;\nbackground: palevioletred;\n}\nLabel {\npadding: 1 2;\nheight: 100%;\ncolor: auto;\n}\nGrid {\ngrid-size: 2 2;\n}\n
"},{"location":"styles/text_align/#css","title":"CSS","text":"
/* Set text in the widget to be right aligned */\ntext-align: right;\n
"},{"location":"styles/text_align/#python","title":"Python","text":"
# Set text in the widget to be right aligned\nwidget.styles.text_align = \"right\"\n
"},{"location":"styles/text_align/#see-also","title":"See also","text":"
  • align to set the alignment of children widgets inside a container.
  • content-align to set the alignment of content inside a widget.
"},{"location":"styles/text_opacity/","title":"Text-opacity","text":"

The text-opacity style blends the foreground color (i.e. text) with the background color.

"},{"location":"styles/text_opacity/#syntax","title":"Syntax","text":"
\ntext-opacity: <number> | <percentage>;\n

The text opacity of a widget can be set as a <number> or a <percentage>. If given as a number, then text-opacity should be a value between 0 and 1, where 0 makes the foreground color match the background (effectively making text invisible) and 1 will display text as normal. If given as a percentage, 0% will result in invisible text, and 100% will display fully opaque text.

Typically, if you set this value it would be somewhere between the two extremes. For instance, setting text-opacity to 70% would result in slightly faded text. Setting it to 0.3 would result in very dim text.

Warning

Be careful not to set text opacity so low as to make it hard to read.

"},{"location":"styles/text_opacity/#example","title":"Example","text":"

This example shows, from top to bottom, increasing text-opacity values.

Outputtext_opacity.pytext_opacity.tcss

TextOpacityApp \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0text-opacity:\u00a025%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0text-opacity:\u00a050%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0text-opacity:\u00a075%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0text-opacity:\u00a0100%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0

from textual.app import App\nfrom textual.widgets import Label\nclass TextOpacityApp(App):\ndef compose(self):\nyield Label(\"text-opacity: 0%\", id=\"zero-opacity\")\nyield Label(\"text-opacity: 25%\", id=\"quarter-opacity\")\nyield Label(\"text-opacity: 50%\", id=\"half-opacity\")\nyield Label(\"text-opacity: 75%\", id=\"three-quarter-opacity\")\nyield Label(\"text-opacity: 100%\", id=\"full-opacity\")\napp = TextOpacityApp(css_path=\"text_opacity.tcss\")\n
#zero-opacity {\ntext-opacity: 0%;\n}\n#quarter-opacity {\ntext-opacity: 25%;\n}\n#half-opacity {\ntext-opacity: 50%;\n}\n#three-quarter-opacity {\ntext-opacity: 75%;\n}\n#full-opacity {\ntext-opacity: 100%;\n}\nLabel {\nheight: 1fr;\nwidth: 100%;\ntext-align: center;\ntext-style: bold;\n}\n
"},{"location":"styles/text_opacity/#css","title":"CSS","text":"
/* Set the text to be \"half-faded\" against the background of the widget */\ntext-opacity: 50%;\n
"},{"location":"styles/text_opacity/#python","title":"Python","text":"
# Set the text to be \"half-faded\" against the background of the widget\nwidget.styles.text_opacity = \"50%\"\n
"},{"location":"styles/text_opacity/#see-also","title":"See also","text":"
  • opacity to specify the opacity of a whole widget.
"},{"location":"styles/text_style/","title":"Text-style","text":"

The text-style style sets the style for the text in a widget.

"},{"location":"styles/text_style/#syntax","title":"Syntax","text":"
\ntext-style: <text-style>;\n

text-style will take all the values specified and will apply that styling combination to the text in the widget.

"},{"location":"styles/text_style/#examples","title":"Examples","text":""},{"location":"styles/text_style/#basic-usage","title":"Basic usage","text":"

Each of the three text panels has a different text style, respectively bold, italic, and reverse, from left to right.

Outputtext_style.pytext_style.tcss

TextStyleApp I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0Fear\u00a0is\u00a0the\u00a0little-death\u00a0Fear\u00a0is\u00a0the\u00a0little-death\u00a0 that\u00a0brings\u00a0total\u00a0that\u00a0brings\u00a0total\u00a0that\u00a0brings\u00a0total\u00a0 obliteration.obliteration.obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0 over\u00a0me\u00a0and\u00a0through\u00a0me.over\u00a0me\u00a0and\u00a0through\u00a0me.over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0 I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0 to\u00a0see\u00a0its\u00a0path.to\u00a0see\u00a0its\u00a0path.to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0 there\u00a0will\u00a0be\u00a0nothing.\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Onlythere\u00a0will\u00a0be\u00a0nothing.\u00a0Only Only\u00a0I\u00a0will\u00a0remain.I\u00a0will\u00a0remain.I\u00a0will\u00a0remain.

from textual.app import App\nfrom textual.widgets import Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass TextStyleApp(App):\ndef compose(self):\nyield Label(TEXT, id=\"lbl1\")\nyield Label(TEXT, id=\"lbl2\")\nyield Label(TEXT, id=\"lbl3\")\napp = TextStyleApp(css_path=\"text_style.tcss\")\n
Screen {\nlayout: horizontal;\n}\nLabel {\nwidth: 1fr;\n}\n#lbl1 {\nbackground: red 30%;\ntext-style: bold;\n}\n#lbl2 {\nbackground: green 30%;\ntext-style: italic;\n}\n#lbl3 {\nbackground: blue 30%;\ntext-style: reverse;\n}\n
"},{"location":"styles/text_style/#all-text-styles","title":"All text styles","text":"

The next example shows all different text styles on their own, as well as some combinations of styles in a single widget.

Outputtext_style_all.pytext_style_all.tcss

AllTextStyleApp nonebolditalicreverse I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0 mind-killer.mind-killer.mind-killer.mind-killer. Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0 little-death\u00a0thatlittle-death\u00a0that\u00a0little-death\u00a0thatlittle-death\u00a0that\u00a0 brings\u00a0total\u00a0brings\u00a0total\u00a0brings\u00a0total\u00a0brings\u00a0total\u00a0 obliteration.obliteration.obliteration.obliteration. I\u00a0will\u00a0face\u00a0my\u00a0I\u00a0will\u00a0face\u00a0my\u00a0I\u00a0will\u00a0face\u00a0my\u00a0I\u00a0will\u00a0face\u00a0my\u00a0 fear.fear.fear.fear. strikeunderlinebold\u00a0italicreverse\u00a0strike I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0 mind-killer.mind-killer.mind-killer.mind-killer. Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0 little-death\u00a0thatlittle-death\u00a0that\u00a0little-death\u00a0thatlittle-death\u00a0that\u00a0 brings\u00a0total\u00a0brings\u00a0total\u00a0brings\u00a0total\u00a0brings\u00a0total\u00a0 obliteration.obliteration.obliteration.obliteration. I\u00a0will\u00a0face\u00a0my\u00a0I\u00a0will\u00a0face\u00a0my\u00a0I\u00a0will\u00a0face\u00a0my\u00a0I\u00a0will\u00a0face\u00a0my\u00a0 fear.fear.fear.fear. I\u00a0will\u00a0permit\u00a0it\u00a0I\u00a0will\u00a0permit\u00a0it\u00a0I\u00a0will\u00a0permit\u00a0it\u00a0I\u00a0will\u00a0permit\u00a0it\u00a0

from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass AllTextStyleApp(App):\ndef compose(self):\nyield Grid(\nLabel(\"none\\n\" + TEXT, id=\"lbl1\"),\nLabel(\"bold\\n\" + TEXT, id=\"lbl2\"),\nLabel(\"italic\\n\" + TEXT, id=\"lbl3\"),\nLabel(\"reverse\\n\" + TEXT, id=\"lbl4\"),\nLabel(\"strike\\n\" + TEXT, id=\"lbl5\"),\nLabel(\"underline\\n\" + TEXT, id=\"lbl6\"),\nLabel(\"bold italic\\n\" + TEXT, id=\"lbl7\"),\nLabel(\"reverse strike\\n\" + TEXT, id=\"lbl8\"),\n)\napp = AllTextStyleApp(css_path=\"text_style_all.tcss\")\n
#lbl1 {\ntext-style: none;\n}\n#lbl2 {\ntext-style: bold;\n}\n#lbl3 {\ntext-style: italic;\n}\n#lbl4 {\ntext-style: reverse;\n}\n#lbl5 {\ntext-style: strike;\n}\n#lbl6 {\ntext-style: underline;\n}\n#lbl7 {\ntext-style: bold italic;\n}\n#lbl8 {\ntext-style: reverse strike;\n}\nGrid {\ngrid-size: 4;\ngrid-gutter: 1 2;\nmargin: 1 2;\nheight: 100%;\n}\nLabel {\nheight: 100%;\n}\n
"},{"location":"styles/text_style/#css","title":"CSS","text":"
text-style: italic;\n
"},{"location":"styles/text_style/#python","title":"Python","text":"
widget.styles.text_style = \"italic\"\n
"},{"location":"styles/tint/","title":"Tint","text":"

The tint style blends a color with the whole widget.

"},{"location":"styles/tint/#syntax","title":"Syntax","text":"
\ntint: <color> [<percentage>];\n

The tint style blends a <color> with the widget. The color should likely have an alpha component (specified directly in the color used or by the optional <percentage>), otherwise the end result will obscure the widget content.

"},{"location":"styles/tint/#example","title":"Example","text":"

This examples shows a green tint with gradually increasing alpha.

Outputtint.pytint.tcss

TintApp tint:\u00a0green\u00a00%; tint:\u00a0green\u00a010%; tint:\u00a0green\u00a020%; tint:\u00a0green\u00a030%; tint:\u00a0green\u00a040%; tint:\u00a0green\u00a050%; \u2584\u2584 tint:\u00a0green\u00a060%; tint:\u00a0green\u00a070%;

from textual.app import App\nfrom textual.color import Color\nfrom textual.widgets import Label\nclass TintApp(App):\ndef compose(self):\ncolor = Color.parse(\"green\")\nfor tint_alpha in range(0, 101, 10):\nwidget = Label(f\"tint: green {tint_alpha}%;\")\nwidget.styles.tint = color.with_alpha(tint_alpha / 100)  # (1)!\nyield widget\napp = TintApp(css_path=\"tint.tcss\")\n
  1. We set the tint to a Color instance with varying levels of opacity, set through the method with_alpha.
Label {\nheight: 3;\nwidth: 100%;\ntext-style: bold;\nbackground: white;\ncolor: black;\ncontent-align: center middle;\n}\n
"},{"location":"styles/tint/#css","title":"CSS","text":"
/* A red tint (could indicate an error) */\ntint: red 20%;\n/* A green tint */\ntint: rgba(0, 200, 0, 0.3);\n
"},{"location":"styles/tint/#python","title":"Python","text":"
# A red tint\nfrom textual.color import Color\nwidget.styles.tint = Color.parse(\"red\").with_alpha(0.2);\n# A green tint\nwidget.styles.tint = \"rgba(0, 200, 0, 0.3)\"\n
"},{"location":"styles/visibility/","title":"Visibility","text":"

The visibility style determines whether a widget is visible or not.

"},{"location":"styles/visibility/#syntax","title":"Syntax","text":"
\nvisibility: hidden | visible;\n

visibility takes one of two values to set the visibility of a widget.

"},{"location":"styles/visibility/#values","title":"Values","text":"Value Description hidden The widget will be invisible. visible (default) The widget will be displayed as normal."},{"location":"styles/visibility/#visibility-inheritance","title":"Visibility inheritance","text":"

Note

Children of an invisible container can be visible.

By default, children inherit the visibility of their parents. So, if a container is set to be invisible, its children widgets will also be invisible by default. However, those widgets can be made visible if their visibility is explicitly set to visibility: visible. This is shown in the second example below.

"},{"location":"styles/visibility/#examples","title":"Examples","text":""},{"location":"styles/visibility/#basic-usage","title":"Basic usage","text":"

Note that the second widget is hidden while leaving a space where it would have been rendered.

Outputvisibility.pyvisibility.tcss

VisibilityApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Widget\u00a01\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Widget\u00a03\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

from textual.app import App\nfrom textual.widgets import Label\nclass VisibilityApp(App):\ndef compose(self):\nyield Label(\"Widget 1\")\nyield Label(\"Widget 2\", classes=\"invisible\")\nyield Label(\"Widget 3\")\napp = VisibilityApp(css_path=\"visibility.tcss\")\n
Screen {\nbackground: green;\n}\nLabel {\nheight: 5;\nwidth: 100%;\nbackground: white;\ncolor: blue;\nborder: heavy blue;\n}\nLabel.invisible {\nvisibility: hidden;\n}\n
"},{"location":"styles/visibility/#overriding-container-visibility","title":"Overriding container visibility","text":"

The next example shows the interaction of the visibility style with invisible containers that have visible children. The app below has three rows with a Horizontal container per row and three placeholders per row. The containers all have a white background, and then:

  • the top container is visible by default (we can see the white background around the placeholders);
  • the middle container is invisible and the children placeholders inherited that setting;
  • the bottom container is invisible but the children placeholders are visible because they were set to be visible.
Outputvisibility_containers.pyvisibility_containers.tcss

VisibilityContainersApp PlaceholderPlaceholderPlaceholder PlaceholderPlaceholderPlaceholder

from textual.app import App\nfrom textual.containers import Horizontal, VerticalScroll\nfrom textual.widgets import Placeholder\nclass VisibilityContainersApp(App):\ndef compose(self):\nyield VerticalScroll(\nHorizontal(\nPlaceholder(),\nPlaceholder(),\nPlaceholder(),\nid=\"top\",\n),\nHorizontal(\nPlaceholder(),\nPlaceholder(),\nPlaceholder(),\nid=\"middle\",\n),\nHorizontal(\nPlaceholder(),\nPlaceholder(),\nPlaceholder(),\nid=\"bot\",\n),\n)\napp = VisibilityContainersApp(css_path=\"visibility_containers.tcss\")\n
Horizontal {\npadding: 1 2;     /* (1)! */\nbackground: white;\nheight: 1fr;\n}\n#top {}               /* (2)! */\n#middle {             /* (3)! */\nvisibility: hidden;\n}\n#bot {                /* (4)! */\nvisibility: hidden;\n}\n#bot > Placeholder {  /* (5)! */\nvisibility: visible;\n}\nPlaceholder {\nwidth: 1fr;\n}\n
  1. The padding and the white background let us know when the Horizontal is visible.
  2. The top Horizontal is visible by default, and so are its children.
  3. The middle Horizontal is made invisible and its children will inherit that setting.
  4. The bottom Horizontal is made invisible...
  5. ... but its children override that setting and become visible.
"},{"location":"styles/visibility/#css","title":"CSS","text":"
/* Widget is invisible */\nvisibility: hidden;\n/* Widget is visible */\nvisibility: visible;\n
"},{"location":"styles/visibility/#python","title":"Python","text":"
# Widget is invisible\nself.styles.visibility = \"hidden\"\n# Widget is visible\nself.styles.visibility = \"visible\"\n

There is also a shortcut to set a Widget's visibility. The visible property on Widget may be set to True or False.

# Make a widget invisible\nwidget.visible = False\n# Make the widget visible again\nwidget.visible = True\n
"},{"location":"styles/visibility/#see-also","title":"See also","text":"
  • display to specify whether a widget is displayed or not.
"},{"location":"styles/width/","title":"Width","text":"

The width style sets a widget's width.

"},{"location":"styles/width/#syntax","title":"Syntax","text":"
\nwidth: <scalar>;\n

The style width needs a <scalar> to determine the horizontal length of the width. By default, it sets the width of the content area, but if box-sizing is set to border-box it sets the width of the border area.

"},{"location":"styles/width/#examples","title":"Examples","text":""},{"location":"styles/width/#basic-usage","title":"Basic usage","text":"

This example adds a widget with 50% width of the screen.

Outputwidth.pywidth.tcss

WidthApp Widget

from textual.app import App\nfrom textual.widget import Widget\nclass WidthApp(App):\ndef compose(self):\nyield Widget()\napp = WidthApp(css_path=\"width.tcss\")\n
Screen > Widget {\nbackground: green;\nwidth: 50%;\ncolor: white;\n}\n
"},{"location":"styles/width/#all-width-formats","title":"All width formats","text":"Outputwidth_comparison.pywidth_comparison.tcss

WidthComparisonApp #cells#percent#w#h#vw#vh#auto#fr1#fr3 \u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022

from textual.app import App\nfrom textual.containers import Horizontal\nfrom textual.widgets import Label, Placeholder, Static\nclass Ruler(Static):\ndef compose(self):\nruler_text = \"\u00b7\u00b7\u00b7\u00b7\u2022\" * 100\nyield Label(ruler_text)\nclass WidthComparisonApp(App):\ndef compose(self):\nyield Horizontal(\nPlaceholder(id=\"cells\"),  # (1)!\nPlaceholder(id=\"percent\"),\nPlaceholder(id=\"w\"),\nPlaceholder(id=\"h\"),\nPlaceholder(id=\"vw\"),\nPlaceholder(id=\"vh\"),\nPlaceholder(id=\"auto\"),\nPlaceholder(id=\"fr1\"),\nPlaceholder(id=\"fr3\"),\n)\nyield Ruler()\napp = WidthComparisonApp(css_path=\"width_comparison.tcss\")\nif __name__ == \"__main__\":\napp.run()\n
  1. The id of the placeholder identifies which unit will be used to set the width of the widget.
#cells {\nwidth: 9;      /* (1)! */\n}\n#percent {\nwidth: 12.5%;  /* (2)! */\n}\n#w {\nwidth: 10w;    /* (3)! */\n}\n#h {\nwidth: 25h;    /* (4)! */\n}\n#vw {\nwidth: 15vw;   /* (5)! */\n}\n#vh {\nwidth: 25vh;   /* (6)! */\n}\n#auto {\nwidth: auto;   /* (7)! */\n}\n#fr1 {\nwidth: 1fr;    /* (8)! */\n}\n#fr3 {\nwidth: 3fr;    /* (9)! */\n}\nScreen {\nlayers: ruler;\n}\nRuler {\nlayer: ruler;\ndock: bottom;\noverflow: hidden;\nheight: 1;\nbackground: $accent;\n}\n
  1. This sets the width to 9 columns.
  2. This sets the width to 12.5% of the space made available by the container. The container is 80 columns wide, so 12.5% of 80 is 10.
  3. This sets the width to 10% of the width of the direct container, which is the Horizontal container. Because it expands to fit all of the terminal, the width of the Horizontal is 80 and 10% of 80 is 8.
  4. This sets the width to 25% of the height of the direct container, which is the Horizontal container. Because it expands to fit all of the terminal, the height of the Horizontal is 24 and 25% of 24 is 6.
  5. This sets the width to 15% of the viewport width, which is 80. 15% of 80 is 12.
  6. This sets the width to 25% of the viewport height, which is 24. 25% of 24 is 6.
  7. This sets the width of the placeholder to be the optimal size that fits the content without scrolling. Because the content is the string \"#auto\", the placeholder has its width set to 5.
  8. This sets the width to 1fr, which means this placeholder will have a third of the width of a placeholder with 3fr.
  9. This sets the width to 3fr, which means this placeholder will have triple the width of a placeholder with 1fr.
"},{"location":"styles/width/#css","title":"CSS","text":"
/* Explicit cell width */\nwidth: 10;\n/* Percentage width */\nwidth: 50%;\n/* Automatic width */\nwidth: auto;\n
"},{"location":"styles/width/#python","title":"Python","text":"
widget.styles.width = 10\nwidget.styles.width = \"50%\nwidget.styles.width = \"auto\"\n
"},{"location":"styles/width/#see-also","title":"See also","text":"
  • max-width and min-width to limit the width of a widget.
  • height to set the height of a widget.
"},{"location":"styles/grid/","title":"Grid","text":"

There are a number of styles relating to the Textual grid layout.

For an in-depth look at the grid layout, visit the grid guide.

Property Description column-span Number of columns a cell spans. grid-columns Width of grid columns. grid-gutter Spacing between grid cells. grid-rows Height of grid rows. grid-size Number of columns and rows in the grid layout. row-span Number of rows a cell spans."},{"location":"styles/grid/#syntax","title":"Syntax","text":"
\ncolumn-span: <integer>;\n\ngrid-columns: <scalar>+;\n\ngrid-gutter: <scalar> [<scalar>];\n\ngrid-rows: <scalar>+;\n\ngrid-size: <integer> [<integer>];\n\nrow-span: <integer>;\n

Visit each style's reference page to learn more about how the values are used.

"},{"location":"styles/grid/#example","title":"Example","text":"

The example below shows all the styles above in action. The grid-size: 3 4; declaration sets the grid to 3 columns and 4 rows. The first cell of the grid, tinted magenta, shows a cell spanning multiple rows and columns. The spacing between grid cells is defined by the grid-gutter style.

Outputgrid.pygrid.tcss

GridApp Grid\u00a0cell\u00a01Grid\u00a0cell\u00a02 row-span:\u00a03; column-span:\u00a02; Grid\u00a0cell\u00a03 Grid\u00a0cell\u00a04 Grid\u00a0cell\u00a05Grid\u00a0cell\u00a06Grid\u00a0cell\u00a07

from textual.app import App\nfrom textual.widgets import Static\nclass GridApp(App):\ndef compose(self):\nyield Static(\"Grid cell 1\\n\\nrow-span: 3;\\ncolumn-span: 2;\", id=\"static1\")\nyield Static(\"Grid cell 2\", id=\"static2\")\nyield Static(\"Grid cell 3\", id=\"static3\")\nyield Static(\"Grid cell 4\", id=\"static4\")\nyield Static(\"Grid cell 5\", id=\"static5\")\nyield Static(\"Grid cell 6\", id=\"static6\")\nyield Static(\"Grid cell 7\", id=\"static7\")\napp = GridApp(css_path=\"grid.tcss\")\n
Screen {\nlayout: grid;\ngrid-size: 3 4;\ngrid-rows: 1fr;\ngrid-columns: 1fr;\ngrid-gutter: 1;\n}\nStatic {\ncolor: auto;\nbackground: lightblue;\nheight: 100%;\npadding: 1 2;\n}\n#static1 {\ntint: magenta 40%;\nrow-span: 3;\ncolumn-span: 2;\n}\n

Warning

The styles listed on this page will only work when the layout is grid.

"},{"location":"styles/grid/#see-also","title":"See also","text":"
  • The grid layout guide.
"},{"location":"styles/grid/column_span/","title":"Column-span","text":"

The column-span style specifies how many columns a widget will span in a grid layout.

Note

This style only affects widgets that are direct children of a widget with layout: grid.

"},{"location":"styles/grid/column_span/#syntax","title":"Syntax","text":"
\ncolumn-span: <integer>;\n

The column-span style accepts a single non-negative <integer> that quantifies how many columns the given widget spans.

"},{"location":"styles/grid/column_span/#example","title":"Example","text":"

The example below shows a 4 by 4 grid where many placeholders span over several columns.

Outputcolumn_span.pycolumn_span.tcss

MyApp #p1 #p2#p3 #p4#p5 #p6#p7

from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Placeholder\nclass MyApp(App):\ndef compose(self):\nyield Grid(\nPlaceholder(id=\"p1\"),\nPlaceholder(id=\"p2\"),\nPlaceholder(id=\"p3\"),\nPlaceholder(id=\"p4\"),\nPlaceholder(id=\"p5\"),\nPlaceholder(id=\"p6\"),\nPlaceholder(id=\"p7\"),\n)\napp = MyApp(css_path=\"column_span.tcss\")\n
#p1 {\ncolumn-span: 4;\n}\n#p2 {\ncolumn-span: 3;\n}\n#p3 {\ncolumn-span: 1;  /* Didn't need to be set explicitly. */\n}\n#p4 {\ncolumn-span: 2;\n}\n#p5 {\ncolumn-span: 2;\n}\n#p6 {\n/* Default value is 1. */\n}\n#p7 {\ncolumn-span: 3;\n}\nGrid {\ngrid-size: 4 4;\ngrid-gutter: 1 2;\n}\nPlaceholder {\nheight: 100%;\n}\n
"},{"location":"styles/grid/column_span/#css","title":"CSS","text":"
column-span: 3;\n
"},{"location":"styles/grid/column_span/#python","title":"Python","text":"
widget.styles.column_span = 3\n
"},{"location":"styles/grid/column_span/#see-also","title":"See also","text":"
  • row-span to specify how many rows a widget spans.
"},{"location":"styles/grid/grid_columns/","title":"Grid-columns","text":"

The grid-columns style allows to define the width of the columns of the grid.

Note

This style only affects widgets with layout: grid.

"},{"location":"styles/grid/grid_columns/#syntax","title":"Syntax","text":"
\ngrid-columns: <scalar>+;\n

The grid-columns style takes one or more <scalar> that specify the length of the columns of the grid.

If there are more columns in the grid than scalars specified in grid-columns, they are reused cyclically. If the number of <scalar> is in excess, the excess is ignored.

"},{"location":"styles/grid/grid_columns/#example","title":"Example","text":"

The example below shows a grid with 10 labels laid out in a grid with 2 rows and 5 columns.

We set grid-columns: 1fr 16 2fr. Because there are more rows than scalars in the style definition, the scalars will be reused:

  • columns 1 and 4 have width 1fr;
  • columns 2 and 5 have width 16; and
  • column 3 has width 2fr.
Outputgrid_columns.pygrid_columns.tcss

MyApp \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u25021fr\u2502\u2502width\u00a0=\u00a016\u2502\u25022fr\u2502\u25021fr\u2502\u2502width\u00a0=\u00a016\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u25021fr\u2502\u2502width\u00a0=\u00a016\u2502\u25022fr\u2502\u25021fr\u2502\u2502width\u00a0=\u00a016\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Label\nclass MyApp(App):\ndef compose(self):\nyield Grid(\nLabel(\"1fr\"),\nLabel(\"width = 16\"),\nLabel(\"2fr\"),\nLabel(\"1fr\"),\nLabel(\"width = 16\"),\nLabel(\"1fr\"),\nLabel(\"width = 16\"),\nLabel(\"2fr\"),\nLabel(\"1fr\"),\nLabel(\"width = 16\"),\n)\napp = MyApp(css_path=\"grid_columns.tcss\")\n
Grid {\ngrid-size: 5 2;\ngrid-columns: 1fr 16 2fr;\n}\nLabel {\nborder: round white;\ncontent-align-horizontal: center;\nwidth: 100%;\nheight: 100%;\n}\n
"},{"location":"styles/grid/grid_columns/#css","title":"CSS","text":"
/* Set all columns to have 50% width */\ngrid-columns: 50%;\n/* Every other column is twice as wide as the first one */\ngrid-columns: 1fr 2fr;\n
"},{"location":"styles/grid/grid_columns/#python","title":"Python","text":"
grid.styles.grid_columns = \"50%\"\ngrid.styles.grid_columns = \"1fr 2fr\"\n
"},{"location":"styles/grid/grid_columns/#see-also","title":"See also","text":"
  • grid-rows to specify the height of the grid rows.
"},{"location":"styles/grid/grid_gutter/","title":"Grid-gutter","text":"

The grid-gutter style sets the size of the gutter in the grid layout. That is, it sets the space between adjacent cells in the grid.

Gutter is only applied between the edges of cells. No spacing is added between the edges of the cells and the edges of the container.

Note

This style only affects widgets with layout: grid.

"},{"location":"styles/grid/grid_gutter/#syntax","title":"Syntax","text":"
\ngrid-gutter: <integer> [<integer>];\n

The grid-gutter style takes one or two <integer> that set the length of the gutter along the vertical and horizontal axes. If only one <integer> is supplied, it sets the vertical and horizontal gutters. If two are supplied, they set the vertical and horizontal gutters, respectively.

"},{"location":"styles/grid/grid_gutter/#example","title":"Example","text":"

The example below employs a common trick to apply visually consistent spacing around all grid cells.

Outputgrid_gutter.pygrid_gutter.tcss

MyApp \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u25021\u2502\u25022\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u25023\u2502\u25024\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u25025\u2502\u25026\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u25027\u2502\u25028\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Label\nclass MyApp(App):\ndef compose(self):\nyield Grid(\nLabel(\"1\"),\nLabel(\"2\"),\nLabel(\"3\"),\nLabel(\"4\"),\nLabel(\"5\"),\nLabel(\"6\"),\nLabel(\"7\"),\nLabel(\"8\"),\n)\napp = MyApp(css_path=\"grid_gutter.tcss\")\n
Grid {\ngrid-size: 2 4;\ngrid-gutter: 1 2;  /* (1)! */\n}\nLabel {\nborder: round white;\ncontent-align: center middle;\nwidth: 100%;\nheight: 100%;\n}\n
  1. We set the horizontal gutter to be double the vertical gutter because terminal cells are typically two times taller than they are wide. Thus, the result shows visually consistent spacing around grid cells.
"},{"location":"styles/grid/grid_gutter/#css","title":"CSS","text":"
/* Set vertical and horizontal gutters to be the same */\ngrid-gutter: 5;\n/* Set vertical and horizontal gutters separately */\ngrid-gutter: 1 2;\n
"},{"location":"styles/grid/grid_gutter/#python","title":"Python","text":"

Vertical and horizontal gutters correspond to different Python properties, so they must be set separately:

widget.styles.grid_gutter_vertical = \"1\"\nwidget.styles.grid_gutter_horizontal = \"2\"\n
"},{"location":"styles/grid/grid_rows/","title":"Grid-rows","text":"

The grid-rows style allows to define the height of the rows of the grid.

Note

This style only affects widgets with layout: grid.

"},{"location":"styles/grid/grid_rows/#syntax","title":"Syntax","text":"
\ngrid-rows: <scalar>+;\n

The grid-rows style takes one or more <scalar> that specify the length of the rows of the grid.

If there are more rows in the grid than scalars specified in grid-rows, they are reused cyclically. If the number of <scalar> is in excess, the excess is ignored.

"},{"location":"styles/grid/grid_rows/#example","title":"Example","text":"

The example below shows a grid with 10 labels laid out in a grid with 5 rows and 2 columns.

We set grid-rows: 1fr 6 25%. Because there are more rows than scalars in the style definition, the scalars will be reused:

  • rows 1 and 4 have height 1fr;
  • rows 2 and 5 have height 6; and
  • row 3 has height 25%.
Outputgrid_rows.pygrid_rows.tcss

MyApp \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u25021fr\u2502\u25021fr\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u2502height\u00a0=\u00a06\u2502\u2502height\u00a0=\u00a06\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u250225%\u2502\u250225%\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u25021fr\u2502\u25021fr\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u2502height\u00a0=\u00a06\u2502\u2502height\u00a0=\u00a06\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Label\nclass MyApp(App):\ndef compose(self):\nyield Grid(\nLabel(\"1fr\"),\nLabel(\"1fr\"),\nLabel(\"height = 6\"),\nLabel(\"height = 6\"),\nLabel(\"25%\"),\nLabel(\"25%\"),\nLabel(\"1fr\"),\nLabel(\"1fr\"),\nLabel(\"height = 6\"),\nLabel(\"height = 6\"),\n)\napp = MyApp(css_path=\"grid_rows.tcss\")\n
Grid {\ngrid-size: 2 5;\ngrid-rows: 1fr 6 25%;\n}\nLabel {\nborder: round white;\ncontent-align: center middle;\nwidth: 100%;\nheight: 100%;\n}\n
"},{"location":"styles/grid/grid_rows/#css","title":"CSS","text":"
/* Set all rows to have 50% height */\ngrid-rows: 50%;\n/* Every other row is twice as tall as the first one */\ngrid-rows: 1fr 2fr;\n
"},{"location":"styles/grid/grid_rows/#python","title":"Python","text":"
grid.styles.grid_rows = \"50%\"\ngrid.styles.grid_rows = \"1fr 2fr\"\n
"},{"location":"styles/grid/grid_rows/#see-also","title":"See also","text":"
  • grid-columns to specify the width of the grid columns.
"},{"location":"styles/grid/grid_size/","title":"Grid-size","text":"

The grid-size style sets the number of columns and rows in a grid layout.

The number of rows can be left unspecified and it will be computed automatically.

Note

This style only affects widgets with layout: grid.

"},{"location":"styles/grid/grid_size/#syntax","title":"Syntax","text":"
\ngrid-size: <integer> [<integer>];\n

The grid-size style takes one or two non-negative <integer>. The first defines how many columns there are in the grid. If present, the second one sets the number of rows \u2013 regardless of the number of children of the grid \u2013, otherwise the number of rows is computed automatically.

"},{"location":"styles/grid/grid_size/#examples","title":"Examples","text":""},{"location":"styles/grid/grid_size/#columns-and-rows","title":"Columns and rows","text":"

In the first example, we create a grid with 2 columns and 5 rows, although we do not have enough labels to fill in the whole grid:

Outputgrid_size_both.pygrid_size_both.tcss

MyApp \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u25021\u2502\u25022\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u25023\u2502\u25024\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502 \u25025\u2502 \u2502\u2502 \u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Label\nclass MyApp(App):\ndef compose(self):\nyield Grid(\nLabel(\"1\"),\nLabel(\"2\"),\nLabel(\"3\"),\nLabel(\"4\"),\nLabel(\"5\"),\n)\napp = MyApp(css_path=\"grid_size_both.tcss\")\n
Grid {\ngrid-size: 2 4;  /* (1)! */\n}\nLabel {\nborder: round white;\ncontent-align: center middle;\nwidth: 100%;\nheight: 100%;\n}\n
  1. Create a grid with 2 columns and 4 rows.
"},{"location":"styles/grid/grid_size/#columns-only","title":"Columns only","text":"

In the second example, we create a grid with 2 columns and however many rows are needed to display all of the grid children:

Outputgrid_size_columns.pygrid_size_columns.tcss

MyApp \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u25021\u2502\u25022\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u25023\u2502\u25024\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502 \u2502\u2502 \u25025\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Label\nclass MyApp(App):\ndef compose(self):\nyield Grid(\nLabel(\"1\"),\nLabel(\"2\"),\nLabel(\"3\"),\nLabel(\"4\"),\nLabel(\"5\"),\n)\napp = MyApp(css_path=\"grid_size_columns.tcss\")\n
Grid {\ngrid-size: 2;  /* (1)! */\n}\nLabel {\nborder: round white;\ncontent-align: center middle;\nwidth: 100%;\nheight: 100%;\n}\n
  1. Create a grid with 2 columns and however many rows.
"},{"location":"styles/grid/grid_size/#css","title":"CSS","text":"
/* Grid with 3 rows and 5 columns */\ngrid-size: 3 5;\n/* Grid with 4 columns and as many rows as needed */\ngrid-size: 4;\n
"},{"location":"styles/grid/grid_size/#python","title":"Python","text":"

To programmatically change the grid size, the number of rows and columns must be specified separately:

widget.styles.grid_size_rows = 3\nwidget.styles.grid_size_columns = 6\n
"},{"location":"styles/grid/row_span/","title":"Row-span","text":"

The row-span style specifies how many rows a widget will span in a grid layout.

Note

This style only affects widgets that are direct children of a widget with layout: grid.

"},{"location":"styles/grid/row_span/#syntax","title":"Syntax","text":"
\nrow-span: <integer>;\n

The row-span style accepts a single non-negative <integer> that quantifies how many rows the given widget spans.

"},{"location":"styles/grid/row_span/#example","title":"Example","text":"

The example below shows a 4 by 4 grid where many placeholders span over several rows.

Notice that grid cells are filled from left to right, top to bottom. After placing the placeholders #p1, #p2, #p3, and #p4, the next available cell is in the second row, fourth column, which is where the top of #p5 is.

Outputrow_span.pyrow_span.tcss

MyApp #p4 #p3 #p2 #p1 #p5 #p6 #p7

from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Placeholder\nclass MyApp(App):\ndef compose(self):\nyield Grid(\nPlaceholder(id=\"p1\"),\nPlaceholder(id=\"p2\"),\nPlaceholder(id=\"p3\"),\nPlaceholder(id=\"p4\"),\nPlaceholder(id=\"p5\"),\nPlaceholder(id=\"p6\"),\nPlaceholder(id=\"p7\"),\n)\napp = MyApp(css_path=\"row_span.tcss\")\n
#p1 {\nrow-span: 4;\n}\n#p2 {\nrow-span: 3;\n}\n#p3 {\nrow-span: 2;\n}\n#p4 {\nrow-span: 1;  /* Didn't need to be set explicitly. */\n}\n#p5 {\nrow-span: 3;\n}\n#p6 {\nrow-span: 2;\n}\n#p7 {\n/* Default value is 1. */\n}\nGrid {\ngrid-size: 4 4;\ngrid-gutter: 1 2;\n}\nPlaceholder {\nheight: 100%;\n}\n
"},{"location":"styles/grid/row_span/#css","title":"CSS","text":"
row-span: 3\n
"},{"location":"styles/grid/row_span/#python","title":"Python","text":"
widget.styles.row_span = 3\n
"},{"location":"styles/grid/row_span/#see-also","title":"See also","text":"
  • column-span to specify how many columns a widget spans.
"},{"location":"styles/links/","title":"Links","text":"

Textual supports the concept of inline \"links\" embedded in text which trigger an action when pressed. There are a number of styles which influence the appearance of these links within a widget.

Note

These CSS rules only target Textual action links. Internet hyperlinks are not affected by these styles.

Property Description link-background The background color of the link text. link-color The color of the link text. link-hover-background The background color of the link text when the cursor is over it. link-hover-color The color of the link text when the cursor is over it. link-hover-style The style of the link text when the cursor is over it. link-style The style of the link text (e.g. underline)."},{"location":"styles/links/#syntax","title":"Syntax","text":"
\nlink-background: <color> [<percentage>];\n\nlink-color: <color> [<percentage>];\n\nlink-style: <text-style>;\n\nlink-hover-background: <color> [<percentage>];\n\nlink-hover-color: <color> [<percentage>];\n\nlink-hover-style: <text-style>;\n

Visit each style's reference page to learn more about how the values are used.

"},{"location":"styles/links/#example","title":"Example","text":"

In the example below, the first label illustrates default link styling. The second label uses CSS to customize the link color, background, and style.

Outputlinks.pylinks.tcss

LinksApp Here\u00a0is\u00a0a\u00a0link\u00a0which\u00a0you\u00a0can\u00a0click! Here\u00a0is\u00a0a\u00a0link\u00a0which\u00a0you\u00a0can\u00a0click!

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nTEXT = \"\"\"\\\nHere is a [@click='app.bell']link[/] which you can click!\n\"\"\"\nclass LinksApp(App):\ndef compose(self) -> ComposeResult:\nyield Static(TEXT)\nyield Static(TEXT, id=\"custom\")\napp = LinksApp(css_path=\"links.tcss\")\n
#custom {\nlink-color: black 90%;\nlink-background: dodgerblue;\nlink-style: bold italic underline;\n}\n
"},{"location":"styles/links/#additional-notes","title":"Additional Notes","text":"
  • Inline links are not widgets, and thus cannot be focused.
"},{"location":"styles/links/#see-also","title":"See Also","text":"
  • An introduction to links in the Actions guide.
"},{"location":"styles/links/link_background/","title":"Link-background","text":"

The link-background style sets the background color of the link.

Note

link-background only applies to Textual action links as described in the actions guide and not to regular hyperlinks.

"},{"location":"styles/links/link_background/#syntax","title":"Syntax","text":"
\nlink-background: <color> [<percentage>];\n

link-background accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the background color of text enclosed in Textual action links.

"},{"location":"styles/links/link_background/#example","title":"Example","text":"

The example below shows some links with their background color changed. It also shows that link-background does not affect hyperlinks.

Outputlink_background.pylink_background.tcss

LinkBackgroundApp Visit\u00a0the\u00a0Textualize\u00a0website. Click\u00a0here\u00a0for\u00a0the\u00a0bell\u00a0sound. You\u00a0can\u00a0also\u00a0click\u00a0here\u00a0for\u00a0the\u00a0bell\u00a0sound. Exit\u00a0this\u00a0application.

from textual.app import App\nfrom textual.widgets import Label\nclass LinkBackgroundApp(App):\ndef compose(self):\nyield Label(\n\"Visit the [link=https://textualize.io]Textualize[/link] website.\",\nid=\"lbl1\",  # (1)!\n)\nyield Label(\n\"Click [@click=app.bell]here[/] for the bell sound.\",\nid=\"lbl2\",  # (2)!\n)\nyield Label(\n\"You can also click [@click=app.bell]here[/] for the bell sound.\",\nid=\"lbl3\",  # (3)!\n)\nyield Label(\n\"[@click=app.quit]Exit this application.[/]\",\nid=\"lbl4\",  # (4)!\n)\napp = LinkBackgroundApp(css_path=\"link_background.tcss\")\n
  1. This label has an hyperlink so it won't be affected by the link-background rule.
  2. This label has an \"action link\" that can be styled with link-background.
  3. This label has an \"action link\" that can be styled with link-background.
  4. This label has an \"action link\" that can be styled with link-background.
#lbl1, #lbl2 {\nlink-background: red;  /* (1)! */\n}\n#lbl3 {\nlink-background: hsl(60,100%,50%) 50%;\n}\n#lbl4 {\nlink-background: $accent;\n}\n
  1. This will only affect one of the labels because action links are the only links that this rule affects.
"},{"location":"styles/links/link_background/#css","title":"CSS","text":"
link-background: red 70%;\nlink-background: $accent;\n
"},{"location":"styles/links/link_background/#python","title":"Python","text":"
widget.styles.link_background = \"red 70%\"\nwidget.styles.link_background = \"$accent\"\n# You can also use a `Color` object directly:\nwidget.styles.link_background = Color(100, 30, 173)\n
"},{"location":"styles/links/link_background/#see-also","title":"See also","text":"
  • link-color to set the color of link text.
  • `link-hover-background to set the background color of link text when the mouse pointer is over it.
"},{"location":"styles/links/link_color/","title":"Link-color","text":"

The link-color style sets the color of the link text.

Note

link-color only applies to Textual action links as described in the actions guide and not to regular hyperlinks.

"},{"location":"styles/links/link_color/#syntax","title":"Syntax","text":"
\nlink-color: <color> [<percentage>];\n

link-color accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the color of text enclosed in Textual action links.

"},{"location":"styles/links/link_color/#example","title":"Example","text":"

The example below shows some links with their color changed. It also shows that link-color does not affect hyperlinks.

Outputlink_color.pylink_color.tcss

LinkColorApp Visit\u00a0the\u00a0Textualize\u00a0website. Click\u00a0here\u00a0for\u00a0the\u00a0bell\u00a0sound. You\u00a0can\u00a0also\u00a0click\u00a0here\u00a0for\u00a0the\u00a0bell\u00a0sound. Exit\u00a0this\u00a0application.

from textual.app import App\nfrom textual.widgets import Label\nclass LinkColorApp(App):\ndef compose(self):\nyield Label(\n\"Visit the [link=https://textualize.io]Textualize[/link] website.\",\nid=\"lbl1\",  # (1)!\n)\nyield Label(\n\"Click [@click=app.bell]here[/] for the bell sound.\",\nid=\"lbl2\",  # (2)!\n)\nyield Label(\n\"You can also click [@click=app.bell]here[/] for the bell sound.\",\nid=\"lbl3\",  # (3)!\n)\nyield Label(\n\"[@click=app.quit]Exit this application.[/]\",\nid=\"lbl4\",  # (4)!\n)\napp = LinkColorApp(css_path=\"link_color.tcss\")\n
  1. This label has an hyperlink so it won't be affected by the link-color rule.
  2. This label has an \"action link\" that can be styled with link-color.
  3. This label has an \"action link\" that can be styled with link-color.
  4. This label has an \"action link\" that can be styled with link-color.
#lbl1, #lbl2 {\nlink-color: red;  /* (1)! */\n}\n#lbl3 {\nlink-color: hsl(60,100%,50%) 50%;\n}\n#lbl4 {\nlink-color: $accent;\n}\n
  1. This will only affect one of the labels because action links are the only links that this rule affects.
"},{"location":"styles/links/link_color/#css","title":"CSS","text":"
link-color: red 70%;\nlink-color: $accent;\n
"},{"location":"styles/links/link_color/#python","title":"Python","text":"
widget.styles.link_color = \"red 70%\"\nwidget.styles.link_color = \"$accent\"\n# You can also use a `Color` object directly:\nwidget.styles.link_color = Color(100, 30, 173)\n
"},{"location":"styles/links/link_color/#see-also","title":"See also","text":"
  • link-background to set the background color of link text.
  • `link-hover-color to set the color of link text when the mouse pointer is over it.
"},{"location":"styles/links/link_hover_background/","title":"Link-hover-background","text":"

The link-hover-background style sets the background color of the link when the mouse cursor is over the link.

Note

link-hover-background only applies to Textual action links as described in the actions guide and not to regular hyperlinks.

"},{"location":"styles/links/link_hover_background/#syntax","title":"Syntax","text":"
\nlink-hover-background: <color> [<percentage>];\n

link-hover-background accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the background color of text enclosed in Textual action links when the mouse pointer is over it.

"},{"location":"styles/links/link_hover_background/#defaults","title":"Defaults","text":"

If not provided, a Textual action link will have link-hover-background set to $accent.

"},{"location":"styles/links/link_hover_background/#example","title":"Example","text":"

The example below shows some links that have their background colour changed when the mouse moves over it and it shows that there is a default color for link-hover-background.

It also shows that link-hover-background does not affect hyperlinks.

Outputlink_hover_background.pylink_hover_background.tcss

Note

The GIF has reduced quality to make it easier to load in the documentation. Try running the example yourself with textual run docs/examples/styles/link_hover_background.py.

from textual.app import App\nfrom textual.widgets import Label\nclass LinkHoverBackgroundApp(App):\ndef compose(self):\nyield Label(\n\"Visit the [link=https://textualize.io]Textualize[/link] website.\",\nid=\"lbl1\",  # (1)!\n)\nyield Label(\n\"Click [@click=app.bell]here[/] for the bell sound.\",\nid=\"lbl2\",  # (2)!\n)\nyield Label(\n\"You can also click [@click=app.bell]here[/] for the bell sound.\",\nid=\"lbl3\",  # (3)!\n)\nyield Label(\n\"[@click=app.quit]Exit this application.[/]\",\nid=\"lbl4\",  # (4)!\n)\napp = LinkHoverBackgroundApp(css_path=\"link_hover_background.tcss\")\n
  1. This label has an hyperlink so it won't be affected by the link-hover-background rule.
  2. This label has an \"action link\" that can be styled with link-hover-background.
  3. This label has an \"action link\" that can be styled with link-hover-background.
  4. This label has an \"action link\" that can be styled with link-hover-background.
#lbl1, #lbl2 {\nlink-hover-background: red;  /* (1)! */\n}\n#lbl3 {\nlink-hover-background: hsl(60,100%,50%) 50%;\n}\n#lbl4 {\n/* Empty to show the default hover background */ /* (2)! */\n}\n
  1. This will only affect one of the labels because action links are the only links that this rule affects.
  2. The default behavior for links on hover is to change to a different background color, so we don't need to change anything if all we want is to add emphasis to the link under the mouse.
"},{"location":"styles/links/link_hover_background/#css","title":"CSS","text":"
link-hover-background: red 70%;\nlink-hover-background: $accent;\n
"},{"location":"styles/links/link_hover_background/#python","title":"Python","text":"
widget.styles.link_hover_background = \"red 70%\"\nwidget.styles.link_hover_background = \"$accent\"\n# You can also use a `Color` object directly:\nwidget.styles.link_hover_background = Color(100, 30, 173)\n
"},{"location":"styles/links/link_hover_background/#see-also","title":"See also","text":"
  • link-background to set the background color of link text.
  • `link-hover-color to set the color of link text when the mouse pointer is over it.
  • `link-hover-style to set the style of link text when the mouse pointer is over it.
"},{"location":"styles/links/link_hover_color/","title":"Link-hover-color","text":"

The link-hover-color style sets the color of the link text when the mouse cursor is over the link.

Note

link-hover-color only applies to Textual action links as described in the actions guide and not to regular hyperlinks.

"},{"location":"styles/links/link_hover_color/#syntax","title":"Syntax","text":"
\nlink-hover-color: <color> [<percentage>];\n

link-hover-color accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the color of text enclosed in Textual action links when the mouse pointer is over it.

"},{"location":"styles/links/link_hover_color/#defaults","title":"Defaults","text":"

If not provided, a Textual action link will have link-hover-color set to white.

"},{"location":"styles/links/link_hover_color/#example","title":"Example","text":"

The example below shows some links that have their colour changed when the mouse moves over it. It also shows that link-hover-color does not affect hyperlinks.

Outputlink_hover_color.pylink_hover_color.tcss

Note

The background color also changes when the mouse moves over the links because that is the default behavior. That can be customised by setting link-hover-background but we haven't done so in this example.

Note

The GIF has reduced quality to make it easier to load in the documentation. Try running the example yourself with textual run docs/examples/styles/link_hover_color.py.

from textual.app import App\nfrom textual.widgets import Label\nclass LinkHoverColorApp(App):\ndef compose(self):\nyield Label(\n\"Visit the [link=https://textualize.io]Textualize[/link] website.\",\nid=\"lbl1\",  # (1)!\n)\nyield Label(\n\"Click [@click=app.bell]here[/] for the bell sound.\",\nid=\"lbl2\",  # (2)!\n)\nyield Label(\n\"You can also click [@click=app.bell]here[/] for the bell sound.\",\nid=\"lbl3\",  # (3)!\n)\nyield Label(\n\"[@click=app.quit]Exit this application.[/]\",\nid=\"lbl4\",  # (4)!\n)\napp = LinkHoverColorApp(css_path=\"link_hover_color.tcss\")\n
  1. This label has an hyperlink so it won't be affected by the link-hover-color rule.
  2. This label has an \"action link\" that can be styled with link-hover-color.
  3. This label has an \"action link\" that can be styled with link-hover-color.
  4. This label has an \"action link\" that can be styled with link-hover-color.
#lbl1, #lbl2 {\nlink-hover-color: red;  /* (1)! */\n}\n#lbl3 {\nlink-hover-color: hsl(60,100%,50%) 50%;\n}\n#lbl4 {\nlink-hover-color: black;\n}\n
  1. This will only affect one of the labels because action links are the only links that this rule affects.
"},{"location":"styles/links/link_hover_color/#css","title":"CSS","text":"
link-hover-color: red 70%;\nlink-hover-color: black;\n
"},{"location":"styles/links/link_hover_color/#python","title":"Python","text":"
widget.styles.link_hover_color = \"red 70%\"\nwidget.styles.link_hover_color = \"black\"\n# You can also use a `Color` object directly:\nwidget.styles.link_hover_color = Color(100, 30, 173)\n
"},{"location":"styles/links/link_hover_color/#see-also","title":"See also","text":"
  • link-color to set the color of link text.
  • `link-hover-background to set the background color of link text when the mouse pointer is over it.
  • `link-hover-style to set the style of link text when the mouse pointer is over it.
"},{"location":"styles/links/link_hover_style/","title":"Link-hover-style","text":"

The link-hover-style style sets the text style for the link text when the mouse cursor is over the link.

Note

link-hover-style only applies to Textual action links as described in the actions guide and not to regular hyperlinks.

"},{"location":"styles/links/link_hover_style/#syntax","title":"Syntax","text":"
\nlink-hover-style: <text-style>;\n

link-hover-style applies its <text-style> to the text of Textual action links when the mouse pointer is over them.

"},{"location":"styles/links/link_hover_style/#defaults","title":"Defaults","text":"

If not provided, a Textual action link will have link-hover-style set to bold.

"},{"location":"styles/links/link_hover_style/#example","title":"Example","text":"

The example below shows some links that have their colour changed when the mouse moves over it. It also shows that link-hover-style does not affect hyperlinks.

Outputlink_hover_style.pylink_hover_style.tcss

Note

The background color also changes when the mouse moves over the links because that is the default behavior. That can be customised by setting link-hover-background but we haven't done so in this example.

Note

The GIF has reduced quality to make it easier to load in the documentation. Try running the example yourself with textual run docs/examples/styles/link_hover_style.py.

from textual.app import App\nfrom textual.widgets import Label\nclass LinkHoverStyleApp(App):\ndef compose(self):\nyield Label(\n\"Visit the [link=https://textualize.io]Textualize[/link] website.\",\nid=\"lbl1\",  # (1)!\n)\nyield Label(\n\"Click [@click=app.bell]here[/] for the bell sound.\",\nid=\"lbl2\",  # (2)!\n)\nyield Label(\n\"You can also click [@click=app.bell]here[/] for the bell sound.\",\nid=\"lbl3\",  # (3)!\n)\nyield Label(\n\"[@click=app.quit]Exit this application.[/]\",\nid=\"lbl4\",  # (4)!\n)\napp = LinkHoverStyleApp(css_path=\"link_hover_style.tcss\")\n
  1. This label has an hyperlink so it won't be affected by the link-hover-style rule.
  2. This label has an \"action link\" that can be styled with link-hover-style.
  3. This label has an \"action link\" that can be styled with link-hover-style.
  4. This label has an \"action link\" that can be styled with link-hover-style.
#lbl1, #lbl2 {\nlink-hover-style: bold italic;  /* (1)! */\n}\n#lbl3 {\nlink-hover-style: reverse strike;\n}\n#lbl4 {\nlink-hover-style: bold;\n}\n
  1. This will only affect one of the labels because action links are the only links that this rule affects.
  2. The default behavior for links on hover is to change to a different text style, so we don't need to change anything if all we want is to add emphasis to the link under the mouse.
"},{"location":"styles/links/link_hover_style/#css","title":"CSS","text":"
link-hover-style: bold;\nlink-hover-style: bold italic reverse;\n
"},{"location":"styles/links/link_hover_style/#python","title":"Python","text":"
widget.styles.link_hover_style = \"bold\"\nwidget.styles.link_hover_style = \"bold italic reverse\"\n
"},{"location":"styles/links/link_hover_style/#see-also","title":"See also","text":"
  • `link-hover-background to set the background color of link text when the mouse pointer is over it.
  • `link-hover-color to set the color of link text when the mouse pointer is over it.
  • link-style to set the style of link text.
  • text-style to set the style of text in a widget.
"},{"location":"styles/links/link_style/","title":"Link-style","text":"

The link-style style sets the text style for the link text.

Note

link-style only applies to Textual action links as described in the actions guide and not to regular hyperlinks.

"},{"location":"styles/links/link_style/#syntax","title":"Syntax","text":"
\nlink-style: <text-style>;\n

link-style will take all the values specified and will apply that styling to text that is enclosed by a Textual action link.

"},{"location":"styles/links/link_style/#defaults","title":"Defaults","text":"

If not provided, a Textual action link will have link-style set to underline.

"},{"location":"styles/links/link_style/#example","title":"Example","text":"

The example below shows some links with different styles applied to their text. It also shows that link-style does not affect hyperlinks.

Outputlink_style.pylink_style.tcss

LinkStyleApp Visit\u00a0the\u00a0Textualize\u00a0website. Click\u00a0here\u00a0for\u00a0the\u00a0bell\u00a0sound. You\u00a0can\u00a0also\u00a0click\u00a0here\u00a0for\u00a0the\u00a0bell\u00a0sound. Exit\u00a0this\u00a0application.

from textual.app import App\nfrom textual.widgets import Label\nclass LinkStyleApp(App):\ndef compose(self):\nyield Label(\n\"Visit the [link=https://textualize.io]Textualize[/link] website.\",\nid=\"lbl1\",  # (1)!\n)\nyield Label(\n\"Click [@click=app.bell]here[/] for the bell sound.\",\nid=\"lbl2\",  # (2)!\n)\nyield Label(\n\"You can also click [@click=app.bell]here[/] for the bell sound.\",\nid=\"lbl3\",  # (3)!\n)\nyield Label(\n\"[@click=app.quit]Exit this application.[/]\",\nid=\"lbl4\",  # (4)!\n)\napp = LinkStyleApp(css_path=\"link_style.tcss\")\n
  1. This label has an hyperlink so it won't be affected by the link-style rule.
  2. This label has an \"action link\" that can be styled with link-style.
  3. This label has an \"action link\" that can be styled with link-style.
  4. This label has an \"action link\" that can be styled with link-style.
#lbl1, #lbl2 {\nlink-style: bold italic;  /* (1)! */\n}\n#lbl3 {\nlink-style: reverse strike;\n}\n#lbl4 {\nlink-style: bold;\n}\n
  1. This will only affect one of the labels because action links are the only links that this rule affects.
"},{"location":"styles/links/link_style/#css","title":"CSS","text":"
link-style: bold;\nlink-style: bold italic reverse;\n
"},{"location":"styles/links/link_style/#python","title":"Python","text":"
widget.styles.link_style = \"bold\"\nwidget.styles.link_style = \"bold italic reverse\"\n
"},{"location":"styles/links/link_style/#see-also","title":"See also","text":"
  • `link-hover-style to set the style of link text when the mouse pointer is over it.
  • text-style to set the style of text in a widget.
"},{"location":"styles/scrollbar_colors/","title":"Scrollbar colors","text":"

There are a number of styles to set the colors used in Textual scrollbars. You won't typically need to do this, as the default themes have carefully chosen colors, but you can if you want to.

Style Applies to scrollbar-background Scrollbar background. scrollbar-background-active Scrollbar background when the thumb is being dragged. scrollbar-background-hover Scrollbar background when the mouse is hovering over it. scrollbar-color Scrollbar \"thumb\" (movable part). scrollbar-color-active Scrollbar thumb when it is active (being dragged). scrollbar-color-hover Scrollbar thumb when the mouse is hovering over it. scrollbar-corner-color The gap between the horizontal and vertical scrollbars."},{"location":"styles/scrollbar_colors/#syntax","title":"Syntax","text":"
\nscrollbar-background: <color> [<percentage>];\n\nscrollbar-background-active: <color> [<percentage>];\n\nscrollbar-background-hover: <color> [<percentage>];\n\nscrollbar-color: <color> [<percentage>];\n\nscrollbar-color-active: <color> [<percentage>];\n\nscrollbar-color-hover: <color> [<percentage>];\n\nscrollbar-corner-color: <color> [<percentage>];\n

Visit each style's reference page to learn more about how the values are used.

"},{"location":"styles/scrollbar_colors/#example","title":"Example","text":"

This example shows two panels that contain oversized text. The right panel sets scrollbar-background, scrollbar-color, and scrollbar-corner-color, and the left panel shows the default colors for comparison.

Outputscrollbars.pyscrollbars.tcss

ScrollbarApp I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0tFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0t I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0tI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0t And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turnAnd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn see\u00a0its\u00a0path.see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0 will\u00a0remain.will\u00a0remain. I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0tFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0t I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0tI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0t And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turnAnd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn see\u00a0its\u00a0path.\u2583\u2583see\u00a0its\u00a0path.\u2583\u2583 Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0 will\u00a0remain.will\u00a0remain. I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0tFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0t I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0tI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0t \u258d\u258d

from textual.app import App\nfrom textual.containers import Horizontal, ScrollableContainer\nfrom textual.widgets import Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\nclass ScrollbarApp(App):\ndef compose(self):\nyield Horizontal(\nScrollableContainer(Label(TEXT * 10)),\nScrollableContainer(Label(TEXT * 10), classes=\"right\"),\n)\napp = ScrollbarApp(css_path=\"scrollbars.tcss\")\nif __name__ == \"__main__\":\napp.run()\n
Label {\nwidth: 150%;\nheight: 150%;\n}\n.right {\nscrollbar-background: red;\nscrollbar-color: green;\nscrollbar-corner-color: blue;\n}\nHorizontal > ScrollableContainer {\nwidth: 50%;\n}\n
"},{"location":"styles/scrollbar_colors/scrollbar_background/","title":"Scrollbar-background","text":"

The scrollbar-background style sets the background color of the scrollbar.

"},{"location":"styles/scrollbar_colors/scrollbar_background/#syntax","title":"Syntax","text":"
\nscrollbar-background: <color> [<percentage>];\n

scrollbar-background accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the background color of a scrollbar.

"},{"location":"styles/scrollbar_colors/scrollbar_background/#example","title":"Example","text":"Outputscrollbars2.pyscrollbars2.tcss

Note

The GIF above has reduced quality to make it easier to load in the documentation. Try running the example yourself with textual run docs/examples/styles/scrollbars2.py.

from textual.app import App\nfrom textual.widgets import Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\nclass Scrollbar2App(App):\ndef compose(self):\nyield Label(TEXT * 10)\napp = Scrollbar2App(css_path=\"scrollbars2.tcss\")\n
Screen {\nscrollbar-background: blue;\nscrollbar-background-active: red;\nscrollbar-background-hover: purple;\nscrollbar-color: cyan;\nscrollbar-color-active: yellow;\nscrollbar-color-hover: pink;\n}\n
"},{"location":"styles/scrollbar_colors/scrollbar_background/#css","title":"CSS","text":"
scrollbar-backround: blue;\n
"},{"location":"styles/scrollbar_colors/scrollbar_background/#python","title":"Python","text":"
widget.styles.scrollbar_background = \"blue\"\n
"},{"location":"styles/scrollbar_colors/scrollbar_background/#see-also","title":"See also","text":"
  • scrollbar-bakcground-active to set the scrollbar bakcground color when the scrollbar is being dragged.
  • scrollbar-bakcground-hover to set the scrollbar bakcground color when the mouse pointer is over it.
  • scrollbar-color to set the color of scrollbars.
  • scrollbar-corner-color to set the color of the corner where horizontal and vertical scrollbars meet.
"},{"location":"styles/scrollbar_colors/scrollbar_background_active/","title":"Scrollbar-background-active","text":"

The scrollbar-background-active style sets the background color of the scrollbar when the thumb is being dragged.

"},{"location":"styles/scrollbar_colors/scrollbar_background_active/#syntax","title":"Syntax","text":"
\nscrollbar-background-active: <color> [<percentage>];\n

scrollbar-background-active accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the background color of a scrollbar when its thumb is being dragged.

"},{"location":"styles/scrollbar_colors/scrollbar_background_active/#example","title":"Example","text":"Outputscrollbars2.pyscrollbars2.tcss

Note

The GIF above has reduced quality to make it easier to load in the documentation. Try running the example yourself with textual run docs/examples/styles/scrollbars2.py.

from textual.app import App\nfrom textual.widgets import Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\nclass Scrollbar2App(App):\ndef compose(self):\nyield Label(TEXT * 10)\napp = Scrollbar2App(css_path=\"scrollbars2.tcss\")\n
Screen {\nscrollbar-background: blue;\nscrollbar-background-active: red;\nscrollbar-background-hover: purple;\nscrollbar-color: cyan;\nscrollbar-color-active: yellow;\nscrollbar-color-hover: pink;\n}\n
"},{"location":"styles/scrollbar_colors/scrollbar_background_active/#css","title":"CSS","text":"
scrollbar-backround-active: red;\n
"},{"location":"styles/scrollbar_colors/scrollbar_background_active/#python","title":"Python","text":"
widget.styles.scrollbar_background_active = \"red\"\n
"},{"location":"styles/scrollbar_colors/scrollbar_background_active/#see-also","title":"See also","text":"
  • scrollbar-background to set the background color of scrollbars.
  • scrollbar-bakcground-hover to set the scrollbar bakcground color when the mouse pointer is over it.
  • scrollbar-color-active to set the scrollbar color when the scrollbar is being dragged.
"},{"location":"styles/scrollbar_colors/scrollbar_background_hover/","title":"Scrollbar-background-hover","text":"

The scrollbar-background-hover style sets the background color of the scrollbar when the cursor is over it.

"},{"location":"styles/scrollbar_colors/scrollbar_background_hover/#syntax","title":"Syntax","text":"
\nscrollbar-background-hover: <color> [<percentage>];\n

scrollbar-background-hover accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the background color of a scrollbar when the cursor is over it.

"},{"location":"styles/scrollbar_colors/scrollbar_background_hover/#example","title":"Example","text":"Outputscrollbars2.pyscrollbars2.tcss

Note

The GIF above has reduced quality to make it easier to load in the documentation. Try running the example yourself with textual run docs/examples/styles/scrollbars2.py.

from textual.app import App\nfrom textual.widgets import Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\nclass Scrollbar2App(App):\ndef compose(self):\nyield Label(TEXT * 10)\napp = Scrollbar2App(css_path=\"scrollbars2.tcss\")\n
Screen {\nscrollbar-background: blue;\nscrollbar-background-active: red;\nscrollbar-background-hover: purple;\nscrollbar-color: cyan;\nscrollbar-color-active: yellow;\nscrollbar-color-hover: pink;\n}\n
"},{"location":"styles/scrollbar_colors/scrollbar_background_hover/#css","title":"CSS","text":"
scrollbar-background-hover: purple;\n
"},{"location":"styles/scrollbar_colors/scrollbar_background_hover/#python","title":"Python","text":"
widget.styles.scrollbar_background_hover = \"purple\"\n
"},{"location":"styles/scrollbar_colors/scrollbar_background_hover/#see-also","title":"See also","text":""},{"location":"styles/scrollbar_colors/scrollbar_background_hover/#see-also_1","title":"See also","text":"
  • scrollbar-background to set the background color of scrollbars.
  • scrollbar-bakcground-active to set the scrollbar bakcground color when the scrollbar is being dragged.
  • scrollbar-color-hover to set the scrollbar color when the mouse pointer is over it.
"},{"location":"styles/scrollbar_colors/scrollbar_color/","title":"Scrollbar-color","text":"

The scrollbar-color style sets the color of the scrollbar.

"},{"location":"styles/scrollbar_colors/scrollbar_color/#syntax","title":"Syntax","text":"
\nscrollbar-color: <color> [<percentage>];\n

scrollbar-color accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the color of a scrollbar.

"},{"location":"styles/scrollbar_colors/scrollbar_color/#example","title":"Example","text":"Outputscrollbars2.pyscrollbars2.tcss

Note

The GIF above has reduced quality to make it easier to load in the documentation. Try running the example yourself with textual run docs/examples/styles/scrollbars2.py.

from textual.app import App\nfrom textual.widgets import Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\nclass Scrollbar2App(App):\ndef compose(self):\nyield Label(TEXT * 10)\napp = Scrollbar2App(css_path=\"scrollbars2.tcss\")\n
Screen {\nscrollbar-background: blue;\nscrollbar-background-active: red;\nscrollbar-background-hover: purple;\nscrollbar-color: cyan;\nscrollbar-color-active: yellow;\nscrollbar-color-hover: pink;\n}\n
"},{"location":"styles/scrollbar_colors/scrollbar_color/#css","title":"CSS","text":"
scrollbar-color: cyan;\n
"},{"location":"styles/scrollbar_colors/scrollbar_color/#python","title":"Python","text":"
widget.styles.scrollbar_color = \"cyan\"\n
"},{"location":"styles/scrollbar_colors/scrollbar_color/#see-also","title":"See also","text":"
  • scrollbar-background to set the background color of scrollbars.
  • scrollbar-color-active to set the scrollbar color when the scrollbar is being dragged.
  • scrollbar-color-hover to set the scrollbar color when the mouse pointer is over it.
  • scrollbar-corner-color to set the color of the corner where horizontal and vertical scrollbars meet.
"},{"location":"styles/scrollbar_colors/scrollbar_color_active/","title":"Scrollbar-color-active","text":"

The scrollbar-color-active style sets the color of the scrollbar when the thumb is being dragged.

"},{"location":"styles/scrollbar_colors/scrollbar_color_active/#syntax","title":"Syntax","text":"
\nscrollbar-color-active: <color> [<percentage>];\n

scrollbar-color-active accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the color of a scrollbar when its thumb is being dragged.

"},{"location":"styles/scrollbar_colors/scrollbar_color_active/#example","title":"Example","text":"Outputscrollbars2.pyscrollbars2.tcss

Note

The GIF above has reduced quality to make it easier to load in the documentation. Try running the example yourself with textual run docs/examples/styles/scrollbars2.py.

from textual.app import App\nfrom textual.widgets import Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\nclass Scrollbar2App(App):\ndef compose(self):\nyield Label(TEXT * 10)\napp = Scrollbar2App(css_path=\"scrollbars2.tcss\")\n
Screen {\nscrollbar-background: blue;\nscrollbar-background-active: red;\nscrollbar-background-hover: purple;\nscrollbar-color: cyan;\nscrollbar-color-active: yellow;\nscrollbar-color-hover: pink;\n}\n
"},{"location":"styles/scrollbar_colors/scrollbar_color_active/#css","title":"CSS","text":"
scrollbar-color-active: yellow;\n
"},{"location":"styles/scrollbar_colors/scrollbar_color_active/#python","title":"Python","text":"
widget.styles.scrollbar_color_active = \"yellow\"\n
"},{"location":"styles/scrollbar_colors/scrollbar_color_active/#see-also","title":"See also","text":"
  • scrollbar-bakcground-active to set the scrollbar bakcground color when the scrollbar is being dragged.
  • scrollbar-color to set the color of scrollbars.
  • scrollbar-color-hover to set the scrollbar color when the mouse pointer is over it.
"},{"location":"styles/scrollbar_colors/scrollbar_color_hover/","title":"Scrollbar-color-hover","text":"

The scrollbar-color-hover style sets the color of the scrollbar when the cursor is over it.

"},{"location":"styles/scrollbar_colors/scrollbar_color_hover/#syntax","title":"Syntax","text":"
\nscrollbar-color-hover: <color> [<percentage>];\n

scrollbar-color-hover accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the color of a scrollbar when the cursor is over it.

"},{"location":"styles/scrollbar_colors/scrollbar_color_hover/#example","title":"Example","text":"Outputscrollbars2.pyscrollbars2.tcss

Note

The GIF above has reduced quality to make it easier to load in the documentation. Try running the example yourself with textual run docs/examples/styles/scrollbars2.py.

from textual.app import App\nfrom textual.widgets import Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\nclass Scrollbar2App(App):\ndef compose(self):\nyield Label(TEXT * 10)\napp = Scrollbar2App(css_path=\"scrollbars2.tcss\")\n
Screen {\nscrollbar-background: blue;\nscrollbar-background-active: red;\nscrollbar-background-hover: purple;\nscrollbar-color: cyan;\nscrollbar-color-active: yellow;\nscrollbar-color-hover: pink;\n}\n
"},{"location":"styles/scrollbar_colors/scrollbar_color_hover/#css","title":"CSS","text":"
scrollbar-color-hover: pink;\n
"},{"location":"styles/scrollbar_colors/scrollbar_color_hover/#python","title":"Python","text":"
widget.styles.scrollbar_color_hover = \"pink\"\n
"},{"location":"styles/scrollbar_colors/scrollbar_color_hover/#see-also","title":"See also","text":"
  • scrollbar-bakcground-hover to set the scrollbar bakcground color when the mouse pointer is over it.
  • scrollbar-color to set the color of scrollbars.
  • scrollbar-color-active to set the scrollbar color when the scrollbar is being dragged.
"},{"location":"styles/scrollbar_colors/scrollbar_corner_color/","title":"Scrollbar-corner-color","text":"

The scrollbar-corner-color style sets the color of the gap between the horizontal and vertical scrollbars.

"},{"location":"styles/scrollbar_colors/scrollbar_corner_color/#syntax","title":"Syntax","text":"
\nscrollbar-corner-color: <color> [<percentage>];\n

scrollbar-corner-color accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the color of the gap between the horizontal and vertical scrollbars of a widget.

"},{"location":"styles/scrollbar_colors/scrollbar_corner_color/#example","title":"Example","text":"

The example below sets the scrollbar corner (bottom-right corner of the screen) to white.

Outputscrollbar_corner_color.pyscrollbar_corner_color.tcss

ScrollbarCornerColorApp I\u00a0must\u00a0not\u00a0fear.\u00a0Fear\u00a0is\u00a0the\u00a0mind-killer.\u00a0Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.

from textual.app import App\nfrom textual.widgets import Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\nclass ScrollbarCornerColorApp(App):\ndef compose(self):\nyield Label(TEXT.replace(\"\\n\", \" \") + \"\\n\" + TEXT * 10)\napp = ScrollbarCornerColorApp(css_path=\"scrollbar_corner_color.tcss\")\n
Screen {\noverflow: auto auto;\nscrollbar-corner-color: white;\n}\n
"},{"location":"styles/scrollbar_colors/scrollbar_corner_color/#css","title":"CSS","text":"
scrollbar-corner-color: white;\n
"},{"location":"styles/scrollbar_colors/scrollbar_corner_color/#python","title":"Python","text":"
widget.styles.scrollbar_corner_color = \"white\"\n
"},{"location":"styles/scrollbar_colors/scrollbar_corner_color/#see-also","title":"See also","text":"
  • scrollbar-background to set the background color of scrollbars.
  • scrollbar-color to set the color of scrollbars.
"},{"location":"widgets/","title":"Widgets","text":"

A reference to the builtin widgets.

See the links to the left of the page, or in the hamburger menu (three horizontal bars, top left).

"},{"location":"widgets/button/","title":"Button","text":"

A simple button widget which can be pressed using a mouse click or by pressing Enter when it has focus.

  • Focusable
  • Container
"},{"location":"widgets/button/#example","title":"Example","text":"

The example below shows each button variant, and its disabled equivalent. Clicking any of the non-disabled buttons in the example app below will result in the app exiting and the details of the selected button being printed to the console.

Outputbutton.pybutton.tcss

ButtonsApp Standard\u00a0ButtonsDisabled\u00a0Buttons \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Default\u00a0\u00a0Default\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Primary!\u00a0\u00a0Primary!\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Success!\u00a0\u00a0Success!\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Warning!\u00a0\u00a0Warning!\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Error!\u00a0\u00a0Error!\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal, VerticalScroll\nfrom textual.widgets import Button, Static\nclass ButtonsApp(App[str]):\nCSS_PATH = \"button.tcss\"\ndef compose(self) -> ComposeResult:\nyield Horizontal(\nVerticalScroll(\nStatic(\"Standard Buttons\", classes=\"header\"),\nButton(\"Default\"),\nButton(\"Primary!\", variant=\"primary\"),\nButton.success(\"Success!\"),\nButton.warning(\"Warning!\"),\nButton.error(\"Error!\"),\n),\nVerticalScroll(\nStatic(\"Disabled Buttons\", classes=\"header\"),\nButton(\"Default\", disabled=True),\nButton(\"Primary!\", variant=\"primary\", disabled=True),\nButton.success(\"Success!\", disabled=True),\nButton.warning(\"Warning!\", disabled=True),\nButton.error(\"Error!\", disabled=True),\n),\n)\ndef on_button_pressed(self, event: Button.Pressed) -> None:\nself.exit(str(event.button))\nif __name__ == \"__main__\":\napp = ButtonsApp()\nprint(app.run())\n
Button {\nmargin: 1 2;\n}\nHorizontal > VerticalScroll {\nwidth: 24;\n}\n.header {\nmargin: 1 0 0 2;\ntext-style: bold;\n}\n
"},{"location":"widgets/button/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description label str \"\" The text that appears inside the button. variant ButtonVariant \"default\" Semantic styling variant. One of default, primary, success, warning, error. disabled bool False Whether the button is disabled or not. Disabled buttons cannot be focused or clicked, and are styled in a way that suggests this."},{"location":"widgets/button/#messages","title":"Messages","text":"
  • Button.Pressed
"},{"location":"widgets/button/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/button/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/button/#additional-notes","title":"Additional Notes","text":"
  • The spacing between the text and the edges of a button are not due to padding. The default styling for a Button has the height set to 3 lines and a min-width of 16 columns. To create a button with zero visible padding, you will need to change these values and also remove the border with border: none;.
"},{"location":"widgets/button/#textual.widgets.Button","title":"textual.widgets.Button class","text":"
def __init__(\nself,\nlabel=None,\nvariant=\"default\",\n*,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: Static

A simple clickable button.

Parameters Name Type Description Default label TextType | None

The text that appears within the button.

None variant ButtonVariant

The variant of the button.

'default' name str | None

The name of the button.

None id str | None

The ID of the button in the DOM.

None classes str | None

The CSS classes of the button.

None disabled bool

Whether the button is disabled or not.

False"},{"location":"widgets/button/#textual.widgets._button.Button.ACTIVE_EFFECT_DURATION","title":"ACTIVE_EFFECT_DURATION class-attribute instance-attribute","text":"
ACTIVE_EFFECT_DURATION = 0.3\n

When buttons are clicked they get the -active class for this duration (in seconds)

"},{"location":"widgets/button/#textual.widgets._button.Button.label","title":"label class-attribute instance-attribute","text":"
label: reactive[TextType] = self.validate_label(label)\n

The text label that appears within the button.

"},{"location":"widgets/button/#textual.widgets._button.Button.variant","title":"variant class-attribute instance-attribute","text":"
variant = self.validate_variant(variant)\n

The variant name for the button.

"},{"location":"widgets/button/#textual.widgets._button.Button.Pressed","title":"Pressed class","text":"
def __init__(self, button):\n

Bases: Message

Event sent when a Button is pressed.

Can be handled using on_button_pressed in a subclass of Button or in a parent widget in the DOM.

"},{"location":"widgets/button/#textual.widgets._button.Button.Pressed.button","title":"button instance-attribute","text":"
button: Button = button\n

The button that was pressed.

"},{"location":"widgets/button/#textual.widgets._button.Button.Pressed.control","title":"control property","text":"
control: Button\n

An alias for Pressed.button.

This will be the same value as Pressed.button.

"},{"location":"widgets/button/#textual.widgets._button.Button.action_press","title":"action_press method","text":"
def action_press(self):\n

Activate a press of the button.

"},{"location":"widgets/button/#textual.widgets._button.Button.error","title":"error classmethod","text":"
def error(\ncls,\nlabel=None,\n*,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Utility constructor for creating an error Button variant.

Parameters Name Type Description Default label TextType | None

The text that appears within the button.

None disabled bool

Whether the button is disabled or not.

False name str | None

The name of the button.

None id str | None

The ID of the button in the DOM.

None classes str | None

The CSS classes of the button.

None disabled bool

Whether the button is disabled or not.

False Returns Type Description Button

A Button widget of the 'error' variant.

"},{"location":"widgets/button/#textual.widgets._button.Button.press","title":"press method","text":"
def press(self):\n

Respond to a button press.

Returns Type Description Self

The button instance.

"},{"location":"widgets/button/#textual.widgets._button.Button.success","title":"success classmethod","text":"
def success(\ncls,\nlabel=None,\n*,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Utility constructor for creating a success Button variant.

Parameters Name Type Description Default label TextType | None

The text that appears within the button.

None disabled bool

Whether the button is disabled or not.

False name str | None

The name of the button.

None id str | None

The ID of the button in the DOM.

None classes str | None

The CSS classes of the button.

None disabled bool

Whether the button is disabled or not.

False Returns Type Description Button

A Button widget of the 'success' variant.

"},{"location":"widgets/button/#textual.widgets._button.Button.validate_label","title":"validate_label method","text":"
def validate_label(self, label):\n

Parse markup for self.label

"},{"location":"widgets/button/#textual.widgets._button.Button.warning","title":"warning classmethod","text":"
def warning(\ncls,\nlabel=None,\n*,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Utility constructor for creating a warning Button variant.

Parameters Name Type Description Default label TextType | None

The text that appears within the button.

None disabled bool

Whether the button is disabled or not.

False name str | None

The name of the button.

None id str | None

The ID of the button in the DOM.

None classes str | None

The CSS classes of the button.

None disabled bool

Whether the button is disabled or not.

False Returns Type Description Button

A Button widget of the 'warning' variant.

"},{"location":"widgets/button/#textual.widgets.button","title":"textual.widgets.button","text":""},{"location":"widgets/button/#textual.widgets.button.ButtonVariant","title":"ButtonVariant module-attribute","text":"
ButtonVariant = Literal[\n\"default\", \"primary\", \"success\", \"warning\", \"error\"\n]\n

The names of the valid button variants.

These are the variants that can be used with a Button.

"},{"location":"widgets/checkbox/","title":"Checkbox","text":"

Added in version 0.13.0

A simple checkbox widget which stores a boolean value.

  • Focusable
  • Container
"},{"location":"widgets/checkbox/#example","title":"Example","text":"

The example below shows check boxes in various states.

Outputcheckbox.pycheckbox.tcss

CheckboxApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258c\u00a0Arrakis\u00a0\ud83d\ude13\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258c\u00a0Caladan\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258c\u00a0Chusuk\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258cGiedi\u00a0Prime\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258cGinaz\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258c\u00a0Grumman\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2583\u2583 \u258a\u2590X\u258cKaitain\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e

from textual.app import App, ComposeResult\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Checkbox\nclass CheckboxApp(App[None]):\nCSS_PATH = \"checkbox.tcss\"\ndef compose(self) -> ComposeResult:\nwith VerticalScroll():\nyield Checkbox(\"Arrakis :sweat:\")\nyield Checkbox(\"Caladan\")\nyield Checkbox(\"Chusuk\")\nyield Checkbox(\"[b]Giedi Prime[/b]\")\nyield Checkbox(\"[magenta]Ginaz[/]\")\nyield Checkbox(\"Grumman\", True)\nyield Checkbox(\"Kaitain\", id=\"initial_focus\")\nyield Checkbox(\"Novebruns\", True)\ndef on_mount(self):\nself.query_one(\"#initial_focus\", Checkbox).focus()\nif __name__ == \"__main__\":\nCheckboxApp().run()\n
Screen {\nalign: center middle;\n}\nVerticalScroll {\nwidth: auto;\nheight: auto;\nbackground: $boost;\npadding: 2;\n}\n
"},{"location":"widgets/checkbox/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description value bool False The value of the checkbox."},{"location":"widgets/checkbox/#messages","title":"Messages","text":"
  • Checkbox.Changed
"},{"location":"widgets/checkbox/#bindings","title":"Bindings","text":"

The checkbox widget defines the following bindings:

Key(s) Description enter, space Toggle the value."},{"location":"widgets/checkbox/#component-classes","title":"Component Classes","text":"

The checkbox widget inherits the following component classes:

Class Description toggle--button Targets the toggle button itself. toggle--label Targets the text label of the toggle button."},{"location":"widgets/checkbox/#textual.widgets.Checkbox","title":"textual.widgets.Checkbox class","text":"

Bases: ToggleButton

A check box widget that represents a boolean value.

"},{"location":"widgets/checkbox/#textual.widgets._checkbox.Checkbox.Changed","title":"Changed class","text":"

Bases: ToggleButton.Changed

Posted when the value of the checkbox changes.

This message can be handled using an on_checkbox_changed method.

"},{"location":"widgets/checkbox/#textual.widgets._checkbox.Checkbox.Changed.checkbox","title":"checkbox property","text":"
checkbox: Checkbox\n

The checkbox that was changed.

"},{"location":"widgets/checkbox/#textual.widgets._checkbox.Checkbox.Changed.control","title":"control property","text":"
control: Checkbox\n

An alias for Changed.checkbox.

"},{"location":"widgets/collapsible/","title":"Collapsible","text":"

Added in version 0.37

A container with a title that can be used to show (expand) or hide (collapse) content, either by clicking or focusing and pressing Enter.

  • Focusable
  • Container
"},{"location":"widgets/collapsible/#composing","title":"Composing","text":"

You can add content to a Collapsible widget either by passing in children to the constructor, or with a context manager (with statement).

Here is an example of using the constructor to add content:

def compose(self) -> ComposeResult:\nyield Collapsible(Label(\"Hello, world.\"))\n

Here's how the to use it with the context manager:

def compose(self) -> ComposeResult:\nwith Collapsible():\nyield Label(\"Hello, world.\")\n

The second form is generally preferred, but the end result is the same.

"},{"location":"widgets/collapsible/#title","title":"Title","text":"

The default title \"Toggle\" can be customized by setting the title parameter of the constructor:

def compose(self) -> ComposeResult:\nwith Collapsible(title=\"An interesting story.\"):\nyield Label(\"Interesting but verbose story.\")\n
"},{"location":"widgets/collapsible/#initial-state","title":"Initial State","text":"

The initial state of the Collapsible widget can be customized via the collapsed parameter of the constructor:

def compose(self) -> ComposeResult:\nwith Collapsible(title=\"Contents 1\", collapsed=False):\nyield Label(\"Hello, world.\")\nwith Collapsible(title=\"Contents 2\", collapsed=True):  # Default.\nyield Label(\"Hello, world.\")\n
"},{"location":"widgets/collapsible/#collapseexpand-symbols","title":"Collapse/Expand Symbols","text":"

The symbols used to show the collapsed / expanded state can be customized by setting the parameters collapsed_symbol and expanded_symbol:

def compose(self) -> ComposeResult:\nwith Collapsible(collapsed_symbol=\">>>\", expanded_symbol=\"v\"):\nyield Label(\"Hello, world.\")\n
"},{"location":"widgets/collapsible/#examples","title":"Examples","text":"

The following example contains three Collapsibles in different states.

All expandedAll collapsedMixedcollapsible.py

CollapsibleApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25bc\u00a0Leto #\u00a0Duke\u00a0Leto\u00a0I\u00a0Atreides Head\u00a0of\u00a0House\u00a0Atreides. \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25bc\u00a0Jessica \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eLady\u00a0Jessica\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Bene\u00a0Gesserit\u00a0and\u00a0concubine\u00a0of\u00a0Leto,\u00a0and\u00a0mother\u00a0of\u00a0Paul\u00a0and\u00a0Alia. \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25bc\u00a0Paul \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 C\u00a0\u00a0Collapse\u00a0All\u00a0\u00a0E\u00a0\u00a0Expand\u00a0All\u00a0

CollapsibleApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25b6\u00a0Leto \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25b6\u00a0Jessica \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25b6\u00a0Paul \u00a0C\u00a0\u00a0Collapse\u00a0All\u00a0\u00a0E\u00a0\u00a0Expand\u00a0All\u00a0

CollapsibleApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25bc\u00a0Leto #\u00a0Duke\u00a0Leto\u00a0I\u00a0Atreides Head\u00a0of\u00a0House\u00a0Atreides. \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25bc\u00a0Jessica \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eLady\u00a0Jessica\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Bene\u00a0Gesserit\u00a0and\u00a0concubine\u00a0of\u00a0Leto,\u00a0and\u00a0mother\u00a0of\u00a0Paul\u00a0and\u00a0Alia. \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25b6\u00a0Paul \u00a0C\u00a0\u00a0Collapse\u00a0All\u00a0\u00a0E\u00a0\u00a0Expand\u00a0All\u00a0

from textual.app import App, ComposeResult\nfrom textual.widgets import Collapsible, Footer, Label, Markdown\nLETO = \"\"\"\\\n# Duke Leto I Atreides\nHead of House Atreides.\"\"\"\nJESSICA = \"\"\"\n# Lady Jessica\nBene Gesserit and concubine of Leto, and mother of Paul and Alia.\n\"\"\"\nPAUL = \"\"\"\n# Paul Atreides\nSon of Leto and Jessica.\n\"\"\"\nclass CollapsibleApp(App[None]):\n\"\"\"An example of collapsible container.\"\"\"\nBINDINGS = [\n(\"c\", \"collapse_or_expand(True)\", \"Collapse All\"),\n(\"e\", \"collapse_or_expand(False)\", \"Expand All\"),\n]\ndef compose(self) -> ComposeResult:\n\"\"\"Compose app with collapsible containers.\"\"\"\nyield Footer()\nwith Collapsible(collapsed=False, title=\"Leto\"):\nyield Label(LETO)\nyield Collapsible(Markdown(JESSICA), collapsed=False, title=\"Jessica\")\nwith Collapsible(collapsed=True, title=\"Paul\"):\nyield Markdown(PAUL)\ndef action_collapse_or_expand(self, collapse: bool) -> None:\nfor child in self.walk_children(Collapsible):\nchild.collapsed = collapse\nif __name__ == \"__main__\":\napp = CollapsibleApp()\napp.run()\n
"},{"location":"widgets/collapsible/#setting-initial-state","title":"Setting Initial State","text":"

The example below shows nested Collapsible widgets and how to set their initial state.

Outputcollapsible_nested.py

CollapsibleApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25bc\u00a0Toggle \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25b6\u00a0Toggle

from textual.app import App, ComposeResult\nfrom textual.widgets import Collapsible, Label\nclass CollapsibleApp(App[None]):\ndef compose(self) -> ComposeResult:\nwith Collapsible(collapsed=False):\nwith Collapsible():\nyield Label(\"Hello, world.\")\nif __name__ == \"__main__\":\napp = CollapsibleApp()\napp.run()\n
"},{"location":"widgets/collapsible/#custom-symbols","title":"Custom Symbols","text":"

The following example shows Collapsible widgets with custom expand/collapse symbols.

Outputcollapsible_custom_symbol.py

CollapsibleApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 >>>\u00a0Togglev\u00a0Toggle Hello,\u00a0world.

from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.widgets import Collapsible, Label\nclass CollapsibleApp(App[None]):\ndef compose(self) -> ComposeResult:\nwith Horizontal():\nwith Collapsible(\ncollapsed_symbol=\">>>\",\nexpanded_symbol=\"v\",\n):\nyield Label(\"Hello, world.\")\nwith Collapsible(\ncollapsed_symbol=\">>>\",\nexpanded_symbol=\"v\",\ncollapsed=False,\n):\nyield Label(\"Hello, world.\")\nif __name__ == \"__main__\":\napp = CollapsibleApp()\napp.run()\n
"},{"location":"widgets/collapsible/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description collapsed bool True Controls the collapsed/expanded state of the widget."},{"location":"widgets/collapsible/#messages","title":"Messages","text":"

This widget posts no messages.

"},{"location":"widgets/collapsible/#bindings","title":"Bindings","text":"

The collapsible widget defines the following binding on its title:

Key(s) Description enter Toggle the collapsible."},{"location":"widgets/collapsible/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/collapsible/#textual.widgets.Collapsible","title":"textual.widgets.Collapsible class","text":"
def __init__(\nself,\n*children,\ntitle=\"Toggle\",\ncollapsed=True,\ncollapsed_symbol=\"\u25b6\",\nexpanded_symbol=\"\u25bc\",\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: Widget

A collapsible container.

Parameters Name Type Description Default *children Widget

Contents that will be collapsed/expanded.

() title str

Title of the collapsed/expanded contents.

'Toggle' collapsed bool

Default status of the contents.

True collapsed_symbol str

Collapsed symbol before the title.

'\u25b6' expanded_symbol str

Expanded symbol before the title.

'\u25bc' name str | None

The name of the collapsible.

None id str | None

The ID of the collapsible in the DOM.

None classes str | None

The CSS classes of the collapsible.

None disabled bool

Whether the collapsible is disabled or not.

False"},{"location":"widgets/content_switcher/","title":"ContentSwitcher","text":"

Added in version 0.14.0

A widget for containing and switching display between multiple child widgets.

  • Focusable
  • Container
"},{"location":"widgets/content_switcher/#example","title":"Example","text":"

The example below uses a ContentSwitcher in combination with two Buttons to create a simple tabbed view. Note how each Button has an ID set, and how each child of the ContentSwitcher has a corresponding ID; then a Button.Clicked handler is used to set ContentSwitcher.current to switch between the different views.

Outputcontent_switcher.pycontent_switcher.tcss

ContentSwitcherApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0DataTable\u00a0\u00a0Markdown\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u00a0Book\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Year\u00a0\u2502 \u2502\u00a0Dune\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01965\u00a0\u2502 \u2502\u00a0Dune\u00a0Messiah\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01969\u00a0\u2502 \u2502\u00a0Children\u00a0of\u00a0Dune\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01976\u00a0\u2502 \u2502\u00a0God\u00a0Emperor\u00a0of\u00a0Dune\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01981\u00a0\u2502 \u2502\u00a0Heretics\u00a0of\u00a0Dune\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01984\u00a0\u2502 \u2502\u00a0Chapterhouse:\u00a0Dune\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01985\u00a0\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

from rich.align import VerticalCenter\nfrom textual.app import App, ComposeResult\nfrom textual.containers import Horizontal, VerticalScroll\nfrom textual.widgets import Button, ContentSwitcher, DataTable, Markdown\nMARKDOWN_EXAMPLE = \"\"\"# Three Flavours Cornetto\nThe Three Flavours Cornetto trilogy is an anthology series of British\ncomedic genre films directed by Edgar Wright.\n## Shaun of the Dead\n| Flavour | UK Release Date | Director |\n| -- | -- | -- |\n| Strawberry | 2004-04-09 | Edgar Wright |\n## Hot Fuzz\n| Flavour | UK Release Date | Director |\n| -- | -- | -- |\n| Classico | 2007-02-17 | Edgar Wright |\n## The World's End\n| Flavour | UK Release Date | Director |\n| -- | -- | -- |\n| Mint | 2013-07-19 | Edgar Wright |\n\"\"\"\nclass ContentSwitcherApp(App[None]):\nCSS_PATH = \"content_switcher.tcss\"\ndef compose(self) -> ComposeResult:\nwith Horizontal(id=\"buttons\"):  # (1)!\nyield Button(\"DataTable\", id=\"data-table\")  # (2)!\nyield Button(\"Markdown\", id=\"markdown\")  # (3)!\nwith ContentSwitcher(initial=\"data-table\"):  # (4)!\nyield DataTable(id=\"data-table\")\nwith VerticalScroll(id=\"markdown\"):\nyield Markdown(MARKDOWN_EXAMPLE)\ndef on_button_pressed(self, event: Button.Pressed) -> None:\nself.query_one(ContentSwitcher).current = event.button.id  # (5)!\ndef on_mount(self) -> None:\ntable = self.query_one(DataTable)\ntable.add_columns(\"Book\", \"Year\")\ntable.add_rows(\n[\n(title.ljust(35), year)\nfor title, year in (\n(\"Dune\", 1965),\n(\"Dune Messiah\", 1969),\n(\"Children of Dune\", 1976),\n(\"God Emperor of Dune\", 1981),\n(\"Heretics of Dune\", 1984),\n(\"Chapterhouse: Dune\", 1985),\n)\n]\n)\nif __name__ == \"__main__\":\nContentSwitcherApp().run()\n
  1. A Horizontal to hold the buttons, each with a unique ID.
  2. This button will select the DataTable in the ContentSwitcher.
  3. This button will select the Markdown in the ContentSwitcher.
  4. Note that the initial visible content is set by its ID, see below.
  5. When a button is pressed, its ID is used to switch to a different widget in the ContentSwitcher. Remember that IDs are unique within parent, so the buttons and the widgets in the ContentSwitcher can share IDs.
Screen {\nalign: center middle;\npadding: 1;\n}\n#buttons {\nheight: 3;\nwidth: auto;\n}\nContentSwitcher {\nbackground: $panel;\nborder: round $primary;\nwidth: 90%;\nheight: 1fr;\n}\nDataTable {\nbackground: $panel;\n}\nMarkdownH2 {\nbackground: $primary;\ncolor: yellow;\nborder: none;\npadding: 0;\n}\n

When the user presses the \"Markdown\" button the view is switched:

ContentSwitcherApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0DataTable\u00a0\u00a0Markdown\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2502 \u2502\u258e\u258a\u2502 \u2502\u258eThree\u00a0Flavours\u00a0Cornetto\u258a\u2502 \u2502\u258e\u258a\u2502 \u2502\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2502 \u2502The\u00a0Three\u00a0Flavours\u00a0Cornetto\u00a0trilogy\u00a0is\u00a0an\u00a0anthology\u00a0series\u2502 \u2502of\u00a0British\u00a0comedic\u00a0genre\u00a0films\u00a0directed\u00a0by\u00a0Edgar\u00a0Wright.\u2502 \u2502\u2502 \u2502\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Shaun\u00a0of\u00a0the\u00a0Dead\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502 \u2502\u2502 \u2502\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2502 \u2502\u258e\u258a\u2502 \u2502\u258eFlavour\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0UK\u00a0Release\u00a0Date\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Director\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a\u2502 \u2502\u258e\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0\u258a\u2502 \u2502\u258eStrawberry\u00a0\u00a0\u00a0\u00a0\u00a02004-04-09\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Edgar\u00a0Wright\u00a0\u00a0\u00a0\u00a0\u258a\u2502 \u2502\u258e\u258a\u2502 \u2502\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2502 \u2502\u2502 \u2502\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Hot\u00a0Fuzz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502 \u2502\u2502 \u2502\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2502 \u2502\u258e\u258a\u2502 \u2502\u258eFlavour\u00a0\u00a0\u00a0\u00a0\u00a0UK\u00a0Release\u00a0Date\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Director\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a\u2502 \u2502\u258e\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0\u258a\u2502 \u2502\u258eClassico\u00a0\u00a0\u00a0\u00a02007-02-17\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Edgar\u00a0Wright\u00a0\u00a0\u00a0\u00a0\u00a0\u258a\u2502 \u2502\u258e\u258a\u2502 \u2502\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2502 \u2502\u2502 \u2502\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0The\u00a0World's\u00a0End\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502 \u2502\u2502 \u2502\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2502 \u2502\u258e\u258a\u2502 \u2502\u258eFlavour\u00a0\u00a0\u00a0\u00a0UK\u00a0Release\u00a0Date\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Director\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

"},{"location":"widgets/content_switcher/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description current str | None None The ID of the currently-visible child. None means nothing is visible."},{"location":"widgets/content_switcher/#messages","title":"Messages","text":"

This widget posts no messages.

"},{"location":"widgets/content_switcher/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/content_switcher/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/content_switcher/#textual.widgets.ContentSwitcher","title":"textual.widgets.ContentSwitcher class","text":"
def __init__(\nself,\n*children,\nname=None,\nid=None,\nclasses=None,\ndisabled=False,\ninitial=None\n):\n

Bases: Container

A widget for switching between different children.

Note

All child widgets that are to be switched between need a unique ID. Children that have no ID will be hidden and ignored.

Parameters Name Type Description Default *children Widget

The widgets to switch between.

() name str | None

The name of the content switcher.

None id str | None

The ID of the content switcher in the DOM.

None classes str | None

The CSS classes of the content switcher.

None disabled bool

Whether the content switcher is disabled or not.

False initial str | None

The ID of the initial widget to show, None or empty string for the first tab.

None Note

If initial is not supplied no children will be shown to start with.

"},{"location":"widgets/content_switcher/#textual.widgets._content_switcher.ContentSwitcher.current","title":"current class-attribute instance-attribute","text":"
current: reactive[str | None] = reactive[Optional[str]](\nNone, init=False\n)\n

The ID of the currently-displayed widget.

If set to None then no widget is visible.

Note

If set to an unknown ID, this will result in NoMatches being raised.

"},{"location":"widgets/content_switcher/#textual.widgets._content_switcher.ContentSwitcher.visible_content","title":"visible_content property","text":"
visible_content: Widget | None\n

A reference to the currently-visible widget.

None if nothing is visible.

"},{"location":"widgets/content_switcher/#textual.widgets._content_switcher.ContentSwitcher.watch_current","title":"watch_current method","text":"
def watch_current(self, old, new):\n

React to the current visible child choice being changed.

Parameters Name Type Description Default old str | None

The old widget ID (or None if there was no widget).

required new str | None

The new widget ID (or None if nothing should be shown).

required"},{"location":"widgets/data_table/","title":"DataTable","text":"

A table widget optimized for displaying a lot of data.

  • Focusable
  • Container
"},{"location":"widgets/data_table/#guide","title":"Guide","text":""},{"location":"widgets/data_table/#adding-data","title":"Adding data","text":"

The following example shows how to fill a table with data. First, we use add_columns to include the lane, swimmer, country, and time columns in the table. After that, we use the add_rows method to insert the rows into the table.

Outputdata_table.py

TableApp \u00a0lane\u00a0\u00a0swimmer\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0country\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0time\u00a0\u00a0 \u00a04\u00a0\u00a0\u00a0\u00a0\u00a0Joseph\u00a0Schooling\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Singapore\u00a0\u00a0\u00a0\u00a0\u00a0\u00a050.39\u00a0 \u00a02\u00a0\u00a0\u00a0\u00a0\u00a0Michael\u00a0Phelps\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.14\u00a0 \u00a05\u00a0\u00a0\u00a0\u00a0\u00a0Chad\u00a0le\u00a0Clos\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0South\u00a0Africa\u00a0\u00a0\u00a051.14\u00a0 \u00a06\u00a0\u00a0\u00a0\u00a0\u00a0L\u00e1szl\u00f3\u00a0Cseh\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Hungary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.14\u00a0 \u00a03\u00a0\u00a0\u00a0\u00a0\u00a0Li\u00a0Zhuhao\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0China\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.26\u00a0 \u00a08\u00a0\u00a0\u00a0\u00a0\u00a0Mehdy\u00a0Metella\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0France\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.58\u00a0 \u00a07\u00a0\u00a0\u00a0\u00a0\u00a0Tom\u00a0Shields\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.73\u00a0 \u00a01\u00a0\u00a0\u00a0\u00a0\u00a0Aleksandr\u00a0Sadovnikov\u00a0\u00a0Russia\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0 \u00a010\u00a0\u00a0\u00a0\u00a0Darren\u00a0Burns\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Scotland\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0

from textual.app import App, ComposeResult\nfrom textual.widgets import DataTable\nROWS = [\n(\"lane\", \"swimmer\", \"country\", \"time\"),\n(4, \"Joseph Schooling\", \"Singapore\", 50.39),\n(2, \"Michael Phelps\", \"United States\", 51.14),\n(5, \"Chad le Clos\", \"South Africa\", 51.14),\n(6, \"L\u00e1szl\u00f3 Cseh\", \"Hungary\", 51.14),\n(3, \"Li Zhuhao\", \"China\", 51.26),\n(8, \"Mehdy Metella\", \"France\", 51.58),\n(7, \"Tom Shields\", \"United States\", 51.73),\n(1, \"Aleksandr Sadovnikov\", \"Russia\", 51.84),\n(10, \"Darren Burns\", \"Scotland\", 51.84),\n]\nclass TableApp(App):\ndef compose(self) -> ComposeResult:\nyield DataTable()\ndef on_mount(self) -> None:\ntable = self.query_one(DataTable)\ntable.add_columns(*ROWS[0])\ntable.add_rows(ROWS[1:])\napp = TableApp()\nif __name__ == \"__main__\":\napp.run()\n

To add a single row or column use add_row and add_column, respectively.

"},{"location":"widgets/data_table/#styling-and-justifying-cells","title":"Styling and justifying cells","text":"

Cells can contain more than just plain strings - Rich renderables such as Text are also supported. Text objects provide an easy way to style and justify cell content:

Outputdata_table_renderables.py

TableApp \u00a0lane\u00a0\u00a0swimmer\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0country\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0time\u00a0\u00a0 \u00a0\u00a0\u00a04\u00a0\u00a0\u00a0\u00a0Joseph\u00a0Schooling\u00a0\u00a0\u00a0\u00a0Singapore50.39 \u00a0\u00a0\u00a02\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Michael\u00a0PhelpsUnited\u00a0States51.14 \u00a0\u00a0\u00a05\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Chad\u00a0le\u00a0Clos\u00a0South\u00a0Africa51.14 \u00a0\u00a0\u00a06\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0L\u00e1szl\u00f3\u00a0Cseh\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Hungary51.14 \u00a0\u00a0\u00a03\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Li\u00a0Zhuhao\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0China51.26 \u00a0\u00a0\u00a08\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Mehdy\u00a0Metella\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0France51.58 \u00a0\u00a0\u00a07\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Tom\u00a0ShieldsUnited\u00a0States51.73 \u00a0\u00a0\u00a01Aleksandr\u00a0Sadovnikov\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Russia51.84 \u00a0\u00a010\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Darren\u00a0Burns\u00a0\u00a0\u00a0\u00a0\u00a0Scotland51.84

from rich.text import Text\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import DataTable\nROWS = [\n(\"lane\", \"swimmer\", \"country\", \"time\"),\n(4, \"Joseph Schooling\", \"Singapore\", 50.39),\n(2, \"Michael Phelps\", \"United States\", 51.14),\n(5, \"Chad le Clos\", \"South Africa\", 51.14),\n(6, \"L\u00e1szl\u00f3 Cseh\", \"Hungary\", 51.14),\n(3, \"Li Zhuhao\", \"China\", 51.26),\n(8, \"Mehdy Metella\", \"France\", 51.58),\n(7, \"Tom Shields\", \"United States\", 51.73),\n(1, \"Aleksandr Sadovnikov\", \"Russia\", 51.84),\n(10, \"Darren Burns\", \"Scotland\", 51.84),\n]\nclass TableApp(App):\ndef compose(self) -> ComposeResult:\nyield DataTable()\ndef on_mount(self) -> None:\ntable = self.query_one(DataTable)\ntable.add_columns(*ROWS[0])\nfor row in ROWS[1:]:\n# Adding styled and justified `Text` objects instead of plain strings.\nstyled_row = [\nText(str(cell), style=\"italic #03AC13\", justify=\"right\") for cell in row\n]\ntable.add_row(*styled_row)\napp = TableApp()\nif __name__ == \"__main__\":\napp.run()\n
"},{"location":"widgets/data_table/#keys","title":"Keys","text":"

When adding a row to the table, you can supply a key to add_row. A key is a unique identifier for that row. If you don't supply a key, Textual will generate one for you and return it from add_row. This key can later be used to reference the row, regardless of its current position in the table.

When working with data from a database, for example, you may wish to set the row key to the primary key of the data to ensure uniqueness. The method add_column also accepts a key argument and works similarly.

Keys are important because cells in a data table can change location due to factors like row deletion and sorting. Thus, using keys instead of coordinates allows us to refer to data without worrying about its current location in the table.

If you want to change the table based solely on coordinates, you can use the coordinate_to_cell_key method to convert a coordinate to a cell key, which is a (row_key, column_key) pair.

"},{"location":"widgets/data_table/#cursors","title":"Cursors","text":"

The coordinate of the cursor is exposed via the cursor_coordinate reactive attribute. Three types of cursors are supported: cell, row, and column. Change the cursor type by assigning to the cursor_type reactive attribute.

Column CursorRow CursorCell Cursordata_table_cursors.py

TableApp \u00a0lane\u00a0\u00a0swimmer\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0country\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0time\u00a0\u00a0 \u00a04\u00a0\u00a0\u00a0\u00a0\u00a0Joseph\u00a0Schooling\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Singapore\u00a0\u00a0\u00a0\u00a0\u00a0\u00a050.39\u00a0 \u00a02\u00a0\u00a0\u00a0\u00a0\u00a0Michael\u00a0Phelps\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.14\u00a0 \u00a05\u00a0\u00a0\u00a0\u00a0\u00a0Chad\u00a0le\u00a0Clos\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0South\u00a0Africa\u00a0\u00a0\u00a051.14\u00a0 \u00a06\u00a0\u00a0\u00a0\u00a0\u00a0L\u00e1szl\u00f3\u00a0Cseh\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Hungary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.14\u00a0 \u00a03\u00a0\u00a0\u00a0\u00a0\u00a0Li\u00a0Zhuhao\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0China\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.26\u00a0 \u00a08\u00a0\u00a0\u00a0\u00a0\u00a0Mehdy\u00a0Metella\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0France\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.58\u00a0 \u00a07\u00a0\u00a0\u00a0\u00a0\u00a0Tom\u00a0Shields\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.73\u00a0 \u00a01\u00a0\u00a0\u00a0\u00a0\u00a0Aleksandr\u00a0Sadovnikov\u00a0\u00a0Russia\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0 \u00a010\u00a0\u00a0\u00a0\u00a0Darren\u00a0Burns\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Scotland\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0

TableApp \u00a0lane\u00a0\u00a0swimmer\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0country\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0time\u00a0\u00a0 \u00a04\u00a0\u00a0\u00a0\u00a0\u00a0Joseph\u00a0Schooling\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Singapore\u00a0\u00a0\u00a0\u00a0\u00a0\u00a050.39\u00a0 \u00a02\u00a0\u00a0\u00a0\u00a0\u00a0Michael\u00a0Phelps\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.14\u00a0 \u00a05\u00a0\u00a0\u00a0\u00a0\u00a0Chad\u00a0le\u00a0Clos\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0South\u00a0Africa\u00a0\u00a0\u00a051.14\u00a0 \u00a06\u00a0\u00a0\u00a0\u00a0\u00a0L\u00e1szl\u00f3\u00a0Cseh\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Hungary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.14\u00a0 \u00a03\u00a0\u00a0\u00a0\u00a0\u00a0Li\u00a0Zhuhao\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0China\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.26\u00a0 \u00a08\u00a0\u00a0\u00a0\u00a0\u00a0Mehdy\u00a0Metella\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0France\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.58\u00a0 \u00a07\u00a0\u00a0\u00a0\u00a0\u00a0Tom\u00a0Shields\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.73\u00a0 \u00a01\u00a0\u00a0\u00a0\u00a0\u00a0Aleksandr\u00a0Sadovnikov\u00a0\u00a0Russia\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0 \u00a010\u00a0\u00a0\u00a0\u00a0Darren\u00a0Burns\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Scotland\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0

TableApp \u00a0lane\u00a0\u00a0swimmer\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0country\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0time\u00a0\u00a0 \u00a04\u00a0\u00a0\u00a0\u00a0\u00a0Joseph\u00a0Schooling\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Singapore\u00a0\u00a0\u00a0\u00a0\u00a0\u00a050.39\u00a0 \u00a02\u00a0\u00a0\u00a0\u00a0\u00a0Michael\u00a0Phelps\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.14\u00a0 \u00a05\u00a0\u00a0\u00a0\u00a0\u00a0Chad\u00a0le\u00a0Clos\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0South\u00a0Africa\u00a0\u00a0\u00a051.14\u00a0 \u00a06\u00a0\u00a0\u00a0\u00a0\u00a0L\u00e1szl\u00f3\u00a0Cseh\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Hungary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.14\u00a0 \u00a03\u00a0\u00a0\u00a0\u00a0\u00a0Li\u00a0Zhuhao\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0China\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.26\u00a0 \u00a08\u00a0\u00a0\u00a0\u00a0\u00a0Mehdy\u00a0Metella\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0France\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.58\u00a0 \u00a07\u00a0\u00a0\u00a0\u00a0\u00a0Tom\u00a0Shields\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.73\u00a0 \u00a01\u00a0\u00a0\u00a0\u00a0\u00a0Aleksandr\u00a0Sadovnikov\u00a0\u00a0Russia\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0 \u00a010\u00a0\u00a0\u00a0\u00a0Darren\u00a0Burns\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Scotland\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0

from itertools import cycle\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import DataTable\nROWS = [\n(\"lane\", \"swimmer\", \"country\", \"time\"),\n(4, \"Joseph Schooling\", \"Singapore\", 50.39),\n(2, \"Michael Phelps\", \"United States\", 51.14),\n(5, \"Chad le Clos\", \"South Africa\", 51.14),\n(6, \"L\u00e1szl\u00f3 Cseh\", \"Hungary\", 51.14),\n(3, \"Li Zhuhao\", \"China\", 51.26),\n(8, \"Mehdy Metella\", \"France\", 51.58),\n(7, \"Tom Shields\", \"United States\", 51.73),\n(1, \"Aleksandr Sadovnikov\", \"Russia\", 51.84),\n(10, \"Darren Burns\", \"Scotland\", 51.84),\n]\ncursors = cycle([\"column\", \"row\", \"cell\"])\nclass TableApp(App):\ndef compose(self) -> ComposeResult:\nyield DataTable()\ndef on_mount(self) -> None:\ntable = self.query_one(DataTable)\ntable.cursor_type = next(cursors)\ntable.zebra_stripes = True\ntable.add_columns(*ROWS[0])\ntable.add_rows(ROWS[1:])\ndef key_c(self):\ntable = self.query_one(DataTable)\ntable.cursor_type = next(cursors)\napp = TableApp()\nif __name__ == \"__main__\":\napp.run()\n

You can change the position of the cursor using the arrow keys, Page Up, Page Down, Home and End, or by assigning to the cursor_coordinate reactive attribute.

"},{"location":"widgets/data_table/#updating-data","title":"Updating data","text":"

Cells can be updated in the DataTable by using the update_cell and update_cell_at methods.

"},{"location":"widgets/data_table/#removing-data","title":"Removing data","text":"

To remove all data in the table, use the clear method. To remove individual rows, use remove_row. The remove_row method accepts a key argument, which identifies the row to be removed.

If you wish to remove the row below the cursor in the DataTable, use coordinate_to_cell_key to get the row key of the row under the current cursor_coordinate, then supply this key to remove_row:

# Get the keys for the row and column under the cursor.\nrow_key, _ = table.coordinate_to_cell_key(table.cursor_coordinate)\n# Supply the row key to `remove_row` to delete the row.\ntable.remove_row(row_key)\n
"},{"location":"widgets/data_table/#removing-columns","title":"Removing columns","text":"

To remove individual columns, use remove_column. The remove_column method accepts a key argument, which identifies the column to be removed.

You can remove the column below the cursor using the same coordinate_to_cell_key method described above:

# Get the keys for the row and column under the cursor.\n_, column_key = table.coordinate_to_cell_key(table.cursor_coordinate)\n# Supply the column key to `column_row` to delete the column.\ntable.remove_column(column_key)\n
"},{"location":"widgets/data_table/#fixed-data","title":"Fixed data","text":"

You can fix a number of rows and columns in place, keeping them pinned to the top and left of the table respectively. To do this, assign an integer to the fixed_rows or fixed_columns reactive attributes of the DataTable.

Fixed datadata_table_fixed.py

TableApp \u00a0A\u00a0\u00a0\u00a0B\u00a0\u00a0\u00a0\u00a0C\u00a0\u00a0\u00a0 \u00a01\u00a0\u00a0\u00a02\u00a0\u00a0\u00a0\u00a03\u00a0\u00a0\u00a0 \u00a02\u00a0\u00a0\u00a04\u00a0\u00a0\u00a0\u00a06\u00a0\u00a0\u00a0 \u00a079\u00a0\u00a0158\u00a0\u00a0237\u00a0 \u00a080\u00a0\u00a0160\u00a0\u00a0240\u00a0 \u00a081\u00a0\u00a0162\u00a0\u00a0243\u00a0 \u00a082\u00a0\u00a0164\u00a0\u00a0246\u00a0 \u00a083\u00a0\u00a0166\u00a0\u00a0249\u00a0 \u00a084\u00a0\u00a0168\u00a0\u00a0252\u00a0 \u00a085\u00a0\u00a0170\u00a0\u00a0255\u00a0 \u00a086\u00a0\u00a0172\u00a0\u00a0258\u00a0 \u00a087\u00a0\u00a0174\u00a0\u00a0261\u00a0 \u00a088\u00a0\u00a0176\u00a0\u00a0264\u00a0 \u00a089\u00a0\u00a0178\u00a0\u00a0267\u00a0 \u00a090\u00a0\u00a0180\u00a0\u00a0270\u00a0 \u00a091\u00a0\u00a0182\u00a0\u00a0273\u00a0 \u00a092\u00a0\u00a0184\u00a0\u00a0276\u00a0 \u00a093\u00a0\u00a0186\u00a0\u00a0279\u00a0 \u00a094\u00a0\u00a0188\u00a0\u00a0282\u00a0\u2587\u2587 \u00a095\u00a0\u00a0190\u00a0\u00a0285\u00a0 \u00a096\u00a0\u00a0192\u00a0\u00a0288\u00a0 \u00a097\u00a0\u00a0194\u00a0\u00a0291\u00a0 \u00a098\u00a0\u00a0196\u00a0\u00a0294\u00a0 \u00a099\u00a0\u00a0198\u00a0\u00a0297\u00a0

from textual.app import App, ComposeResult\nfrom textual.widgets import DataTable\nclass TableApp(App):\nCSS = \"DataTable {height: 1fr}\"\ndef compose(self) -> ComposeResult:\nyield DataTable()\ndef on_mount(self) -> None:\ntable = self.query_one(DataTable)\ntable.focus()\ntable.add_columns(\"A\", \"B\", \"C\")\nfor number in range(1, 100):\ntable.add_row(str(number), str(number * 2), str(number * 3))\ntable.fixed_rows = 2\ntable.fixed_columns = 1\ntable.cursor_type = \"row\"\ntable.zebra_stripes = True\napp = TableApp()\nif __name__ == \"__main__\":\napp.run()\n

In the example above, we set fixed_rows to 2, and fixed_columns to 1, meaning the first two rows and the leftmost column do not scroll - they always remain visible as you scroll through the data table.

"},{"location":"widgets/data_table/#sorting","title":"Sorting","text":"

The DataTable can be sorted using the sort method. In order to sort your data by a column, you must have supplied a key to the add_column method when you added it. You can then pass this key to the sort method to sort by that column. Additionally, you can sort by multiple columns by passing multiple keys to sort.

"},{"location":"widgets/data_table/#labelled-rows","title":"Labelled rows","text":"

A \"label\" can be attached to a row using the add_row method. This will add an extra column to the left of the table which the cursor cannot interact with. This column is similar to the leftmost column in a spreadsheet containing the row numbers. The example below shows how to attach simple numbered labels to rows.

Labelled rowsdata_table_labels.py

TableApp \u00a0lane\u00a0\u00a0swimmer\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0country\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0time\u00a0\u00a0 1\u00a04\u00a0\u00a0\u00a0\u00a0\u00a0Joseph\u00a0Schooling\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Singapore\u00a0\u00a0\u00a0\u00a0\u00a0\u00a050.39\u00a0 2\u00a02\u00a0\u00a0\u00a0\u00a0\u00a0Michael\u00a0Phelps\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.14\u00a0 3\u00a05\u00a0\u00a0\u00a0\u00a0\u00a0Chad\u00a0le\u00a0Clos\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0South\u00a0Africa\u00a0\u00a0\u00a051.14\u00a0 4\u00a06\u00a0\u00a0\u00a0\u00a0\u00a0L\u00e1szl\u00f3\u00a0Cseh\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Hungary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.14\u00a0 5\u00a03\u00a0\u00a0\u00a0\u00a0\u00a0Li\u00a0Zhuhao\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0China\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.26\u00a0 6\u00a08\u00a0\u00a0\u00a0\u00a0\u00a0Mehdy\u00a0Metella\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0France\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.58\u00a0 7\u00a07\u00a0\u00a0\u00a0\u00a0\u00a0Tom\u00a0Shields\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.73\u00a0 8\u00a01\u00a0\u00a0\u00a0\u00a0\u00a0Aleksandr\u00a0Sadovnikov\u00a0\u00a0Russia\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0 9\u00a010\u00a0\u00a0\u00a0\u00a0Darren\u00a0Burns\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Scotland\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0

from rich.text import Text\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import DataTable\nROWS = [\n(\"lane\", \"swimmer\", \"country\", \"time\"),\n(4, \"Joseph Schooling\", \"Singapore\", 50.39),\n(2, \"Michael Phelps\", \"United States\", 51.14),\n(5, \"Chad le Clos\", \"South Africa\", 51.14),\n(6, \"L\u00e1szl\u00f3 Cseh\", \"Hungary\", 51.14),\n(3, \"Li Zhuhao\", \"China\", 51.26),\n(8, \"Mehdy Metella\", \"France\", 51.58),\n(7, \"Tom Shields\", \"United States\", 51.73),\n(1, \"Aleksandr Sadovnikov\", \"Russia\", 51.84),\n(10, \"Darren Burns\", \"Scotland\", 51.84),\n]\nclass TableApp(App):\ndef compose(self) -> ComposeResult:\nyield DataTable()\ndef on_mount(self) -> None:\ntable = self.query_one(DataTable)\ntable.add_columns(*ROWS[0])\nfor number, row in enumerate(ROWS[1:], start=1):\nlabel = Text(str(number), style=\"#B0FC38 italic\")\ntable.add_row(*row, label=label)\napp = TableApp()\nif __name__ == \"__main__\":\napp.run()\n
"},{"location":"widgets/data_table/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description show_header bool True Show the table header show_row_labels bool True Show the row labels (if applicable) fixed_rows int 0 Number of fixed rows (rows which do not scroll) fixed_columns int 0 Number of fixed columns (columns which do not scroll) zebra_stripes bool False Display alternating colors on rows header_height int 1 Height of header row show_cursor bool True Show the cursor cursor_type str \"cell\" One of \"cell\", \"row\", \"column\", or \"none\" cursor_coordinate Coordinate Coordinate(0, 0) The current coordinate of the cursor hover_coordinate Coordinate Coordinate(0, 0) The coordinate the mouse cursor is above"},{"location":"widgets/data_table/#messages","title":"Messages","text":"
  • DataTable.CellHighlighted
  • DataTable.CellSelected
  • DataTable.RowHighlighted
  • DataTable.RowSelected
  • DataTable.ColumnHighlighted
  • DataTable.ColumnSelected
  • DataTable.HeaderSelected
  • DataTable.RowLabelSelected
"},{"location":"widgets/data_table/#bindings","title":"Bindings","text":"

The data table widget defines the following bindings:

Key(s) Description enter Select cells under the cursor. up Move the cursor up. down Move the cursor down. right Move the cursor right. left Move the cursor left."},{"location":"widgets/data_table/#component-classes","title":"Component Classes","text":"

The data table widget provides the following component classes:

Class Description datatable--cursor Target the cursor. datatable--hover Target the cells under the hover cursor. datatable--fixed Target fixed columns and fixed rows. datatable--fixed-cursor Target highlighted and fixed columns or header. datatable--header Target the header of the data table. datatable--header-cursor Target cells highlighted by the cursor. datatable--header-hover Target hovered header or row label cells. datatable--even-row Target even rows (row indices start at 0). datatable--odd-row Target odd rows (row indices start at 0)."},{"location":"widgets/data_table/#textual.widgets.DataTable","title":"textual.widgets.DataTable class","text":"
def __init__(\nself,\n*,\nshow_header=True,\nshow_row_labels=True,\nfixed_rows=0,\nfixed_columns=0,\nzebra_stripes=False,\nheader_height=1,\nshow_cursor=True,\ncursor_foreground_priority=\"css\",\ncursor_background_priority=\"renderable\",\ncursor_type=\"cell\",\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: ScrollView, Generic[CellType]

A tabular widget that contains data.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.BINDINGS","title":"BINDINGS class-attribute","text":"
BINDINGS: list[BindingType] = [\nBinding(\"enter\", \"select_cursor\", \"Select\", show=False),\nBinding(\"up\", \"cursor_up\", \"Cursor Up\", show=False),\nBinding(\n\"down\", \"cursor_down\", \"Cursor Down\", show=False\n),\nBinding(\n\"right\", \"cursor_right\", \"Cursor Right\", show=False\n),\nBinding(\n\"left\", \"cursor_left\", \"Cursor Left\", show=False\n),\nBinding(\"pageup\", \"page_up\", \"Page Up\", show=False),\nBinding(\n\"pagedown\", \"page_down\", \"Page Down\", show=False\n),\n]\n
Key(s) Description enter Select cells under the cursor. up Move the cursor up. down Move the cursor down. right Move the cursor right. left Move the cursor left."},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
COMPONENT_CLASSES: set[str] = {\n\"datatable--cursor\",\n\"datatable--hover\",\n\"datatable--fixed\",\n\"datatable--fixed-cursor\",\n\"datatable--header\",\n\"datatable--header-cursor\",\n\"datatable--header-hover\",\n\"datatable--odd-row\",\n\"datatable--even-row\",\n}\n
Class Description datatable--cursor Target the cursor. datatable--hover Target the cells under the hover cursor. datatable--fixed Target fixed columns and fixed rows. datatable--fixed-cursor Target highlighted and fixed columns or header. datatable--header Target the header of the data table. datatable--header-cursor Target cells highlighted by the cursor. datatable--header-hover Target hovered header or row label cells. datatable--even-row Target even rows (row indices start at 0). datatable--odd-row Target odd rows (row indices start at 0)."},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.columns","title":"columns instance-attribute","text":"
columns: dict[ColumnKey, Column] = {}\n

Metadata about the columns of the table, indexed by their key.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.cursor_background_priority","title":"cursor_background_priority instance-attribute","text":"
cursor_background_priority = cursor_background_priority\n

Should we prioritize the cursor component class CSS background or the renderable background in the event where a cell contains a renderable with a background color.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.cursor_column","title":"cursor_column property","text":"
cursor_column: int\n

The index of the column that the DataTable cursor is currently on.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.cursor_coordinate","title":"cursor_coordinate class-attribute instance-attribute","text":"
cursor_coordinate: Reactive[Coordinate] = Reactive(\nCoordinate(0, 0), repaint=False, always_update=True\n)\n

Current cursor Coordinate.

This can be set programmatically or changed via the method move_cursor.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.cursor_foreground_priority","title":"cursor_foreground_priority instance-attribute","text":"
cursor_foreground_priority = cursor_foreground_priority\n

Should we prioritize the cursor component class CSS foreground or the renderable foreground in the event where a cell contains a renderable with a foreground color.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.cursor_row","title":"cursor_row property","text":"
cursor_row: int\n

The index of the row that the DataTable cursor is currently on.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.cursor_type","title":"cursor_type class-attribute instance-attribute","text":"
cursor_type: Reactive[CursorType] = cursor_type\n

The type of cursor of the DataTable.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.fixed_columns","title":"fixed_columns class-attribute instance-attribute","text":"
fixed_columns = fixed_columns\n

The number of columns to fix (prevented from scrolling).

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.fixed_rows","title":"fixed_rows class-attribute instance-attribute","text":"
fixed_rows = fixed_rows\n

The number of rows to fix (prevented from scrolling).

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.header_height","title":"header_height class-attribute instance-attribute","text":"
header_height = header_height\n

The height of the header row (the row of column labels).

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.hover_column","title":"hover_column property","text":"
hover_column: int\n

The index of the column that the mouse cursor is currently hovering above.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.hover_coordinate","title":"hover_coordinate class-attribute instance-attribute","text":"
hover_coordinate: Reactive[Coordinate] = Reactive(\nCoordinate(0, 0), repaint=False, always_update=True\n)\n

The coordinate of the DataTable that is being hovered.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.hover_row","title":"hover_row property","text":"
hover_row: int\n

The index of the row that the mouse cursor is currently hovering above.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ordered_columns","title":"ordered_columns property","text":"
ordered_columns: list[Column]\n

The list of Columns in the DataTable, ordered as they appear on screen.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ordered_rows","title":"ordered_rows property","text":"
ordered_rows: list[Row]\n

The list of Rows in the DataTable, ordered as they appear on screen.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.row_count","title":"row_count property","text":"
row_count: int\n

The number of rows currently present in the DataTable.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.rows","title":"rows instance-attribute","text":"
rows: dict[RowKey, Row] = {}\n

Metadata about the rows of the table, indexed by their key.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.show_cursor","title":"show_cursor class-attribute instance-attribute","text":"
show_cursor = show_cursor\n

Show/hide both the keyboard and hover cursor.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.show_header","title":"show_header class-attribute instance-attribute","text":"
show_header = show_header\n

Show/hide the header row (the row of column labels).

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.show_row_labels","title":"show_row_labels class-attribute instance-attribute","text":"
show_row_labels = show_row_labels\n

Show/hide the column containing the labels of rows.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.zebra_stripes","title":"zebra_stripes class-attribute instance-attribute","text":"
zebra_stripes = zebra_stripes\n

Apply zebra effect on row backgrounds (light, dark, light, dark, ...).

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellHighlighted","title":"CellHighlighted class","text":"
def __init__(self, data_table, value, coordinate, cell_key):\n

Bases: Message

Posted when the cursor moves to highlight a new cell.

This is only relevant when the cursor_type is \"cell\". It's also posted when the cell cursor is re-enabled (by setting show_cursor=True), and when the cursor type is changed to \"cell\". Can be handled using on_data_table_cell_highlighted in a subclass of DataTable or in a parent widget in the DOM.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellHighlighted.cell_key","title":"cell_key instance-attribute","text":"
cell_key: CellKey = cell_key\n

The key for the highlighted cell.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellHighlighted.control","title":"control property","text":"
control: DataTable\n

Alias for the data table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellHighlighted.coordinate","title":"coordinate instance-attribute","text":"
coordinate: Coordinate = coordinate\n

The coordinate of the highlighted cell.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellHighlighted.data_table","title":"data_table instance-attribute","text":"
data_table = data_table\n

The data table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellHighlighted.value","title":"value instance-attribute","text":"
value: CellType = value\n

The value in the highlighted cell.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellSelected","title":"CellSelected class","text":"
def __init__(self, data_table, value, coordinate, cell_key):\n

Bases: Message

Posted by the DataTable widget when a cell is selected.

This is only relevant when the cursor_type is \"cell\". Can be handled using on_data_table_cell_selected in a subclass of DataTable or in a parent widget in the DOM.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellSelected.cell_key","title":"cell_key instance-attribute","text":"
cell_key: CellKey = cell_key\n

The key for the selected cell.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellSelected.control","title":"control property","text":"
control: DataTable\n

Alias for the data table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellSelected.coordinate","title":"coordinate instance-attribute","text":"
coordinate: Coordinate = coordinate\n

The coordinate of the cell that was selected.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellSelected.data_table","title":"data_table instance-attribute","text":"
data_table = data_table\n

The data table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellSelected.value","title":"value instance-attribute","text":"
value: CellType = value\n

The value in the cell that was selected.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnHighlighted","title":"ColumnHighlighted class","text":"
def __init__(self, data_table, cursor_column, column_key):\n

Bases: Message

Posted when a column is highlighted.

This message is only posted when the cursor_type is set to \"column\". Can be handled using on_data_table_column_highlighted in a subclass of DataTable or in a parent widget in the DOM.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnHighlighted.column_key","title":"column_key instance-attribute","text":"
column_key = column_key\n

The key of the column that was highlighted.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnHighlighted.control","title":"control property","text":"
control: DataTable\n

Alias for the data table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnHighlighted.cursor_column","title":"cursor_column instance-attribute","text":"
cursor_column: int = cursor_column\n

The x-coordinate of the column that was highlighted.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnHighlighted.data_table","title":"data_table instance-attribute","text":"
data_table = data_table\n

The data table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnSelected","title":"ColumnSelected class","text":"
def __init__(self, data_table, cursor_column, column_key):\n

Bases: Message

Posted when a column is selected.

This message is only posted when the cursor_type is set to \"column\". Can be handled using on_data_table_column_selected in a subclass of DataTable or in a parent widget in the DOM.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnSelected.column_key","title":"column_key instance-attribute","text":"
column_key = column_key\n

The key of the column that was selected.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnSelected.control","title":"control property","text":"
control: DataTable\n

Alias for the data table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnSelected.cursor_column","title":"cursor_column instance-attribute","text":"
cursor_column: int = cursor_column\n

The x-coordinate of the column that was selected.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnSelected.data_table","title":"data_table instance-attribute","text":"
data_table = data_table\n

The data table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.HeaderSelected","title":"HeaderSelected class","text":"
def __init__(self, data_table, column_key, column_index, label):\n

Bases: Message

Posted when a column header/label is clicked.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.HeaderSelected.column_index","title":"column_index instance-attribute","text":"
column_index = column_index\n

The index for the column.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.HeaderSelected.column_key","title":"column_key instance-attribute","text":"
column_key = column_key\n

The key for the column.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.HeaderSelected.control","title":"control property","text":"
control: DataTable\n

Alias for the data table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.HeaderSelected.data_table","title":"data_table instance-attribute","text":"
data_table = data_table\n

The data table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.HeaderSelected.label","title":"label instance-attribute","text":"
label = label\n

The text of the label.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowHighlighted","title":"RowHighlighted class","text":"
def __init__(self, data_table, cursor_row, row_key):\n

Bases: Message

Posted when a row is highlighted.

This message is only posted when the cursor_type is set to \"row\". Can be handled using on_data_table_row_highlighted in a subclass of DataTable or in a parent widget in the DOM.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowHighlighted.control","title":"control property","text":"
control: DataTable\n

Alias for the data table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowHighlighted.cursor_row","title":"cursor_row instance-attribute","text":"
cursor_row: int = cursor_row\n

The y-coordinate of the cursor that highlighted the row.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowHighlighted.data_table","title":"data_table instance-attribute","text":"
data_table = data_table\n

The data table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowHighlighted.row_key","title":"row_key instance-attribute","text":"
row_key: RowKey = row_key\n

The key of the row that was highlighted.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowLabelSelected","title":"RowLabelSelected class","text":"
def __init__(self, data_table, row_key, row_index, label):\n

Bases: Message

Posted when a row label is clicked.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowLabelSelected.control","title":"control property","text":"
control: DataTable\n

Alias for the data table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowLabelSelected.data_table","title":"data_table instance-attribute","text":"
data_table = data_table\n

The data table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowLabelSelected.label","title":"label instance-attribute","text":"
label = label\n

The text of the label.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowLabelSelected.row_index","title":"row_index instance-attribute","text":"
row_index = row_index\n

The index for the column.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowLabelSelected.row_key","title":"row_key instance-attribute","text":"
row_key = row_key\n

The key for the column.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowSelected","title":"RowSelected class","text":"
def __init__(self, data_table, cursor_row, row_key):\n

Bases: Message

Posted when a row is selected.

This message is only posted when the cursor_type is set to \"row\". Can be handled using on_data_table_row_selected in a subclass of DataTable or in a parent widget in the DOM.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowSelected.control","title":"control property","text":"
control: DataTable\n

Alias for the data table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowSelected.cursor_row","title":"cursor_row instance-attribute","text":"
cursor_row: int = cursor_row\n

The y-coordinate of the cursor that made the selection.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowSelected.data_table","title":"data_table instance-attribute","text":"
data_table = data_table\n

The data table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowSelected.row_key","title":"row_key instance-attribute","text":"
row_key: RowKey = row_key\n

The key of the row that was selected.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.action_page_down","title":"action_page_down method","text":"
def action_page_down(self):\n

Move the cursor one page down.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.action_page_up","title":"action_page_up method","text":"
def action_page_up(self):\n

Move the cursor one page up.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.action_scroll_end","title":"action_scroll_end method","text":"
def action_scroll_end(self):\n

Scroll to the bottom of the data table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.action_scroll_home","title":"action_scroll_home method","text":"
def action_scroll_home(self):\n

Scroll to the top of the data table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.add_column","title":"add_column method","text":"
def add_column(\nself, label, *, width=None, key=None, default=None\n):\n

Add a column to the table.

Parameters Name Type Description Default label TextType

A str or Text object containing the label (shown top of column).

required width int | None

Width of the column in cells or None to fit content.

None key str | None

A key which uniquely identifies this column. If None, it will be generated for you.

None default CellType | None

The value to insert into pre-existing rows.

None Returns Type Description ColumnKey

Uniquely identifies this column. Can be used to retrieve this column regardless of its current location in the DataTable (it could have moved after being added due to sorting/insertion/deletion of other columns).

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.add_columns","title":"add_columns method","text":"
def add_columns(self, *labels):\n

Add a number of columns.

Parameters Name Type Description Default *labels TextType

Column headers.

() Returns Type Description list[ColumnKey]

A list of the keys for the columns that were added. See the add_column method docstring for more information on how these keys are used.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.add_row","title":"add_row method","text":"
def add_row(self, *cells, height=1, key=None, label=None):\n

Add a row at the bottom of the DataTable.

Parameters Name Type Description Default *cells CellType

Positional arguments should contain cell data.

() height int | None

The height of a row (in lines). Use None to auto-detect the optimal height.

1 key str | None

A key which uniquely identifies this row. If None, it will be generated for you and returned.

None label TextType | None

The label for the row. Will be displayed to the left if supplied.

None Returns Type Description RowKey

Unique identifier for this row. Can be used to retrieve this row regardless of its current location in the DataTable (it could have moved after being added due to sorting or insertion/deletion of other rows).

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.add_rows","title":"add_rows method","text":"
def add_rows(self, rows):\n

Add a number of rows at the bottom of the DataTable.

Parameters Name Type Description Default rows Iterable[Iterable[CellType]]

Iterable of rows. A row is an iterable of cells.

required Returns Type Description list[RowKey]

A list of the keys for the rows that were added. See the add_row method docstring for more information on how these keys are used.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.clear","title":"clear method","text":"
def clear(self, columns=False):\n

Clear the table.

Parameters Name Type Description Default columns bool

Also clear the columns.

False Returns Type Description Self

The DataTable instance.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.coordinate_to_cell_key","title":"coordinate_to_cell_key method","text":"
def coordinate_to_cell_key(self, coordinate):\n

Return the key for the cell currently occupying this coordinate.

Parameters Name Type Description Default coordinate Coordinate

The coordinate to exam the current cell key of.

required Returns Type Description CellKey

The key of the cell currently occupying this coordinate.

Raises Type Description CellDoesNotExist

If the coordinate is not valid.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_cell","title":"get_cell method","text":"
def get_cell(self, row_key, column_key):\n

Given a row key and column key, return the value of the corresponding cell.

Parameters Name Type Description Default row_key RowKey | str

The row key of the cell.

required column_key ColumnKey | str

The column key of the cell.

required Returns Type Description CellType

The value of the cell identified by the row and column keys.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_cell_at","title":"get_cell_at method","text":"
def get_cell_at(self, coordinate):\n

Get the value from the cell occupying the given coordinate.

Parameters Name Type Description Default coordinate Coordinate

The coordinate to retrieve the value from.

required Returns Type Description CellType

The value of the cell at the coordinate.

Raises Type Description CellDoesNotExist

If there is no cell with the given coordinate.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_cell_coordinate","title":"get_cell_coordinate method","text":"
def get_cell_coordinate(self, row_key, column_key):\n

Given a row key and column key, return the corresponding cell coordinate.

Parameters Name Type Description Default row_key RowKey | str

The row key of the cell.

required column_key Column | str

The column key of the cell.

required Returns Type Description Coordinate

The current coordinate of the cell identified by the row and column keys.

Raises Type Description CellDoesNotExist

If the specified cell does not exist.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_column","title":"get_column method","text":"
def get_column(self, column_key):\n

Get the values from the column identified by the given column key.

Parameters Name Type Description Default column_key ColumnKey | str

The key of the column.

required Returns Type Description Iterable[CellType]

A generator which yields the cells in the column.

Raises Type Description ColumnDoesNotExist

If there is no column corresponding to the key.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_column_at","title":"get_column_at method","text":"
def get_column_at(self, column_index):\n

Get the values from the column at a given index.

Parameters Name Type Description Default column_index int

The index of the column.

required Returns Type Description Iterable[CellType]

A generator which yields the cells in the column.

Raises Type Description ColumnDoesNotExist

If there is no column with the given index.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_column_index","title":"get_column_index method","text":"
def get_column_index(self, column_key):\n

Return the current index for the column identified by column_key.

Parameters Name Type Description Default column_key ColumnKey | str

The column key to find the current index of.

required Returns Type Description int

The current index of the specified column key.

Raises Type Description ColumnDoesNotExist

If the column key does not exist.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_row","title":"get_row method","text":"
def get_row(self, row_key):\n

Get the values from the row identified by the given row key.

Parameters Name Type Description Default row_key RowKey | str

The key of the row.

required Returns Type Description list[CellType]

A list of the values contained within the row.

Raises Type Description RowDoesNotExist

When there is no row corresponding to the key.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_row_at","title":"get_row_at method","text":"
def get_row_at(self, row_index):\n

Get the values from the cells in a row at a given index. This will return the values from a row based on the rows current position in the table.

Parameters Name Type Description Default row_index int

The index of the row.

required Returns Type Description list[CellType]

A list of the values contained in the row.

Raises Type Description RowDoesNotExist

If there is no row with the given index.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_row_height","title":"get_row_height method","text":"
def get_row_height(self, row_key):\n

Given a row key, return the height of that row in terminal cells.

Parameters Name Type Description Default row_key RowKey

The key of the row.

required Returns Type Description int

The height of the row, measured in terminal character cells.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_row_index","title":"get_row_index method","text":"
def get_row_index(self, row_key):\n

Return the current index for the row identified by row_key.

Parameters Name Type Description Default row_key RowKey | str

The row key to find the current index of.

required Returns Type Description int

The current index of the specified row key.

Raises Type Description RowDoesNotExist

If the row key does not exist.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.is_valid_column_index","title":"is_valid_column_index method","text":"
def is_valid_column_index(self, column_index):\n

Return a boolean indicating whether the column_index is within table bounds.

Parameters Name Type Description Default column_index int

The column index to check.

required Returns Type Description bool

True if the column index is within the bounds of the table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.is_valid_coordinate","title":"is_valid_coordinate method","text":"
def is_valid_coordinate(self, coordinate):\n

Return a boolean indicating whether the given coordinate is valid.

Parameters Name Type Description Default coordinate Coordinate

The coordinate to validate.

required Returns Type Description bool

True if the coordinate is within the bounds of the table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.is_valid_row_index","title":"is_valid_row_index method","text":"
def is_valid_row_index(self, row_index):\n

Return a boolean indicating whether the row_index is within table bounds.

Parameters Name Type Description Default row_index int

The row index to check.

required Returns Type Description bool

True if the row index is within the bounds of the table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.move_cursor","title":"move_cursor method","text":"
def move_cursor(self, *, row=None, column=None, animate=False):\n

Move the cursor to the given position.

Example
datatable = app.query_one(DataTable)\ndatatable.move_cursor(row=4, column=6)\n# datatable.cursor_coordinate == Coordinate(4, 6)\ndatatable.move_cursor(row=3)\n# datatable.cursor_coordinate == Coordinate(3, 6)\n
Parameters Name Type Description Default row int | None

The new row to move the cursor to.

None column int | None

The new column to move the cursor to.

None animate bool

Whether to animate the change of coordinates.

False"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.refresh_column","title":"refresh_column method","text":"
def refresh_column(self, column_index):\n

Refresh the column at the given index.

Parameters Name Type Description Default column_index int

The index of the column to refresh.

required Returns Type Description Self

The DataTable instance.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.refresh_coordinate","title":"refresh_coordinate method","text":"
def refresh_coordinate(self, coordinate):\n

Refresh the cell at a coordinate.

Parameters Name Type Description Default coordinate Coordinate

The coordinate to refresh.

required Returns Type Description Self

The DataTable instance.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.refresh_row","title":"refresh_row method","text":"
def refresh_row(self, row_index):\n

Refresh the row at the given index.

Parameters Name Type Description Default row_index int

The index of the row to refresh.

required Returns Type Description Self

The DataTable instance.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.remove_column","title":"remove_column method","text":"
def remove_column(self, column_key):\n

Remove a column (identified by a key) from the DataTable.

Parameters Name Type Description Default column_key ColumnKey | str

The key identifying the column to remove.

required Raises Type Description ColumnDoesNotExist

If the column key does not exist.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.remove_row","title":"remove_row method","text":"
def remove_row(self, row_key):\n

Remove a row (identified by a key) from the DataTable.

Parameters Name Type Description Default row_key RowKey | str

The key identifying the row to remove.

required Raises Type Description RowDoesNotExist

If the row key does not exist.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.sort","title":"sort method","text":"
def sort(self, *columns, reverse=False):\n

Sort the rows in the DataTable by one or more column keys.

Parameters Name Type Description Default columns ColumnKey | str

One or more columns to sort by the values in.

() reverse bool

If True, the sort order will be reversed.

False Returns Type Description Self

The DataTable instance.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.update_cell","title":"update_cell method","text":"
def update_cell(\nself, row_key, column_key, value, *, update_width=False\n):\n

Update the cell identified by the specified row key and column key.

Parameters Name Type Description Default row_key RowKey | str

The key identifying the row.

required column_key ColumnKey | str

The key identifying the column.

required value CellType

The new value to put inside the cell.

required update_width bool

Whether to resize the column width to accommodate for the new cell content.

False Raises Type Description CellDoesNotExist

When the supplied row_key and column_key cannot be found in the table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.update_cell_at","title":"update_cell_at method","text":"
def update_cell_at(\nself, coordinate, value, *, update_width=False\n):\n

Update the content inside the cell currently occupying the given coordinate.

Parameters Name Type Description Default coordinate Coordinate

The coordinate to update the cell at.

required value CellType

The new value to place inside the cell.

required update_width bool

Whether to resize the column width to accommodate for the new cell content.

False"},{"location":"widgets/data_table/#textual.widgets.data_table","title":"textual.widgets.data_table","text":""},{"location":"widgets/data_table/#textual.widgets.data_table.CursorType","title":"CursorType module-attribute","text":"
CursorType = Literal['cell', 'row', 'column', 'none']\n

The valid types of cursors for DataTable.cursor_type.

"},{"location":"widgets/data_table/#textual.widgets.data_table.CellDoesNotExist","title":"CellDoesNotExist class","text":"

Bases: Exception

The cell key/index was invalid.

Raised when the coordinates or cell key provided does not exist in the DataTable (e.g. out of bounds index, invalid key)

"},{"location":"widgets/data_table/#textual.widgets.data_table.CellKey","title":"CellKey class","text":"

Bases: NamedTuple

A unique identifier for a cell in the DataTable.

Even if the cell changes visual location (i.e. moves to a different coordinate in the table), this key can still be used to retrieve it, regardless of where it currently is.

"},{"location":"widgets/data_table/#textual.widgets.data_table.Column","title":"Column class","text":"

Metadata for a column in the DataTable.

"},{"location":"widgets/data_table/#textual.widgets._data_table.Column.render_width","title":"render_width property","text":"
render_width: int\n

Width in cells, required to render a column.

"},{"location":"widgets/data_table/#textual.widgets.data_table.ColumnDoesNotExist","title":"ColumnDoesNotExist class","text":"

Bases: Exception

Raised when the column index or column key provided does not exist in the DataTable (e.g. out of bounds index, invalid key)

"},{"location":"widgets/data_table/#textual.widgets.data_table.ColumnKey","title":"ColumnKey class","text":"

Bases: StringKey

Uniquely identifies a column in the DataTable.

Even if the visual location of the column changes due to sorting or other modifications, a key will always refer to the same column.

"},{"location":"widgets/data_table/#textual.widgets.data_table.DuplicateKey","title":"DuplicateKey class","text":"

Bases: Exception

The key supplied already exists.

Raised when the RowKey or ColumnKey provided already refers to an existing row or column in the DataTable. Keys must be unique.

"},{"location":"widgets/data_table/#textual.widgets.data_table.Row","title":"Row class","text":"

Metadata for a row in the DataTable.

"},{"location":"widgets/data_table/#textual.widgets.data_table.RowDoesNotExist","title":"RowDoesNotExist class","text":"

Bases: Exception

Raised when the row index or row key provided does not exist in the DataTable (e.g. out of bounds index, invalid key)

"},{"location":"widgets/data_table/#textual.widgets.data_table.RowKey","title":"RowKey class","text":"

Bases: StringKey

Uniquely identifies a row in the DataTable.

Even if the visual location of the row changes due to sorting or other modifications, a key will always refer to the same row.

"},{"location":"widgets/digits/","title":"Digits","text":"

Added in version 0.33.0

A widget to display numerical values in tall multi-line characters.

The digits 0-9 are supported, in addition to the following characters +, -, ^, :, and \u00d7. Other characters will be displayed in a regular size font.

You can set the text to be displayed in the constructor, or call update() to change the text after the widget has been mounted.

This widget will respect the text-align rule.

  • Focusable
  • Container
"},{"location":"widgets/digits/#example","title":"Example","text":"

The following example displays a few digits of Pi:

Outputdigits.py

DigitApp \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557 \u2551\u257a\u2501\u2513\u00a0\u00a0\u2513\u00a0\u257b\u00a0\u257b\u00a0\u2513\u00a0\u00a0\u250f\u2501\u2578\u250f\u2501\u2513\u257a\u2501\u2513\u00a0\u250f\u2501\u2578\u250f\u2501\u2578\u257a\u2501\u2513\u00a0\u250f\u2501\u2578\u250f\u2501\u2513\u250f\u2501\u2513\u257a\u2501\u2513\u2551 \u2551\u00a0\u2501\u252b\u00a0\u00a0\u2503\u00a0\u2517\u2501\u252b\u00a0\u2503\u00a0\u00a0\u2517\u2501\u2513\u2517\u2501\u252b\u250f\u2501\u251b\u00a0\u2523\u2501\u2513\u2517\u2501\u2513\u00a0\u2501\u252b\u00a0\u2517\u2501\u2513\u2523\u2501\u252b\u2517\u2501\u252b\u00a0\u00a0\u2503\u2551 \u2551\u257a\u2501\u251b.\u257a\u253b\u2578\u00a0\u00a0\u2579\u257a\u253b\u2578,\u257a\u2501\u251b\u257a\u2501\u251b\u2517\u2501\u2578,\u2517\u2501\u251b\u257a\u2501\u251b\u257a\u2501\u251b,\u257a\u2501\u251b\u2517\u2501\u251b\u257a\u2501\u251b\u00a0\u00a0\u2579\u2551 \u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d

from textual.app import App, ComposeResult\nfrom textual.widgets import Digits\nclass DigitApp(App):\nCSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    #pi {\n        border: double green;\n        width: auto;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Digits(\"3.141,592,653,5897\", id=\"pi\")\nif __name__ == \"__main__\":\napp = DigitApp()\napp.run()\n

Here's another example which uses Digits to display the current time:

Outputclock.py

ClockApp \u00a0\u2513\u00a0\u257a\u2501\u2513\u00a0\u00a0\u00a0\u257a\u2501\u2513\u250f\u2501\u2513\u00a0\u00a0\u00a0\u250f\u2501\u2578\u257a\u2501\u2513 \u00a0\u2503\u00a0\u00a0\u2501\u252b\u00a0:\u00a0\u250f\u2501\u251b\u2523\u2501\u252b\u00a0:\u00a0\u2517\u2501\u2513\u250f\u2501\u251b \u257a\u253b\u2578\u257a\u2501\u251b\u00a0\u00a0\u00a0\u2517\u2501\u2578\u2517\u2501\u251b\u00a0\u00a0\u00a0\u257a\u2501\u251b\u2517\u2501\u2578

from datetime import datetime\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Digits\nclass ClockApp(App):\nCSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    #clock {\n        width: auto;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Digits(\"\", id=\"clock\")\ndef on_ready(self) -> None:\nself.update_clock()\nself.set_interval(1, self.update_clock)\ndef update_clock(self) -> None:\nclock = datetime.now().time()\nself.query_one(Digits).update(f\"{clock:%T}\")\nif __name__ == \"__main__\":\napp = ClockApp()\napp.run()\n
"},{"location":"widgets/digits/#reactive-attributes","title":"Reactive Attributes","text":"

This widget has no reactive attributes.

"},{"location":"widgets/digits/#messages","title":"Messages","text":"

This widget posts no messages.

"},{"location":"widgets/digits/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/digits/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/digits/#textual.widgets.Digits","title":"textual.widgets.Digits class","text":"
def __init__(\nself,\nvalue=\"\",\n*,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: Widget

A widget to display numerical values using a 3x3 grid of unicode characters.

name: The name of the widget.\nid: The ID of the widget in the DOM.\nclasses: The CSS classes of the widget.\ndisabled: Whether the widget is disabled or not.\n
"},{"location":"widgets/digits/#textual.widgets._digits.Digits.value","title":"value property","text":"
value: str\n

The current value displayed in the Digits.

"},{"location":"widgets/digits/#textual.widgets._digits.Digits.update","title":"update method","text":"
def update(self, value):\n

Update the Digits with a new value.

Parameters Name Type Description Default value str

New value to display.

required Raises Type Description ValueError

If the value isn't a str.

"},{"location":"widgets/directory_tree/","title":"DirectoryTree","text":"

A tree control to navigate the contents of your filesystem.

  • Focusable
  • Container
"},{"location":"widgets/directory_tree/#example","title":"Example","text":"

The example below creates a simple tree to navigate the current working directory.

from textual.app import App, ComposeResult\nfrom textual.widgets import DirectoryTree\nclass DirectoryTreeApp(App):\ndef compose(self) -> ComposeResult:\nyield DirectoryTree(\"./\")\nif __name__ == \"__main__\":\napp = DirectoryTreeApp()\napp.run()\n
"},{"location":"widgets/directory_tree/#filtering","title":"Filtering","text":"

There may be times where you want to filter what appears in the DirectoryTree. To do this inherit from DirectoryTree and implement your own version of the filter_paths method. It should take an iterable of Python Path objects, and return those that pass the filter. For example, if you wanted to take the above code an filter out all of the \"hidden\" files and directories:

Outputdirectory_tree_filtered.py

DirectoryTreeApp \ud83d\udcc2\u00a0. \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0__pycache__ \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0dist \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0docs \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0examples \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0imgs \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0notes \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0questions \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0reference \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0sandbox \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0site \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0src \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0tests \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0tools \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0tree-sitter \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0CHANGELOG.md \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0CODE_OF_CONDUCT.md\u2584\u2584 \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0CONTRIBUTING.md \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0docs.md \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0faq.yml \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0keys.log \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0LICENSE \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0Makefile \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0mkdocs-common.yml

from pathlib import Path\nfrom typing import Iterable\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import DirectoryTree\nclass FilteredDirectoryTree(DirectoryTree):\ndef filter_paths(self, paths: Iterable[Path]) -> Iterable[Path]:\nreturn [path for path in paths if not path.name.startswith(\".\")]\nclass DirectoryTreeApp(App):\ndef compose(self) -> ComposeResult:\nyield FilteredDirectoryTree(\"./\")\nif __name__ == \"__main__\":\napp = DirectoryTreeApp()\napp.run()\n
"},{"location":"widgets/directory_tree/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description show_root bool True Show the root node. show_guides bool True Show guide lines between levels. guide_depth int 4 Amount of indentation between parent and child."},{"location":"widgets/directory_tree/#messages","title":"Messages","text":"
  • DirectoryTree.FileSelected
"},{"location":"widgets/directory_tree/#bindings","title":"Bindings","text":"

The directory tree widget inherits the bindings from the tree widget.

"},{"location":"widgets/directory_tree/#component-classes","title":"Component Classes","text":"

The directory tree widget provides the following component classes:

Class Description directory-tree--extension Target the extension of a file name. directory-tree--file Target files in the directory structure. directory-tree--folder Target folders in the directory structure. directory-tree--hidden Target hidden items in the directory structure.

See also the component classes for Tree.

"},{"location":"widgets/directory_tree/#see-also","title":"See Also","text":"
  • Tree code reference
"},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree","title":"textual.widgets.DirectoryTree class","text":"
def __init__(\nself,\npath,\n*,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: Tree[DirEntry]

A Tree widget that presents files and directories.

Parameters Name Type Description Default path str | Path

Path to directory.

required name str | None

The name of the widget, or None for no name.

None id str | None

The ID of the widget in the DOM, or None for no ID.

None classes str | None

A space-separated list of classes, or None for no classes.

None disabled bool

Whether the directory tree is disabled or not.

False"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
COMPONENT_CLASSES: set[str] = {\n\"directory-tree--extension\",\n\"directory-tree--file\",\n\"directory-tree--folder\",\n\"directory-tree--hidden\",\n}\n
Class Description directory-tree--extension Target the extension of a file name. directory-tree--file Target files in the directory structure. directory-tree--folder Target folders in the directory structure. directory-tree--hidden Target hidden items in the directory structure.

See also the component classes for Tree.

"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.PATH","title":"PATH class-attribute instance-attribute","text":"
PATH: Callable[[str | Path], Path] = Path\n

Callable that returns a fresh path object.

"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.path","title":"path class-attribute instance-attribute","text":"
path: var[str | Path] = path\n

The path that is the root of the directory tree.

Note

This can be set to either a str or a pathlib.Path object, but the value will always be a pathlib.Path object.

"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.DirectorySelected","title":"DirectorySelected class","text":"
def __init__(self, node, path):\n

Bases: Message

Posted when a directory is selected.

Can be handled using on_directory_tree_directory_selected in a subclass of DirectoryTree or in a parent widget in the DOM.

Parameters Name Type Description Default node TreeNode[DirEntry]

The tree node for the directory that was selected.

required path Path

The path of the directory that was selected.

required"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.DirectorySelected.control","title":"control property","text":"
control: Tree[DirEntry]\n

The Tree that had a directory selected.

"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.DirectorySelected.node","title":"node instance-attribute","text":"
node: TreeNode[DirEntry] = node\n

The tree node of the directory that was selected.

"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.DirectorySelected.path","title":"path instance-attribute","text":"
path: Path = path\n

The path of the directory that was selected.

"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.FileSelected","title":"FileSelected class","text":"
def __init__(self, node, path):\n

Bases: Message

Posted when a file is selected.

Can be handled using on_directory_tree_file_selected in a subclass of DirectoryTree or in a parent widget in the DOM.

Parameters Name Type Description Default node TreeNode[DirEntry]

The tree node for the file that was selected.

required path Path

The path of the file that was selected.

required"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.FileSelected.control","title":"control property","text":"
control: Tree[DirEntry]\n

The Tree that had a file selected.

"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.FileSelected.node","title":"node instance-attribute","text":"
node: TreeNode[DirEntry] = node\n

The tree node of the file that was selected.

"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.FileSelected.path","title":"path instance-attribute","text":"
path: Path = path\n

The path of the file that was selected.

"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.clear_node","title":"clear_node method","text":"
def clear_node(self, node):\n

Clear all nodes under the given node.

Returns Type Description Self

The Tree instance.

"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.filter_paths","title":"filter_paths method","text":"
def filter_paths(self, paths):\n

Filter the paths before adding them to the tree.

Parameters Name Type Description Default paths Iterable[Path]

The paths to be filtered.

required Returns Type Description Iterable[Path]

The filtered paths.

By default this method returns all of the paths provided. To create a filtered DirectoryTree inherit from it and implement your own version of this method.

"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.process_label","title":"process_label method","text":"
def process_label(self, label):\n

Process a str or Text into a label. Maybe overridden in a subclass to modify how labels are rendered.

Parameters Name Type Description Default label TextType

Label.

required Returns Type Description Text

A Rich Text object.

"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.reload","title":"reload method","text":"
def reload(self):\n

Reload the DirectoryTree contents.

"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.reload_node","title":"reload_node method","text":"
def reload_node(self, node):\n

Reload the given node's contents.

Parameters Name Type Description Default node TreeNode[DirEntry]

The node to reload.

required"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.render_label","title":"render_label method","text":"
def render_label(self, node, base_style, style):\n

Render a label for the given node.

Parameters Name Type Description Default node TreeNode[DirEntry]

A tree node.

required base_style Style

The base style of the widget.

required style Style

The additional style for the label.

required Returns Type Description Text

A Rich Text object containing the label.

"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.reset_node","title":"reset_node method","text":"
def reset_node(self, node, label, data=None):\n

Clear the subtree and reset the given node.

Parameters Name Type Description Default label TextType

The label for the node.

required data DirEntry | None

Optional data for the node.

None Returns Type Description Self

The Tree instance.

"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.validate_path","title":"validate_path method","text":"
def validate_path(self, path):\n

Ensure that the path is of the Path type.

Parameters Name Type Description Default path str | Path

The path to validate.

required Returns Type Description Path

The validated Path value.

Note

The result will always be a Python Path object, regardless of the value given.

"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.watch_path","title":"watch_path method","text":"
def watch_path(self):\n

Watch for changes to the path of the directory tree.

If the path is changed the directory tree will be repopulated using the new value as the root.

"},{"location":"widgets/footer/","title":"Footer","text":"

A simple footer widget which is docked to the bottom of its parent container. Displays available keybindings for the currently focused widget.

  • Focusable
  • Container
"},{"location":"widgets/footer/#example","title":"Example","text":"

The example below shows an app with a single keybinding that contains only a Footer widget. Notice how the Footer automatically displays the keybinding.

Outputfooter.py

FooterApp \u00a0Q\u00a0\u00a0Quit\u00a0the\u00a0app\u00a0\u00a0?\u00a0\u00a0Show\u00a0help\u00a0screen\u00a0\u00a0DELETE\u00a0\u00a0Delete\u00a0the\u00a0thing\u00a0

from textual.app import App, ComposeResult\nfrom textual.binding import Binding\nfrom textual.widgets import Footer\nclass FooterApp(App):\nBINDINGS = [\nBinding(key=\"q\", action=\"quit\", description=\"Quit the app\"),\nBinding(\nkey=\"question_mark\",\naction=\"help\",\ndescription=\"Show help screen\",\nkey_display=\"?\",\n),\nBinding(key=\"delete\", action=\"delete\", description=\"Delete the thing\"),\nBinding(key=\"j\", action=\"down\", description=\"Scroll down\", show=False),\n]\ndef compose(self) -> ComposeResult:\nyield Footer()\nif __name__ == \"__main__\":\napp = FooterApp()\napp.run()\n
"},{"location":"widgets/footer/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description highlight_key str None Stores the currently highlighted key. This is typically the key the cursor is hovered over in the footer."},{"location":"widgets/footer/#messages","title":"Messages","text":"

This widget posts no messages.

"},{"location":"widgets/footer/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/footer/#component-classes","title":"Component Classes","text":"

The footer widget provides the following component classes:

Class Description footer--description Targets the descriptions of the key bindings. footer--highlight Targets the highlighted key binding. footer--highlight-key Targets the key portion of the highlighted key binding. footer--key Targets the key portions of the key bindings."},{"location":"widgets/footer/#additional-notes","title":"Additional Notes","text":"
  • You can prevent keybindings from appearing in the footer by setting the show argument of the Binding to False.
  • You can customize the text that appears for the key itself in the footer using the key_display argument of Binding.
"},{"location":"widgets/footer/#textual.widgets.Footer","title":"textual.widgets.Footer class","text":"
def __init__(self):\n

Bases: Widget

A simple footer widget which docks itself to the bottom of the parent container.

"},{"location":"widgets/footer/#textual.widgets._footer.Footer.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
COMPONENT_CLASSES: set[str] = {\n\"footer--description\",\n\"footer--key\",\n\"footer--highlight\",\n\"footer--highlight-key\",\n}\n
Class Description footer--description Targets the descriptions of the key bindings. footer--highlight Targets the highlighted key binding. footer--highlight-key Targets the key portion of the highlighted key binding. footer--key Targets the key portions of the key bindings."},{"location":"widgets/footer/#textual.widgets._footer.Footer.watch_highlight_key","title":"watch_highlight_key async","text":"
def watch_highlight_key(self):\n

If highlight key changes we need to regenerate the text.

"},{"location":"widgets/header/","title":"Header","text":"

A simple header widget which docks itself to the top of the parent container.

Note

The application title which is shown in the header is taken from the title and sub_title of the application.

  • Focusable
  • Container
"},{"location":"widgets/header/#example","title":"Example","text":"

The example below shows an app with a Header.

Outputheader.py

HeaderApp \u2b58HeaderApp

from textual.app import App, ComposeResult\nfrom textual.widgets import Header\nclass HeaderApp(App):\ndef compose(self) -> ComposeResult:\nyield Header()\nif __name__ == \"__main__\":\napp = HeaderApp()\napp.run()\n

This example shows how to set the text in the Header using App.title and App.sub_title:

Outputheader_app_title.py

HeaderApp \u2b58Header\u00a0Application\u00a0\u2014\u00a0With\u00a0title\u00a0and\u00a0sub-title

from textual.app import App, ComposeResult\nfrom textual.widgets import Header\nclass HeaderApp(App):\ndef compose(self) -> ComposeResult:\nyield Header()\ndef on_mount(self) -> None:\nself.title = \"Header Application\"\nself.sub_title = \"With title and sub-title\"\nif __name__ == \"__main__\":\napp = HeaderApp()\napp.run()\n
"},{"location":"widgets/header/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description tall bool True Whether the Header widget is displayed as tall or not. The tall variant is 3 cells tall by default. The non-tall variant is a single cell tall. This can be toggled by clicking on the header."},{"location":"widgets/header/#messages","title":"Messages","text":"

This widget posts no messages.

"},{"location":"widgets/header/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/header/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/header/#textual.widgets.Header","title":"textual.widgets.Header class","text":"
def __init__(\nself,\nshow_clock=False,\n*,\nname=None,\nid=None,\nclasses=None\n):\n

Bases: Widget

A header widget with icon and clock.

Parameters Name Type Description Default show_clock bool

True if the clock should be shown on the right of the header.

False name str | None

The name of the header widget.

None id str | None

The ID of the header widget in the DOM.

None classes str | None

The CSS classes of the header widget.

None"},{"location":"widgets/header/#textual.widgets._header.Header.screen_sub_title","title":"screen_sub_title property","text":"
screen_sub_title: str\n

The sub-title that this header will display.

This depends on Screen.sub_title and App.sub_title.

"},{"location":"widgets/header/#textual.widgets._header.Header.screen_title","title":"screen_title property","text":"
screen_title: str\n

The title that this header will display.

This depends on Screen.title and App.title.

"},{"location":"widgets/header/#textual.widgets._header.Header.tall","title":"tall class-attribute instance-attribute","text":"
tall: Reactive[bool] = Reactive(False)\n

Set to True for a taller header or False for a single line header.

"},{"location":"widgets/input/","title":"Input","text":"

A single-line text input widget.

  • Focusable
  • Container
"},{"location":"widgets/input/#examples","title":"Examples","text":""},{"location":"widgets/input/#a-simple-example","title":"A Simple Example","text":"

The example below shows how you might create a simple form using two Input widgets.

Outputinput.py

InputApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aDarren\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aLast\u00a0Name\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

from textual.app import App, ComposeResult\nfrom textual.widgets import Input\nclass InputApp(App):\ndef compose(self) -> ComposeResult:\nyield Input(placeholder=\"First Name\")\nyield Input(placeholder=\"Last Name\")\nif __name__ == \"__main__\":\napp = InputApp()\napp.run()\n
"},{"location":"widgets/input/#validating-input","title":"Validating Input","text":"

You can supply one or more validators to the Input widget to validate the value.

All the supplied validators will run when the value changes, the Input is submitted, or focus moves out of the Input. The values \"changed\", \"submitted\", and \"blur\", can be passed as an iterable to the Input parameter validate_on to request that validation occur only on the respective mesages. (See InputValidationOn and Input.validate_on.) For example, the code below creates an Input widget that only gets validated when the value is submitted explicitly:

input = Input(validate_on=[\"submitted\"])\n

Validation is considered to have failed if any of the validators fail.

You can check whether the validation succeeded or failed inside an Input.Changed or Input.Submitted handler by looking at the validation_result attribute on these events.

In the example below, we show how to combine multiple validators and update the UI to tell the user why validation failed. Click the tabs to see the output for validation failures and successes.

input_validation.pyValidation FailureValidation Success
from textual import on\nfrom textual.app import App, ComposeResult\nfrom textual.validation import Function, Number, ValidationResult, Validator\nfrom textual.widgets import Input, Label, Pretty\nclass InputApp(App):\n# (6)!\nCSS = \"\"\"\n    Input.-valid {\n        border: tall $success 60%;\n    }\n    Input.-valid:focus {\n        border: tall $success;\n    }\n    Input {\n        margin: 1 1;\n    }\n    Label {\n        margin: 1 2;\n    }\n    Pretty {\n        margin: 1 2;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Label(\"Enter an even number between 1 and 100 that is also a palindrome.\")\nyield Input(\nplaceholder=\"Enter a number...\",\nvalidators=[\nNumber(minimum=1, maximum=100),  # (1)!\nFunction(is_even, \"Value is not even.\"),  # (2)!\nPalindrome(),  # (3)!\n],\n)\nyield Pretty([])\n@on(Input.Changed)\ndef show_invalid_reasons(self, event: Input.Changed) -> None:\n# Updating the UI to show the reasons why validation failed\nif not event.validation_result.is_valid:  # (4)!\nself.query_one(Pretty).update(event.validation_result.failure_descriptions)\nelse:\nself.query_one(Pretty).update([])\ndef is_even(value: str) -> bool:\ntry:\nreturn int(value) % 2 == 0\nexcept ValueError:\nreturn False\n# A custom validator\nclass Palindrome(Validator):  # (5)!\ndef validate(self, value: str) -> ValidationResult:\n\"\"\"Check a string is equal to its reverse.\"\"\"\nif self.is_palindrome(value):\nreturn self.success()\nelse:\nreturn self.failure(\"That's not a palindrome :/\")\n@staticmethod\ndef is_palindrome(value: str) -> bool:\nreturn value == value[::-1]\napp = InputApp()\nif __name__ == \"__main__\":\napp.run()\n
  1. Number is a built-in Validator. It checks that the value in the Input is a valid number, and optionally can check that it falls within a range.
  2. Function lets you quickly define custom validation constraints. In this case, we check the value in the Input is even.
  3. Palindrome is a custom Validator defined below.
  4. The Input.Changed event has a validation_result attribute which contains information about the validation that occurred when the value changed.
  5. Here's how we can implement a custom validator which checks if a string is a palindrome. Note how the description passed into self.failure corresponds to the message seen on UI.
  6. Textual offers default styling for the -invalid CSS class (a red border), which is automatically applied to Input when validation fails. We can also provide custom styling for the -valid class, as seen here. In this case, we add a green border around the Input to indicate successful validation.

InputApp Enter\u00a0an\u00a0even\u00a0number\u00a0between\u00a01\u00a0and\u00a0100\u00a0that\u00a0is\u00a0also\u00a0a\u00a0palindrome. \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a-23\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e [ 'Must\u00a0be\u00a0between\u00a01\u00a0and\u00a0100.', 'Value\u00a0is\u00a0not\u00a0even.', \"That's\u00a0not\u00a0a\u00a0palindrome\u00a0:/\" ]

InputApp Enter\u00a0an\u00a0even\u00a0number\u00a0between\u00a01\u00a0and\u00a0100\u00a0that\u00a0is\u00a0also\u00a0a\u00a0palindrome. \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a44\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e []

Textual offers several built-in validators for common requirements, but you can easily roll your own by extending Validator, as seen for Palindrome in the example above.

"},{"location":"widgets/input/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description cursor_blink bool True True if cursor blinking is enabled. value str \"\" The value currently in the text input. cursor_position int 0 The index of the cursor in the value string. placeholder str str The dimmed placeholder text to display when the input is empty. password bool False True if the input should be masked."},{"location":"widgets/input/#messages","title":"Messages","text":"
  • Input.Changed
  • Input.Submitted
"},{"location":"widgets/input/#bindings","title":"Bindings","text":"

The input widget defines the following bindings:

Key(s) Description left Move the cursor left. ctrl+left Move the cursor one word to the left. right Move the cursor right or accept the completion suggestion. ctrl+right Move the cursor one word to the right. backspace Delete the character to the left of the cursor. home,ctrl+a Go to the beginning of the input. end,ctrl+e Go to the end of the input. delete,ctrl+d Delete the character to the right of the cursor. enter Submit the current value of the input. ctrl+w Delete the word to the left of the cursor. ctrl+u Delete everything to the left of the cursor. ctrl+f Delete the word to the right of the cursor. ctrl+k Delete everything to the right of the cursor."},{"location":"widgets/input/#component-classes","title":"Component Classes","text":"

The input widget provides the following component classes:

Class Description input--cursor Target the cursor. input--placeholder Target the placeholder text (when it exists). input--suggestion Target the auto-completion suggestion (when it exists)."},{"location":"widgets/input/#additional-notes","title":"Additional Notes","text":"
  • The spacing around the text content is due to border. To remove it, set border: none; in your CSS.
"},{"location":"widgets/input/#textual.widgets.Input","title":"textual.widgets.Input class","text":"
def __init__(\nself,\nvalue=None,\nplaceholder=\"\",\nhighlighter=None,\npassword=False,\n*,\nsuggester=None,\nvalidators=None,\nvalidate_on=None,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: Widget

A text input widget.

Parameters Name Type Description Default value str | None

An optional default value for the input.

None placeholder str

Optional placeholder text for the input.

'' highlighter Highlighter | None

An optional highlighter for the input.

None password bool

Flag to say if the field should obfuscate its content.

False suggester Suggester | None

Suggester associated with this input instance.

None validators Validator | Iterable[Validator] | None

An iterable of validators that the Input value will be checked against.

None validate_on Iterable[InputValidationOn] | None

Zero or more of the values \"blur\", \"changed\", and \"submitted\", which determine when to do input validation. The default is to do validation for all messages.

None name str | None

Optional name for the input widget.

None id str | None

Optional ID for the widget.

None classes str | None

Optional initial classes for the widget.

None disabled bool

Whether the input is disabled or not.

False"},{"location":"widgets/input/#textual.widgets._input.Input.BINDINGS","title":"BINDINGS class-attribute","text":"
BINDINGS: list[BindingType] = [\nBinding(\n\"left\", \"cursor_left\", \"cursor left\", show=False\n),\nBinding(\n\"ctrl+left\",\n\"cursor_left_word\",\n\"cursor left word\",\nshow=False,\n),\nBinding(\n\"right\", \"cursor_right\", \"cursor right\", show=False\n),\nBinding(\n\"ctrl+right\",\n\"cursor_right_word\",\n\"cursor right word\",\nshow=False,\n),\nBinding(\n\"backspace\",\n\"delete_left\",\n\"delete left\",\nshow=False,\n),\nBinding(\"home,ctrl+a\", \"home\", \"home\", show=False),\nBinding(\"end,ctrl+e\", \"end\", \"end\", show=False),\nBinding(\n\"delete,ctrl+d\",\n\"delete_right\",\n\"delete right\",\nshow=False,\n),\nBinding(\"enter\", \"submit\", \"submit\", show=False),\nBinding(\n\"ctrl+w\",\n\"delete_left_word\",\n\"delete left to start of word\",\nshow=False,\n),\nBinding(\n\"ctrl+u\",\n\"delete_left_all\",\n\"delete all to the left\",\nshow=False,\n),\nBinding(\n\"ctrl+f\",\n\"delete_right_word\",\n\"delete right to start of word\",\nshow=False,\n),\nBinding(\n\"ctrl+k\",\n\"delete_right_all\",\n\"delete all to the right\",\nshow=False,\n),\n]\n
Key(s) Description left Move the cursor left. ctrl+left Move the cursor one word to the left. right Move the cursor right or accept the completion suggestion. ctrl+right Move the cursor one word to the right. backspace Delete the character to the left of the cursor. home,ctrl+a Go to the beginning of the input. end,ctrl+e Go to the end of the input. delete,ctrl+d Delete the character to the right of the cursor. enter Submit the current value of the input. ctrl+w Delete the word to the left of the cursor. ctrl+u Delete everything to the left of the cursor. ctrl+f Delete the word to the right of the cursor. ctrl+k Delete everything to the right of the cursor."},{"location":"widgets/input/#textual.widgets._input.Input.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
COMPONENT_CLASSES: set[str] = {\n\"input--cursor\",\n\"input--placeholder\",\n\"input--suggestion\",\n}\n
Class Description input--cursor Target the cursor. input--placeholder Target the placeholder text (when it exists). input--suggestion Target the auto-completion suggestion (when it exists)."},{"location":"widgets/input/#textual.widgets._input.Input.cursor_width","title":"cursor_width property","text":"
cursor_width: int\n

The width of the input (with extra space for cursor at the end).

"},{"location":"widgets/input/#textual.widgets._input.Input.suggester","title":"suggester instance-attribute","text":"
suggester: Suggester | None = suggester\n

The suggester used to provide completions as the user types.

"},{"location":"widgets/input/#textual.widgets._input.Input.validate_on","title":"validate_on instance-attribute","text":"
validate_on = (\nset(validate_on) & _POSSIBLE_VALIDATE_ON_VALUES\nif validate_on is not None\nelse _POSSIBLE_VALIDATE_ON_VALUES\n)\n

Set with event names to do input validation on.

Validation can only be performed on blur, on input changes and on input submission.

Example

This creates an Input widget that only gets validated when the value is submitted explicitly:

input = Input(validate_on=[\"submitted\"])\n
"},{"location":"widgets/input/#textual.widgets._input.Input.Changed","title":"Changed class","text":"

Bases: Message

Posted when the value changes.

Can be handled using on_input_changed in a subclass of Input or in a parent widget in the DOM.

"},{"location":"widgets/input/#textual.widgets._input.Input.Changed.control","title":"control property","text":"
control: Input\n

Alias for self.input.

"},{"location":"widgets/input/#textual.widgets._input.Input.Changed.input","title":"input instance-attribute","text":"
input: Input\n

The Input widget that was changed.

"},{"location":"widgets/input/#textual.widgets._input.Input.Changed.validation_result","title":"validation_result class-attribute instance-attribute","text":"
validation_result: ValidationResult | None = None\n

The result of validating the value (formed by combining the results from each validator), or None if validation was not performed (for example when no validators are specified in the Inputs init)

"},{"location":"widgets/input/#textual.widgets._input.Input.Changed.value","title":"value instance-attribute","text":"
value: str\n

The value that the input was changed to.

"},{"location":"widgets/input/#textual.widgets._input.Input.Submitted","title":"Submitted class","text":"

Bases: Message

Posted when the enter key is pressed within an Input.

Can be handled using on_input_submitted in a subclass of Input or in a parent widget in the DOM.

"},{"location":"widgets/input/#textual.widgets._input.Input.Submitted.control","title":"control property","text":"
control: Input\n

Alias for self.input.

"},{"location":"widgets/input/#textual.widgets._input.Input.Submitted.input","title":"input instance-attribute","text":"
input: Input\n

The Input widget that is being submitted.

"},{"location":"widgets/input/#textual.widgets._input.Input.Submitted.validation_result","title":"validation_result class-attribute instance-attribute","text":"
validation_result: ValidationResult | None = None\n

The result of validating the value on submission, formed by combining the results for each validator. This value will be None if no validation was performed, which will be the case if no validators are supplied to the corresponding Input widget.

"},{"location":"widgets/input/#textual.widgets._input.Input.Submitted.value","title":"value instance-attribute","text":"
value: str\n

The value of the Input being submitted.

"},{"location":"widgets/input/#textual.widgets._input.Input.action_cursor_left","title":"action_cursor_left method","text":"
def action_cursor_left(self):\n

Move the cursor one position to the left.

"},{"location":"widgets/input/#textual.widgets._input.Input.action_cursor_left_word","title":"action_cursor_left_word method","text":"
def action_cursor_left_word(self):\n

Move the cursor left to the start of a word.

"},{"location":"widgets/input/#textual.widgets._input.Input.action_cursor_right","title":"action_cursor_right method","text":"
def action_cursor_right(self):\n

Accept an auto-completion or move the cursor one position to the right.

"},{"location":"widgets/input/#textual.widgets._input.Input.action_cursor_right_word","title":"action_cursor_right_word method","text":"
def action_cursor_right_word(self):\n

Move the cursor right to the start of a word.

"},{"location":"widgets/input/#textual.widgets._input.Input.action_delete_left","title":"action_delete_left method","text":"
def action_delete_left(self):\n

Delete one character to the left of the current cursor position.

"},{"location":"widgets/input/#textual.widgets._input.Input.action_delete_left_all","title":"action_delete_left_all method","text":"
def action_delete_left_all(self):\n

Delete all characters to the left of the cursor position.

"},{"location":"widgets/input/#textual.widgets._input.Input.action_delete_left_word","title":"action_delete_left_word method","text":"
def action_delete_left_word(self):\n

Delete leftward of the cursor position to the start of a word.

"},{"location":"widgets/input/#textual.widgets._input.Input.action_delete_right","title":"action_delete_right method","text":"
def action_delete_right(self):\n

Delete one character at the current cursor position.

"},{"location":"widgets/input/#textual.widgets._input.Input.action_delete_right_all","title":"action_delete_right_all method","text":"
def action_delete_right_all(self):\n

Delete the current character and all characters to the right of the cursor position.

"},{"location":"widgets/input/#textual.widgets._input.Input.action_delete_right_word","title":"action_delete_right_word method","text":"
def action_delete_right_word(self):\n

Delete the current character and all rightward to the start of the next word.

"},{"location":"widgets/input/#textual.widgets._input.Input.action_end","title":"action_end method","text":"
def action_end(self):\n

Move the cursor to the end of the input.

"},{"location":"widgets/input/#textual.widgets._input.Input.action_home","title":"action_home method","text":"
def action_home(self):\n

Move the cursor to the start of the input.

"},{"location":"widgets/input/#textual.widgets._input.Input.action_submit","title":"action_submit async","text":"
def action_submit(self):\n

Handle a submit action.

Normally triggered by the user pressing Enter. This may also run any validators.

"},{"location":"widgets/input/#textual.widgets._input.Input.insert_text_at_cursor","title":"insert_text_at_cursor method","text":"
def insert_text_at_cursor(self, text):\n

Insert new text at the cursor, move the cursor to the end of the new text.

Parameters Name Type Description Default text str

New text to insert.

required"},{"location":"widgets/input/#textual.widgets._input.Input.validate","title":"validate method","text":"
def validate(self, value):\n

Run all the validators associated with this Input on the supplied value.

Runs all validators, combines the result into one. If any of the validators failed, the combined result will be a failure. If no validators are present, None will be returned. This also sets the -invalid CSS class on the Input if the validation fails, and sets the -valid CSS class on the Input if the validation succeeds.

Returns Type Description ValidationResult | None

A ValidationResult indicating whether all validators succeeded or not. That is, if any validator fails, the result will be an unsuccessful validation.

"},{"location":"widgets/label/","title":"Label","text":"

Added in version 0.5.0

A widget which displays static text, but which can also contain more complex Rich renderables.

  • Focusable
  • Container
"},{"location":"widgets/label/#example","title":"Example","text":"

The example below shows how you can use a Label widget to display some text.

Outputlabel.py

LabelApp Hello,\u00a0world!

from textual.app import App, ComposeResult\nfrom textual.widgets import Label\nclass LabelApp(App):\ndef compose(self) -> ComposeResult:\nyield Label(\"Hello, world!\")\nif __name__ == \"__main__\":\napp = LabelApp()\napp.run()\n
"},{"location":"widgets/label/#reactive-attributes","title":"Reactive Attributes","text":"

This widget has no reactive attributes.

"},{"location":"widgets/label/#messages","title":"Messages","text":"

This widget posts no messages.

"},{"location":"widgets/label/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/label/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/label/#textual.widgets.Label","title":"textual.widgets.Label class","text":"

Bases: Static

A simple label widget for displaying text-oriented renderables.

"},{"location":"widgets/list_item/","title":"ListItem","text":"

Added in version 0.6.0

ListItem is the type of the elements in a ListView.

  • Focusable
  • Container
"},{"location":"widgets/list_item/#example","title":"Example","text":"

The example below shows an app with a simple ListView, consisting of multiple ListItems. The arrow keys can be used to navigate the list.

Outputlist_view.py

ListViewExample One Two Three

from textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Label, ListItem, ListView\nclass ListViewExample(App):\nCSS_PATH = \"list_view.tcss\"\ndef compose(self) -> ComposeResult:\nyield ListView(\nListItem(Label(\"One\")),\nListItem(Label(\"Two\")),\nListItem(Label(\"Three\")),\n)\nyield Footer()\nif __name__ == \"__main__\":\napp = ListViewExample()\napp.run()\n
"},{"location":"widgets/list_item/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description highlighted bool False True if this ListItem is highlighted"},{"location":"widgets/list_item/#messages","title":"Messages","text":"

This widget posts no messages.

"},{"location":"widgets/list_item/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/list_item/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/list_item/#textual.widgets.ListItem","title":"textual.widgets.ListItem class","text":"

Bases: Widget

A widget that is an item within a ListView.

A ListItem is designed for use within a ListView, please see ListView's documentation for more details on use.

"},{"location":"widgets/list_item/#textual.widgets._list_item.ListItem.highlighted","title":"highlighted class-attribute instance-attribute","text":"
highlighted = reactive(False)\n

Is this item highlighted?

"},{"location":"widgets/list_view/","title":"ListView","text":"

Added in version 0.6.0

Displays a vertical list of ListItems which can be highlighted and selected. Supports keyboard navigation.

  • Focusable
  • Container
"},{"location":"widgets/list_view/#example","title":"Example","text":"

The example below shows an app with a simple ListView.

Outputlist_view.pylist_view.tcss

ListViewExample One Two Three

from textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Label, ListItem, ListView\nclass ListViewExample(App):\nCSS_PATH = \"list_view.tcss\"\ndef compose(self) -> ComposeResult:\nyield ListView(\nListItem(Label(\"One\")),\nListItem(Label(\"Two\")),\nListItem(Label(\"Three\")),\n)\nyield Footer()\nif __name__ == \"__main__\":\napp = ListViewExample()\napp.run()\n
Screen {\nalign: center middle;\n}\nListView {\nwidth: 30;\nheight: auto;\nmargin: 2 2;\n}\nLabel {\npadding: 1 2;\n}\n
"},{"location":"widgets/list_view/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description index int 0 The currently highlighted index."},{"location":"widgets/list_view/#messages","title":"Messages","text":"
  • ListView.Highlighted
  • ListView.Selected
"},{"location":"widgets/list_view/#bindings","title":"Bindings","text":"

The list view widget defines the following bindings:

Key(s) Description enter Select the current item. up Move the cursor up. down Move the cursor down."},{"location":"widgets/list_view/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/list_view/#textual.widgets.ListView","title":"textual.widgets.ListView class","text":"
def __init__(\nself,\n*children,\ninitial_index=0,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: VerticalScroll

A vertical list view widget.

Displays a vertical list of ListItems which can be highlighted and selected using the mouse or keyboard.

Attributes Name Type Description index

The index in the list that's currently highlighted.

Parameters Name Type Description Default *children ListItem

The ListItems to display in the list.

() initial_index int | None

The index that should be highlighted when the list is first mounted.

0 name str | None

The name of the widget.

None id str | None

The unique ID of the widget used in CSS/query selection.

None classes str | None

The CSS classes of the widget.

None disabled bool

Whether the ListView is disabled or not.

False"},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.BINDINGS","title":"BINDINGS class-attribute","text":"
BINDINGS: list[BindingType] = [\nBinding(\"enter\", \"select_cursor\", \"Select\", show=False),\nBinding(\"up\", \"cursor_up\", \"Cursor Up\", show=False),\nBinding(\n\"down\", \"cursor_down\", \"Cursor Down\", show=False\n),\n]\n
Key(s) Description enter Select the current item. up Move the cursor up. down Move the cursor down."},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.highlighted_child","title":"highlighted_child property","text":"
highlighted_child: ListItem | None\n

The currently highlighted ListItem, or None if nothing is highlighted.

"},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Highlighted","title":"Highlighted class","text":"
def __init__(self, list_view, item):\n

Bases: Message

Posted when the highlighted item changes.

Highlighted item is controlled using up/down keys. Can be handled using on_list_view_highlighted in a subclass of ListView or in a parent widget in the DOM.

"},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Highlighted.ALLOW_SELECTOR_MATCH","title":"ALLOW_SELECTOR_MATCH class-attribute instance-attribute","text":"
ALLOW_SELECTOR_MATCH = {'item'}\n

Additional message attributes that can be used with the on decorator.

"},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Highlighted.control","title":"control property","text":"
control: ListView\n

The view that contains the item highlighted.

This is an alias for Highlighted.list_view and is used by the on decorator.

"},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Highlighted.item","title":"item instance-attribute","text":"
item: ListItem | None = item\n

The highlighted item, if there is one highlighted.

"},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Highlighted.list_view","title":"list_view instance-attribute","text":"
list_view: ListView = list_view\n

The view that contains the item highlighted.

"},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Selected","title":"Selected class","text":"
def __init__(self, list_view, item):\n

Bases: Message

Posted when a list item is selected, e.g. when you press the enter key on it.

Can be handled using on_list_view_selected in a subclass of ListView or in a parent widget in the DOM.

"},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Selected.ALLOW_SELECTOR_MATCH","title":"ALLOW_SELECTOR_MATCH class-attribute instance-attribute","text":"
ALLOW_SELECTOR_MATCH = {'item'}\n

Additional message attributes that can be used with the on decorator.

"},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Selected.control","title":"control property","text":"
control: ListView\n

The view that contains the item selected.

This is an alias for Selected.list_view and is used by the on decorator.

"},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Selected.item","title":"item instance-attribute","text":"
item: ListItem = item\n

The selected item.

"},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Selected.list_view","title":"list_view instance-attribute","text":"
list_view: ListView = list_view\n

The view that contains the item selected.

"},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.action_cursor_down","title":"action_cursor_down method","text":"
def action_cursor_down(self):\n

Highlight the next item in the list.

"},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.action_cursor_up","title":"action_cursor_up method","text":"
def action_cursor_up(self):\n

Highlight the previous item in the list.

"},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.action_select_cursor","title":"action_select_cursor method","text":"
def action_select_cursor(self):\n

Select the current item in the list.

"},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.append","title":"append method","text":"
def append(self, item):\n

Append a new ListItem to the end of the ListView.

Parameters Name Type Description Default item ListItem

The ListItem to append.

required Returns Type Description AwaitMount

An awaitable that yields control to the event loop until the DOM has been updated with the new child item.

"},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.clear","title":"clear method","text":"
def clear(self):\n

Clear all items from the ListView.

Returns Type Description AwaitRemove

An awaitable that yields control to the event loop until the DOM has been updated to reflect all children being removed.

"},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.extend","title":"extend method","text":"
def extend(self, items):\n

Append multiple new ListItems to the end of the ListView.

Parameters Name Type Description Default items Iterable[ListItem]

The ListItems to append.

required Returns Type Description AwaitMount

An awaitable that yields control to the event loop until the DOM has been updated with the new child items.

"},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.validate_index","title":"validate_index method","text":"
def validate_index(self, index):\n

Clamp the index to the valid range, or set to None if there's nothing to highlight.

Parameters Name Type Description Default index int | None

The index to clamp.

required Returns Type Description int | None

The clamped index.

"},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.watch_index","title":"watch_index method","text":"
def watch_index(self, old_index, new_index):\n

Updates the highlighting when the index changes.

"},{"location":"widgets/loading_indicator/","title":"LoadingIndicator","text":"

Added in version 0.15.0

Displays pulsating dots to indicate when data is being loaded.

  • Focusable
  • Container
"},{"location":"widgets/loading_indicator/#example","title":"Example","text":"

Simple usage example:

Outputloading_indicator.py

LoadingApp \u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf

from textual.app import App, ComposeResult\nfrom textual.widgets import LoadingIndicator\nclass LoadingApp(App):\ndef compose(self) -> ComposeResult:\nyield LoadingIndicator()\nif __name__ == \"__main__\":\napp = LoadingApp()\napp.run()\n
"},{"location":"widgets/loading_indicator/#changing-indicator-color","title":"Changing Indicator Color","text":"

You can set the color of the loading indicator by setting its color style.

Here's how you would do that with CSS:

LoadingIndicator {\ncolor: red;\n}\n
"},{"location":"widgets/loading_indicator/#reactive-attributes","title":"Reactive Attributes","text":"

This widget has no reactive attributes.

"},{"location":"widgets/loading_indicator/#messages","title":"Messages","text":"

This widget posts no messages.

"},{"location":"widgets/loading_indicator/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/loading_indicator/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/loading_indicator/#textual.widgets.LoadingIndicator","title":"textual.widgets.LoadingIndicator class","text":"

Bases: Widget

Display an animated loading indicator.

"},{"location":"widgets/log/","title":"Log","text":"

Added in version 0.32.0

A Log widget displays lines of text which may be appended to in realtime.

Call Log.write_line to write a line at a time, or Log.write_lines to write multiple lines at once. Call Log.clear to clear the Log widget.

Tip

See also RichLog which can write more than just text, and supports a number of advanced features.

  • Focusable
  • Container
"},{"location":"widgets/log/#example","title":"Example","text":"

The example below shows how to write text to a Log widget:

Outputlog.py

LogApp And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain. I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain. I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\u2584\u2584 I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.

from textual.app import App, ComposeResult\nfrom textual.widgets import Log\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass LogApp(App):\n\"\"\"An app with a simple log.\"\"\"\ndef compose(self) -> ComposeResult:\nyield Log()\ndef on_ready(self) -> None:\nlog = self.query_one(Log)\nlog.write_line(\"Hello, World!\")\nfor _ in range(10):\nlog.write_line(TEXT)\nif __name__ == \"__main__\":\napp = LogApp()\napp.run()\n
"},{"location":"widgets/log/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description max_lines int None Maximum number of lines in the log or None for no maximum. auto_scroll bool False Scroll to end of log when new lines are added."},{"location":"widgets/log/#messages","title":"Messages","text":"

This widget posts no messages.

"},{"location":"widgets/log/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/log/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/log/#textual.widgets.Log","title":"textual.widgets.Log class","text":"
def __init__(\nself,\nhighlight=False,\nmax_lines=None,\nauto_scroll=True,\nname=None,\nid=None,\nclasses=None,\ndisabled=False,\n):\n

Bases: ScrollView

A widget to log text.

Parameters Name Type Description Default highlight bool

Enable highlighting.

False max_lines int | None

Maximum number of lines to display.

None auto_scroll bool

Scroll to end on new lines.

True name str | None

The name of the text log.

None id str | None

The ID of the text log in the DOM.

None classes str | None

The CSS classes of the text log.

None disabled bool

Whether the text log is disabled or not.

False"},{"location":"widgets/log/#textual.widgets._log.Log.auto_scroll","title":"auto_scroll class-attribute instance-attribute","text":"
auto_scroll: var[bool] = auto_scroll\n

Automatically scroll to new lines.

"},{"location":"widgets/log/#textual.widgets._log.Log.highlight","title":"highlight instance-attribute","text":"
highlight = highlight\n

Enable highlighting.

"},{"location":"widgets/log/#textual.widgets._log.Log.highlighter","title":"highlighter instance-attribute","text":"
highlighter = ReprHighlighter()\n

The Rich Highlighter object to use, if highlight=True

"},{"location":"widgets/log/#textual.widgets._log.Log.line_count","title":"line_count property","text":"
line_count: int\n

Number of lines of content.

"},{"location":"widgets/log/#textual.widgets._log.Log.lines","title":"lines property","text":"
lines: Sequence[str]\n

The raw lines in the TextLog.

Note that this attribute is read only. Changing the lines will not update the Log's contents.

"},{"location":"widgets/log/#textual.widgets._log.Log.max_lines","title":"max_lines class-attribute instance-attribute","text":"
max_lines: var[int | None] = max_lines\n

Maximum number of lines to show

"},{"location":"widgets/log/#textual.widgets._log.Log.clear","title":"clear method","text":"
def clear(self):\n

Clear the Log.

Returns Type Description Self

The Log instance.

"},{"location":"widgets/log/#textual.widgets._log.Log.notify_style_update","title":"notify_style_update method","text":"
def notify_style_update(self):\n

Called by Textual when styles update.

"},{"location":"widgets/log/#textual.widgets._log.Log.refresh_lines","title":"refresh_lines method","text":"
def refresh_lines(self, y_start, line_count=1):\n

Refresh one or more lines.

Parameters Name Type Description Default y_start int

First line to refresh.

required line_count int

Total number of lines to refresh.

1"},{"location":"widgets/log/#textual.widgets._log.Log.write","title":"write method","text":"
def write(self, data, scroll_end=None):\n

Write to the log.

Parameters Name Type Description Default data str

Data to write.

required scroll_end bool | None

Scroll to the end after writing, or None to use self.auto_scroll.

None Returns Type Description Self

The Log instance.

"},{"location":"widgets/log/#textual.widgets._log.Log.write_line","title":"write_line method","text":"
def write_line(self, line):\n

Write content on a new line.

Parameters Name Type Description Default line str

String to write to the log.

required Returns Type Description Self

The Log instance.

"},{"location":"widgets/log/#textual.widgets._log.Log.write_lines","title":"write_lines method","text":"
def write_lines(self, lines, scroll_end=None):\n

Write an iterable of lines.

Parameters Name Type Description Default lines Iterable[str]

An iterable of strings to write.

required scroll_end bool | None

Scroll to the end after writing, or None to use self.auto_scroll.

None Returns Type Description Self

The Log instance.

"},{"location":"widgets/markdown/","title":"Markdown","text":"

Added in version 0.11.0

A widget to display a Markdown document.

  • Focusable
  • Container

Tip

See MarkdownViewer for a widget that adds additional features such as a Table of Contents.

"},{"location":"widgets/markdown/#example","title":"Example","text":"

The following example displays Markdown from a string.

Outputmarkdown.py

MarkdownExampleApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eMarkdown\u00a0Document\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 This\u00a0is\u00a0an\u00a0example\u00a0of\u00a0Textual's\u00a0Markdown\u00a0widget. \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Features\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Markdown\u00a0syntax\u00a0and\u00a0extensions\u00a0are\u00a0supported. \u25cf\u00a0Typography\u00a0emphasis,\u00a0strong,\u00a0inline\u00a0code\u00a0etc. \u25cf\u00a0Headers \u25cf\u00a0Lists\u00a0(bullet\u00a0and\u00a0ordered) \u25cf\u00a0Syntax\u00a0highlighted\u00a0code\u00a0blocks \u25cf\u00a0Tables!

from textual.app import App, ComposeResult\nfrom textual.widgets import Markdown\nEXAMPLE_MARKDOWN = \"\"\"\\\n# Markdown Document\nThis is an example of Textual's `Markdown` widget.\n## Features\nMarkdown syntax and extensions are supported.\n- Typography *emphasis*, **strong**, `inline code` etc.\n- Headers\n- Lists (bullet and ordered)\n- Syntax highlighted code blocks\n- Tables!\n\"\"\"\nclass MarkdownExampleApp(App):\ndef compose(self) -> ComposeResult:\nyield Markdown(EXAMPLE_MARKDOWN)\nif __name__ == \"__main__\":\napp = MarkdownExampleApp()\napp.run()\n
"},{"location":"widgets/markdown/#reactive-attributes","title":"Reactive Attributes","text":"

This widget has no reactive attributes.

"},{"location":"widgets/markdown/#messages","title":"Messages","text":"
  • Markdown.TableOfContentsUpdated
  • Markdown.TableOfContentsSelected
  • Markdown.LinkClicked
"},{"location":"widgets/markdown/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/markdown/#component-classes","title":"Component Classes","text":"

The markdown widget provides the following component classes:

These component classes target standard inline markdown styles. Changing these will potentially break the standard markdown formatting.

Class Description code_inline Target text that is styled as inline code. em Target text that is emphasized inline. s Target text that is styled inline with strykethrough. strong Target text that is styled inline with strong."},{"location":"widgets/markdown/#see-also","title":"See Also","text":"
  • MarkdownViewer code reference
"},{"location":"widgets/markdown/#textual.widgets.Markdown","title":"textual.widgets.Markdown class","text":"
def __init__(\nself,\nmarkdown=None,\n*,\nname=None,\nid=None,\nclasses=None,\nparser_factory=None\n):\n

Bases: Widget

Parameters Name Type Description Default markdown str | None

String containing Markdown or None to leave blank for now.

None name str | None

The name of the widget.

None id str | None

The ID of the widget in the DOM.

None classes str | None

The CSS classes of the widget.

None parser_factory Callable[[], MarkdownIt] | None

A factory function to return a configured MarkdownIt instance. If None, a \"gfm-like\" parser is used.

None"},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute instance-attribute","text":"
COMPONENT_CLASSES = {'em', 'strong', 's', 'code_inline'}\n

These component classes target standard inline markdown styles. Changing these will potentially break the standard markdown formatting.

Class Description code_inline Target text that is styled as inline code. em Target text that is emphasized inline. s Target text that is styled inline with strykethrough. strong Target text that is styled inline with strong."},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.LinkClicked","title":"LinkClicked class","text":"
def __init__(self, markdown, href):\n

Bases: Message

A link in the document was clicked.

"},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.LinkClicked.control","title":"control property","text":"
control: Markdown\n

The Markdown widget containing the link clicked.

This is an alias for LinkClicked.markdown and is used by the on decorator.

"},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.LinkClicked.href","title":"href instance-attribute","text":"
href: str = href\n

The link that was selected.

"},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.LinkClicked.markdown","title":"markdown instance-attribute","text":"
markdown: Markdown = markdown\n

The Markdown widget containing the link clicked.

"},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.TableOfContentsSelected","title":"TableOfContentsSelected class","text":"
def __init__(self, markdown, block_id):\n

Bases: Message

An item in the TOC was selected.

"},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.TableOfContentsSelected.block_id","title":"block_id instance-attribute","text":"
block_id: str = block_id\n

ID of the block that was selected.

"},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.TableOfContentsSelected.control","title":"control property","text":"
control: Markdown\n

The Markdown widget where the selected item is.

This is an alias for TableOfContentsSelected.markdown and is used by the on decorator.

"},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.TableOfContentsSelected.markdown","title":"markdown instance-attribute","text":"
markdown: Markdown = markdown\n

The Markdown widget where the selected item is.

"},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.TableOfContentsUpdated","title":"TableOfContentsUpdated class","text":"
def __init__(self, markdown, table_of_contents):\n

Bases: Message

The table of contents was updated.

"},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.TableOfContentsUpdated.control","title":"control property","text":"
control: Markdown\n

The Markdown widget associated with the table of contents.

This is an alias for TableOfContentsUpdated.markdown and is used by the on decorator.

"},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.TableOfContentsUpdated.markdown","title":"markdown instance-attribute","text":"
markdown: Markdown = markdown\n

The Markdown widget associated with the table of contents.

"},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.TableOfContentsUpdated.table_of_contents","title":"table_of_contents instance-attribute","text":"
table_of_contents: TableOfContentsType = table_of_contents\n

Table of contents.

"},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.goto_anchor","title":"goto_anchor method","text":"
def goto_anchor(self, anchor):\n

Try and find the given anchor in the current document.

Parameters Name Type Description Default anchor str

The anchor to try and find.

required Note

The anchor is found by looking at all of the headings in the document and finding the first one whose slug matches the anchor.

Note that the slugging method used is similar to that found on GitHub.

Returns Type Description bool

True when the anchor was found in the current document, False otherwise.

"},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.load","title":"load async","text":"
def load(self, path):\n

Load a new Markdown document.

Parameters Name Type Description Default path Path

Path to the document.

required Raises Type Description OSError

If there was some form of error loading the document.

Note

The exceptions that can be raised by this method are all of those that can be raised by calling Path.read_text.

"},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.sanitize_location","title":"sanitize_location staticmethod","text":"
def sanitize_location(location):\n

Given a location, break out the path and any anchor.

Parameters Name Type Description Default location str

The location to sanitize.

required Returns Type Description Path

A tuple of the path to the location cleaned of any anchor, plus

str

the anchor (or an empty string if none was found).

"},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.unhandled_token","title":"unhandled_token method","text":"
def unhandled_token(self, token):\n

Process an unhandled token.

Parameters Name Type Description Default token Token

The token to handle.

required Returns Type Description MarkdownBlock | None

Either a widget to be added to the output, or None.

"},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.update","title":"update method","text":"
def update(self, markdown):\n

Update the document with new Markdown.

Parameters Name Type Description Default markdown str

A string containing Markdown.

required Returns Type Description AwaitMount

An optionally awaitable object. Await this to ensure that all children have been mounted.

"},{"location":"widgets/markdown_viewer/","title":"MarkdownViewer","text":"

Added in version 0.11.0

A Widget to display Markdown content with an optional Table of Contents.

  • Focusable
  • Container

Note

This Widget adds browser-like functionality on top of the Markdown widget.

"},{"location":"widgets/markdown_viewer/#example","title":"Example","text":"

The following example displays Markdown from a string and a Table of Contents.

Outputmarkdown.py

MarkdownExampleApp \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u25bc\u00a0\u2160\u00a0Markdown\u00a0Viewer\u258a\u258e\u258a \u251c\u2500\u2500\u00a0\u2161\u00a0Features\u258a\u258eMarkdown\u00a0Viewer\u258a \u251c\u2500\u2500\u00a0\u2161\u00a0Tables\u258a\u258e\u258a \u2514\u2500\u2500\u00a0\u2161\u00a0Code\u00a0Blocks\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258aThis\u00a0is\u00a0an\u00a0example\u00a0of\u00a0Textual's\u00a0MarkdownViewer\u00a0widget. \u258a \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258a\u258e\u258a \u258a\u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Features\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258e\u258a \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258aMarkdown\u00a0syntax\u00a0and\u00a0extensions\u00a0are\u00a0supported. \u258a \u258a\u25cf\u00a0Typography\u00a0emphasis,\u00a0strong,\u00a0inline\u00a0code\u00a0etc. \u258a\u25cf\u00a0Headers \u258a\u25cf\u00a0Lists\u00a0(bullet\u00a0and\u00a0ordered) \u258a\u25cf\u00a0Syntax\u00a0highlighted\u00a0code\u00a0blocks \u258a\u25cf\u00a0Tables! \u258a \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258a\u258e\u258a \u258a\u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Tables\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258e\u258a \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258aTables\u00a0are\u00a0displayed\u00a0in\u00a0a\u00a0DataTable\u00a0widget. \u258a \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258a\u258e\u258a \u258a\u258eName\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0TypeDefaultDescription\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258e\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0\u258a \u258a\u258eshow_headerboolTrueShow\u00a0the\u00a0table\u00a0header\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258efixed_rowsint0Number\u00a0of\u00a0fixed\u00a0rows\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258efixed_columnsint0Number\u00a0of\u00a0fixed\u00a0columns\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258ezebra_stripesboolFalseDisplay\u00a0alternating\u00a0colors\u00a0on\u00a0rows\u258a \u258a\u258eheader_heightint1Height\u00a0of\u00a0header\u00a0row\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258eshow_cursorboolTrueShow\u00a0a\u00a0cell\u00a0cursor\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258e\u258a \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258a \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258a\u258e\u258a

from textual.app import App, ComposeResult\nfrom textual.widgets import MarkdownViewer\nEXAMPLE_MARKDOWN = \"\"\"\\\n# Markdown Viewer\nThis is an example of Textual's `MarkdownViewer` widget.\n## Features\nMarkdown syntax and extensions are supported.\n- Typography *emphasis*, **strong**, `inline code` etc.\n- Headers\n- Lists (bullet and ordered)\n- Syntax highlighted code blocks\n- Tables!\n## Tables\nTables are displayed in a DataTable widget.\n| Name            | Type   | Default | Description                        |\n| --------------- | ------ | ------- | ---------------------------------- |\n| `show_header`   | `bool` | `True`  | Show the table header              |\n| `fixed_rows`    | `int`  | `0`     | Number of fixed rows               |\n| `fixed_columns` | `int`  | `0`     | Number of fixed columns            |\n| `zebra_stripes` | `bool` | `False` | Display alternating colors on rows |\n| `header_height` | `int`  | `1`     | Height of header row               |\n| `show_cursor`   | `bool` | `True`  | Show a cell cursor                 |\n## Code Blocks\nCode blocks are syntax highlighted, with guidelines.\n```python\nclass ListViewExample(App):\n    def compose(self) -> ComposeResult:\n        yield ListView(\n            ListItem(Label(\"One\")),\n            ListItem(Label(\"Two\")),\n            ListItem(Label(\"Three\")),\n        )\n        yield Footer()\n```\n\"\"\"\nclass MarkdownExampleApp(App):\ndef compose(self) -> ComposeResult:\nyield MarkdownViewer(EXAMPLE_MARKDOWN, show_table_of_contents=True)\nif __name__ == \"__main__\":\napp = MarkdownExampleApp()\napp.run()\n
"},{"location":"widgets/markdown_viewer/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description show_table_of_contents bool True Wether a Table of Contents should be displayed with the Markdown."},{"location":"widgets/markdown_viewer/#messages","title":"Messages","text":"

This widget posts no messages.

"},{"location":"widgets/markdown_viewer/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/markdown_viewer/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/markdown_viewer/#see-also","title":"See Also","text":"
  • Markdown code reference
"},{"location":"widgets/markdown_viewer/#textual.widgets.MarkdownViewer","title":"textual.widgets.MarkdownViewer class","text":"
def __init__(\nself,\nmarkdown=None,\n*,\nshow_table_of_contents=True,\nname=None,\nid=None,\nclasses=None,\nparser_factory=None\n):\n

Bases: VerticalScroll

A Markdown viewer widget.

Parameters Name Type Description Default markdown str | None

String containing Markdown, or None to leave blank.

None show_table_of_contents bool

Show a table of contents in a sidebar.

True name str | None

The name of the widget.

None id str | None

The ID of the widget in the DOM.

None classes str | None

The CSS classes of the widget.

None parser_factory Callable[[], MarkdownIt] | None

A factory function to return a configured MarkdownIt instance. If None, a \"gfm-like\" parser is used.

None"},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.MarkdownViewer.document","title":"document property","text":"
document: Markdown\n

The Markdown document object.

"},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.MarkdownViewer.table_of_contents","title":"table_of_contents property","text":"
table_of_contents: MarkdownTableOfContents\n

The table of contents widget

"},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.MarkdownViewer.back","title":"back async","text":"
def back(self):\n

Go back one level in the history.

"},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.MarkdownViewer.forward","title":"forward async","text":"
def forward(self):\n

Go forward one level in the history.

"},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.MarkdownViewer.go","title":"go async","text":"
def go(self, location):\n

Navigate to a new document path.

"},{"location":"widgets/option_list/","title":"OptionList","text":"

Added in version 0.17.0

A widget for showing a vertical list of Rich renderable options.

  • Focusable
  • Container
"},{"location":"widgets/option_list/#examples","title":"Examples","text":""},{"location":"widgets/option_list/#options-as-simple-strings","title":"Options as simple strings","text":"

An OptionList can be constructed with a simple collection of string options:

Outputoption_list_strings.pyoption_list.tcss

OptionListApp \u2b58OptionListApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aAerilon\u258e \u258aAquaria\u258e \u258aCanceron\u258e \u258aCaprica\u258e \u258aGemenon\u258e \u258aLeonis\u258e \u258aLibran\u258e \u258aPicon\u258e \u258aSagittaron\u258e \u258aScorpia\u258e \u258aTauron\u258e \u258aVirgon\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

from textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Header, OptionList\nclass OptionListApp(App[None]):\nCSS_PATH = \"option_list.tcss\"\ndef compose(self) -> ComposeResult:\nyield Header()\nyield OptionList(\n\"Aerilon\",\n\"Aquaria\",\n\"Canceron\",\n\"Caprica\",\n\"Gemenon\",\n\"Leonis\",\n\"Libran\",\n\"Picon\",\n\"Sagittaron\",\n\"Scorpia\",\n\"Tauron\",\n\"Virgon\",\n)\nyield Footer()\nif __name__ == \"__main__\":\nOptionListApp().run()\n
Screen {\nalign: center middle;\n}\nOptionList {\nwidth: 70%;\nheight: 80%;\n}\n
"},{"location":"widgets/option_list/#options-as-option-instances","title":"Options as Option instances","text":"

For finer control over the options, the Option class can be used; this allows for setting IDs, setting initial disabled state, etc. The Separator class can be used to add separator lines between options.

Outputoption_list_options.pyoption_list.tcss

OptionListApp \u2b58OptionListApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aAerilon\u258e \u258aAquaria\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aCanceron\u258e \u258aCaprica\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aGemenon\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aLeonis\u258e \u258aLibran\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aPicon\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aSagittaron\u2584\u2584\u258e \u258aScorpia\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

from textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Header, OptionList\nfrom textual.widgets.option_list import Option, Separator\nclass OptionListApp(App[None]):\nCSS_PATH = \"option_list.tcss\"\ndef compose(self) -> ComposeResult:\nyield Header()\nyield OptionList(\nOption(\"Aerilon\", id=\"aer\"),\nOption(\"Aquaria\", id=\"aqu\"),\nSeparator(),\nOption(\"Canceron\", id=\"can\"),\nOption(\"Caprica\", id=\"cap\", disabled=True),\nSeparator(),\nOption(\"Gemenon\", id=\"gem\"),\nSeparator(),\nOption(\"Leonis\", id=\"leo\"),\nOption(\"Libran\", id=\"lib\"),\nSeparator(),\nOption(\"Picon\", id=\"pic\"),\nSeparator(),\nOption(\"Sagittaron\", id=\"sag\"),\nOption(\"Scorpia\", id=\"sco\"),\nSeparator(),\nOption(\"Tauron\", id=\"tau\"),\nSeparator(),\nOption(\"Virgon\", id=\"vir\"),\n)\nyield Footer()\nif __name__ == \"__main__\":\nOptionListApp().run()\n
Screen {\nalign: center middle;\n}\nOptionList {\nwidth: 70%;\nheight: 80%;\n}\n
"},{"location":"widgets/option_list/#options-as-rich-renderables","title":"Options as Rich renderables","text":"

Because the prompts for the options can be Rich renderables, this means they can be any height you wish. As an example, here is an option list comprised of Rich tables:

Outputoption_list_tables.pyoption_list.tcss

OptionListApp \u2b58OptionListApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Data\u00a0for\u00a0Aerilon\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258e \u258a\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u258e \u258a\u2503Patron\u00a0God\u00a0\u00a0\u00a0\u2503Population\u00a0\u00a0\u00a0\u00a0\u2503Capital\u00a0City\u00a0\u00a0\u2503\u258e \u258a\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\u2583\u2583\u258e \u258a\u2502Demeter\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u25021.2\u00a0Billion\u00a0\u00a0\u00a0\u2502Gaoth\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u258e \u258a\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u258e \u258a\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Data\u00a0for\u00a0Aquaria\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258e \u258a\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u258e \u258a\u2503Patron\u00a0God\u00a0\u00a0\u00a0\u2503Population\u00a0\u00a0\u00a0\u2503Capital\u00a0City\u00a0\u00a0\u00a0\u2503\u258e \u258a\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\u258e \u258a\u2502Hermes\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250275,000\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502None\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u258e \u258a\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u258e \u258a\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Data\u00a0for\u00a0Canceron\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258e \u258a\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u258e \u258a\u2503Patron\u00a0God\u00a0\u00a0\u00a0\u2503Population\u00a0\u00a0\u00a0\u00a0\u2503Capital\u00a0City\u00a0\u00a0\u2503\u258e \u258a\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

from __future__ import annotations\nfrom rich.table import Table\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Header, OptionList\nCOLONIES: tuple[tuple[str, str, str, str], ...] = (\n(\"Aerilon\", \"Demeter\", \"1.2 Billion\", \"Gaoth\"),\n(\"Aquaria\", \"Hermes\", \"75,000\", \"None\"),\n(\"Canceron\", \"Hephaestus\", \"6.7 Billion\", \"Hades\"),\n(\"Caprica\", \"Apollo\", \"4.9 Billion\", \"Caprica City\"),\n(\"Gemenon\", \"Hera\", \"2.8 Billion\", \"Oranu\"),\n(\"Leonis\", \"Artemis\", \"2.6 Billion\", \"Luminere\"),\n(\"Libran\", \"Athena\", \"2.1 Billion\", \"None\"),\n(\"Picon\", \"Poseidon\", \"1.4 Billion\", \"Queenstown\"),\n(\"Sagittaron\", \"Zeus\", \"1.7 Billion\", \"Tawa\"),\n(\"Scorpia\", \"Dionysus\", \"450 Million\", \"Celeste\"),\n(\"Tauron\", \"Ares\", \"2.5 Billion\", \"Hypatia\"),\n(\"Virgon\", \"Hestia\", \"4.3 Billion\", \"Boskirk\"),\n)\nclass OptionListApp(App[None]):\nCSS_PATH = \"option_list.tcss\"\n@staticmethod\ndef colony(name: str, god: str, population: str, capital: str) -> Table:\ntable = Table(title=f\"Data for {name}\", expand=True)\ntable.add_column(\"Patron God\")\ntable.add_column(\"Population\")\ntable.add_column(\"Capital City\")\ntable.add_row(god, population, capital)\nreturn table\ndef compose(self) -> ComposeResult:\nyield Header()\nyield OptionList(*[self.colony(*row) for row in COLONIES])\nyield Footer()\nif __name__ == \"__main__\":\nOptionListApp().run()\n
Screen {\nalign: center middle;\n}\nOptionList {\nwidth: 70%;\nheight: 80%;\n}\n
"},{"location":"widgets/option_list/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description highlighted int | None None The index of the highlighted option. None means nothing is highlighted."},{"location":"widgets/option_list/#messages","title":"Messages","text":"
  • OptionList.OptionHighlighted
  • OptionList.OptionSelected

Both of the messages above inherit from the common base OptionList, so refer to its documentation to see what attributes are available.

"},{"location":"widgets/option_list/#bindings","title":"Bindings","text":"

The option list widget defines the following bindings:

Key(s) Description down Move the highlight down. end Move the highlight to the last option. enter Select the current option. home Move the highlight to the first option. pagedown Move the highlight down a page of options. pageup Move the highlight up a page of options. up Move the highlight up."},{"location":"widgets/option_list/#component-classes","title":"Component Classes","text":"

The option list provides the following component classes:

Class Description option-list--option-disabled Target disabled options. option-list--option-highlighted Target the highlighted option. option-list--option-highlighted-disabled Target a disabled option that is also highlighted. option-list--option-hover Target an option that has the mouse over it. option-list--option-hover-disabled Target a disabled option that has the mouse over it. option-list--option-hover-highlighted Target a highlighted option that has the mouse over it. option-list--option-hover-highlighted-disabled Target a disabled highlighted option that has the mouse over it. option-list--separator Target the separators."},{"location":"widgets/option_list/#textual.widgets.OptionList","title":"textual.widgets.OptionList class","text":"
def __init__(\nself,\n*content,\nname=None,\nid=None,\nclasses=None,\ndisabled=False,\nwrap=True\n):\n

Bases: ScrollView

A vertical option list with bounce-bar highlighting.

Parameters Name Type Description Default *content NewOptionListContent

The content for the option list.

() name str | None

The name of the option list.

None id str | None

The ID of the option list in the DOM.

None classes str | None

The CSS classes of the option list.

None disabled bool

Whether the option list is disabled or not.

False wrap bool

Should prompts be auto-wrapped?

True"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.BINDINGS","title":"BINDINGS class-attribute","text":"
BINDINGS: list[BindingType] = [\nBinding(\"down\", \"cursor_down\", \"Down\", show=False),\nBinding(\"end\", \"last\", \"Last\", show=False),\nBinding(\"enter\", \"select\", \"Select\", show=False),\nBinding(\"home\", \"first\", \"First\", show=False),\nBinding(\n\"pagedown\", \"page_down\", \"Page Down\", show=False\n),\nBinding(\"pageup\", \"page_up\", \"Page Up\", show=False),\nBinding(\"up\", \"cursor_up\", \"Up\", show=False),\n]\n
Key(s) Description down Move the highlight down. end Move the highlight to the last option. enter Select the current option. home Move the highlight to the first option. pagedown Move the highlight down a page of options. pageup Move the highlight up a page of options. up Move the highlight up."},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
COMPONENT_CLASSES: set[str] = {\n\"option-list--option\",\n\"option-list--option-disabled\",\n\"option-list--option-highlighted\",\n\"option-list--option-highlighted-disabled\",\n\"option-list--option-hover\",\n\"option-list--option-hover-disabled\",\n\"option-list--option-hover-highlighted\",\n\"option-list--option-hover-highlighted-disabled\",\n\"option-list--separator\",\n}\n
Class Description option-list--option-disabled Target disabled options. option-list--option-highlighted Target the highlighted option. option-list--option-highlighted-disabled Target a disabled option that is also highlighted. option-list--option-hover Target an option that has the mouse over it. option-list--option-hover-disabled Target a disabled option that has the mouse over it. option-list--option-hover-highlighted Target a highlighted option that has the mouse over it. option-list--option-hover-highlighted-disabled Target a disabled highlighted option that has the mouse over it. option-list--separator Target the separators."},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.highlighted","title":"highlighted class-attribute instance-attribute","text":"
highlighted: reactive[int | None] = None\n

The index of the currently-highlighted option, or None if no option is highlighted.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.option_count","title":"option_count property","text":"
option_count: int\n

The count of options.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.OptionHighlighted","title":"OptionHighlighted class","text":"

Bases: OptionMessage

Message sent when an option is highlighted.

Can be handled using on_option_list_option_highlighted in a subclass of OptionList or in a parent node in the DOM.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.OptionMessage","title":"OptionMessage class","text":"
def __init__(self, option_list, index):\n

Bases: Message

Base class for all option messages.

Parameters Name Type Description Default option_list OptionList

The option list that owns the option.

required index int

The index of the option that the message relates to.

required"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.OptionMessage.control","title":"control property","text":"
control: OptionList\n

The option list that sent the message.

This is an alias for OptionMessage.option_list and is used by the on decorator.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.OptionMessage.option","title":"option instance-attribute","text":"
option: Option = option_list.get_option_at_index(index)\n

The highlighted option.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.OptionMessage.option_id","title":"option_id instance-attribute","text":"
option_id: str | None = self.option.id\n

The ID of the option that the message relates to.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.OptionMessage.option_index","title":"option_index instance-attribute","text":"
option_index: int = index\n

The index of the option that the message relates to.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.OptionMessage.option_list","title":"option_list instance-attribute","text":"
option_list: OptionList = option_list\n

The option list that sent the message.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.OptionSelected","title":"OptionSelected class","text":"

Bases: OptionMessage

Message sent when an option is selected.

Can be handled using on_option_list_option_selected in a subclass of OptionList or in a parent node in the DOM.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.action_cursor_down","title":"action_cursor_down method","text":"
def action_cursor_down(self):\n

Move the highlight down by one option.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.action_cursor_up","title":"action_cursor_up method","text":"
def action_cursor_up(self):\n

Move the highlight up by one option.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.action_first","title":"action_first method","text":"
def action_first(self):\n

Move the highlight to the first option.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.action_last","title":"action_last method","text":"
def action_last(self):\n

Move the highlight to the last option.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.action_page_down","title":"action_page_down method","text":"
def action_page_down(self):\n

Move the highlight down one page.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.action_page_up","title":"action_page_up method","text":"
def action_page_up(self):\n

Move the highlight up one page.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.action_select","title":"action_select method","text":"
def action_select(self):\n

Select the currently-highlighted option.

If no option is selected, then nothing happens. If an option is selected, a OptionList.OptionSelected message will be posted.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.add_option","title":"add_option method","text":"
def add_option(self, item=None):\n

Add a new option to the end of the option list.

Parameters Name Type Description Default item NewOptionListContent

The new item to add.

None Returns Type Description Self

The OptionList instance.

Raises Type Description DuplicateID

If there is an attempt to use a duplicate ID.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.add_options","title":"add_options method","text":"
def add_options(self, items):\n

Add new options to the end of the option list.

Parameters Name Type Description Default items Iterable[NewOptionListContent]

The new items to add.

required Returns Type Description Self

The OptionList instance.

Raises Type Description DuplicateID

If there is an attempt to use a duplicate ID.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.clear_options","title":"clear_options method","text":"
def clear_options(self):\n

Clear the content of the option list.

Returns Type Description Self

The OptionList instance.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.disable_option","title":"disable_option method","text":"
def disable_option(self, option_id):\n

Disable the option with the given ID.

Parameters Name Type Description Default option_id str

The ID of the option to disable.

required Returns Type Description Self

The OptionList instance.

Raises Type Description OptionDoesNotExist

If no option has the given ID.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.disable_option_at_index","title":"disable_option_at_index method","text":"
def disable_option_at_index(self, index):\n

Disable the option at the given index.

Returns Type Description Self

The OptionList instance.

Raises Type Description OptionDoesNotExist

If there is no option with the given index.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.enable_option","title":"enable_option method","text":"
def enable_option(self, option_id):\n

Enable the option with the given ID.

Parameters Name Type Description Default option_id str

The ID of the option to enable.

required Returns Type Description Self

The OptionList instance.

Raises Type Description OptionDoesNotExist

If no option has the given ID.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.enable_option_at_index","title":"enable_option_at_index method","text":"
def enable_option_at_index(self, index):\n

Enable the option at the given index.

Returns Type Description Self

The OptionList instance.

Raises Type Description OptionDoesNotExist

If there is no option with the given index.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.get_option","title":"get_option method","text":"
def get_option(self, option_id):\n

Get the option with the given ID.

Parameters Name Type Description Default option_id str

The ID of the option to get.

required Returns Type Description Option

The option with the ID.

Raises Type Description OptionDoesNotExist

If no option has the given ID.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.get_option_at_index","title":"get_option_at_index method","text":"
def get_option_at_index(self, index):\n

Get the option at the given index.

Parameters Name Type Description Default index int

The index of the option to get.

required Returns Type Description Option

The option at that index.

Raises Type Description OptionDoesNotExist

If there is no option with the given index.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.get_option_index","title":"get_option_index method","text":"
def get_option_index(self, option_id):\n

Get the index of the option with the given ID.

Parameters Name Type Description Default option_id

The ID of the option to get the index of.

required Raises Type Description OptionDoesNotExist

If no option has the given ID.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.remove_option","title":"remove_option method","text":"
def remove_option(self, option_id):\n

Remove the option with the given ID.

Parameters Name Type Description Default option_id str

The ID of the option to remove.

required Returns Type Description Self

The OptionList instance.

Raises Type Description OptionDoesNotExist

If no option has the given ID.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.remove_option_at_index","title":"remove_option_at_index method","text":"
def remove_option_at_index(self, index):\n

Remove the option at the given index.

Parameters Name Type Description Default index int

The index of the option to remove.

required Returns Type Description Self

The OptionList instance.

Raises Type Description OptionDoesNotExist

If there is no option with the given index.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.replace_option_prompt","title":"replace_option_prompt method","text":"
def replace_option_prompt(self, option_id, prompt):\n

Replace the prompt of the option with the given ID.

Parameters Name Type Description Default option_id str

The ID of the option to replace the prompt of.

required prompt RenderableType

The new prompt for the option.

required Returns Type Description Self

The OptionList instance.

Raises Type Description OptionDoesNotExist

If no option has the given ID.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.replace_option_prompt_at_index","title":"replace_option_prompt_at_index method","text":"
def replace_option_prompt_at_index(self, index, prompt):\n

Replace the prompt of the option at the given index.

Parameters Name Type Description Default index int

The index of the option to replace the prompt of.

required prompt RenderableType

The new prompt for the option.

required Returns Type Description Self

The OptionList instance.

Raises Type Description OptionDoesNotExist

If there is no option with the given index.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.scroll_to_highlight","title":"scroll_to_highlight method","text":"
def scroll_to_highlight(self, top=False):\n

Ensure that the highlighted option is in view.

Parameters Name Type Description Default top bool

Scroll highlight to top of the list.

False"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.validate_highlighted","title":"validate_highlighted method","text":"
def validate_highlighted(self, highlighted):\n

Validate the highlighted property value on access.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.watch_highlighted","title":"watch_highlighted method","text":"
def watch_highlighted(self, highlighted):\n

React to the highlighted option having changed.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.watch_show_vertical_scrollbar","title":"watch_show_vertical_scrollbar method","text":"
def watch_show_vertical_scrollbar(self):\n

Handle the vertical scrollbar visibility status changing.

show_vertical_scrollbar is watched because it has an impact on the available width in which to render the renderables that make up the options in the list. If a vertical scrollbar appears or disappears we need to recalculate all the lines that make up the list.

"},{"location":"widgets/option_list/#textual.widgets.option_list.DuplicateID","title":"DuplicateID class","text":"

Bases: Exception

Exception raised if a duplicate ID is used.

"},{"location":"widgets/option_list/#textual.widgets.option_list.Option","title":"Option class","text":"
def __init__(self, prompt, id=None, disabled=False):\n

Class that holds the details of an individual option.

Parameters Name Type Description Default prompt RenderableType

The prompt for the option.

required id str | None

The optional ID for the option.

None disabled bool

The initial enabled/disabled state. Enabled by default.

False"},{"location":"widgets/option_list/#textual.widgets._option_list.Option.id","title":"id property","text":"
id: str | None\n

The optional ID for the option.

"},{"location":"widgets/option_list/#textual.widgets._option_list.Option.prompt","title":"prompt property","text":"
prompt: RenderableType\n

The prompt for the option.

"},{"location":"widgets/option_list/#textual.widgets._option_list.Option.set_prompt","title":"set_prompt method","text":"
def set_prompt(self, prompt):\n

Set the prompt for the option.

Parameters Name Type Description Default prompt RenderableType

The new prompt for the option.

required"},{"location":"widgets/option_list/#textual.widgets.option_list.OptionDoesNotExist","title":"OptionDoesNotExist class","text":"

Bases: Exception

Exception raised when a request has been made for an option that doesn't exist.

"},{"location":"widgets/option_list/#textual.widgets.option_list.Separator","title":"Separator class","text":"

Class used to add a separator to an OptionList.

"},{"location":"widgets/placeholder/","title":"Placeholder","text":"

Added in version 0.6.0

A widget that is meant to have no complex functionality. Use the placeholder widget when studying the layout of your app before having to develop your custom widgets.

The placeholder widget has variants that display different bits of useful information. Clicking a placeholder will cycle through its variants.

  • Focusable
  • Container
"},{"location":"widgets/placeholder/#example","title":"Example","text":"

The example below shows each placeholder variant.

Outputplaceholder.pyplaceholder.tcss

PlaceholderApp Placeholder\u00a0p2\u00a0here! This\u00a0is\u00a0a\u00a0custom\u00a0label\u00a0for\u00a0p1. #p4 #p3#p5Placeholde r Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0 26\u00a0x\u00a06amet,\u00a0consectetur\u00a027\u00a0x\u00a06 adipiscing\u00a0elit.\u00a0Etiam\u00a0 feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0 Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0 consectetur\u00a0adipiscing\u00a0elit.\u00a0Etiam\u00a040\u00a0x\u00a06 feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0accumsan.\u00a0 Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis\u00a0 gravida.\u00a0Phasellus\u00a0id\u00a0eleifend\u00a0ligula. Nullam\u00a0imperdiet\u00a0sem\u00a0tellus,\u00a0sed\u00a0 vehicula\u00a0nisl\u00a0faucibus\u00a0sit\u00a0amet.\u00a0Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0 Praesent\u00a0iaculis\u00a0tempor\u00a0ultricies.\u00a0Sedconsectetur\u00a0adipiscing\u00a0elit.\u00a0Etiam\u00a0 lacinia,\u00a0tellus\u00a0id\u00a0rutrum\u00a0lacinia,\u00a0feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0accumsan.\u00a0 sapien\u00a0sapien\u00a0congue\u00a0mauris,\u00a0sit\u00a0amet\u00a0Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis\u00a0

from textual.app import App, ComposeResult\nfrom textual.containers import Container, Horizontal, VerticalScroll\nfrom textual.widgets import Placeholder\nclass PlaceholderApp(App):\nCSS_PATH = \"placeholder.tcss\"\ndef compose(self) -> ComposeResult:\nyield VerticalScroll(\nContainer(\nPlaceholder(\"This is a custom label for p1.\", id=\"p1\"),\nPlaceholder(\"Placeholder p2 here!\", id=\"p2\"),\nPlaceholder(id=\"p3\"),\nPlaceholder(id=\"p4\"),\nPlaceholder(id=\"p5\"),\nPlaceholder(),\nHorizontal(\nPlaceholder(variant=\"size\", id=\"col1\"),\nPlaceholder(variant=\"text\", id=\"col2\"),\nPlaceholder(variant=\"size\", id=\"col3\"),\nid=\"c1\",\n),\nid=\"bot\",\n),\nContainer(\nPlaceholder(variant=\"text\", id=\"left\"),\nPlaceholder(variant=\"size\", id=\"topright\"),\nPlaceholder(variant=\"text\", id=\"botright\"),\nid=\"top\",\n),\nid=\"content\",\n)\nif __name__ == \"__main__\":\napp = PlaceholderApp()\napp.run()\n
Placeholder {\nheight: 100%;\n}\n#top {\nheight: 50%;\nwidth: 100%;\nlayout: grid;\ngrid-size: 2 2;\n}\n#left {\nrow-span: 2;\n}\n#bot {\nheight: 50%;\nwidth: 100%;\nlayout: grid;\ngrid-size: 8 8;\n}\n#c1 {\nrow-span: 4;\ncolumn-span: 8;\nheight: 100%;\n}\n#col1, #col2, #col3 {\nwidth: 1fr;\n}\n#p1 {\nrow-span: 4;\ncolumn-span: 4;\n}\n#p2 {\nrow-span: 2;\ncolumn-span: 4;\n}\n#p3 {\nrow-span: 2;\ncolumn-span: 2;\n}\n#p4 {\nrow-span: 1;\ncolumn-span: 2;\n}\n
"},{"location":"widgets/placeholder/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description variant str \"default\" Styling variant. One of default, size, text."},{"location":"widgets/placeholder/#messages","title":"Messages","text":"

This widget posts no messages.

"},{"location":"widgets/placeholder/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/placeholder/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/placeholder/#textual.widgets.Placeholder","title":"textual.widgets.Placeholder class","text":"
def __init__(\nself,\nlabel=None,\nvariant=\"default\",\n*,\nname=None,\nid=None,\nclasses=None\n):\n

Bases: Widget

A simple placeholder widget to use before you build your custom widgets.

This placeholder has a couple of variants that show different data. Clicking the placeholder cycles through the available variants, but a placeholder can also be initialised in a specific variant.

The variants available are:

Variant Placeholder shows default Identifier label or the ID of the placeholder. size Size of the placeholder. text Lorem Ipsum text. Parameters Name Type Description Default label str | None

The label to identify the placeholder. If no label is present, uses the placeholder ID instead.

None variant PlaceholderVariant

The variant of the placeholder.

'default' name str | None

The name of the placeholder.

None id str | None

The ID of the placeholder in the DOM.

None classes str | None

A space separated string with the CSS classes of the placeholder, if any.

None"},{"location":"widgets/placeholder/#textual.widgets._placeholder.Placeholder.variant","title":"variant class-attribute instance-attribute","text":"
variant: Reactive[\nPlaceholderVariant\n] = self.validate_variant(variant)\n

The current variant of the placeholder.

"},{"location":"widgets/placeholder/#textual.widgets._placeholder.Placeholder.cycle_variant","title":"cycle_variant method","text":"
def cycle_variant(self):\n

Get the next variant in the cycle.

Returns Type Description Self

The Placeholder instance.

"},{"location":"widgets/placeholder/#textual.widgets._placeholder.Placeholder.validate_variant","title":"validate_variant method","text":"
def validate_variant(self, variant):\n

Validate the variant to which the placeholder was set.

"},{"location":"widgets/pretty/","title":"Pretty","text":"

Display a pretty-formatted object.

  • Focusable
  • Container
"},{"location":"widgets/pretty/#example","title":"Example","text":"

The example below shows a pretty-formatted dict, but Pretty can display any Python object.

Outputpretty.py

PrettyExample { 'title':\u00a0'Back\u00a0to\u00a0the\u00a0Future', 'releaseYear':\u00a01985, 'director':\u00a0'Robert\u00a0Zemeckis', 'genre':\u00a0'Adventure,\u00a0Comedy,\u00a0Sci-Fi', 'cast':\u00a0[ {'actor':\u00a0'Michael\u00a0J.\u00a0Fox',\u00a0'character':\u00a0'Marty\u00a0McFly'}, {'actor':\u00a0'Christopher\u00a0Lloyd',\u00a0'character':\u00a0'Dr.\u00a0Emmett\u00a0Brown'} ] }

from textual.app import App, ComposeResult\nfrom textual.widgets import Pretty\nDATA = {\n\"title\": \"Back to the Future\",\n\"releaseYear\": 1985,\n\"director\": \"Robert Zemeckis\",\n\"genre\": \"Adventure, Comedy, Sci-Fi\",\n\"cast\": [\n{\"actor\": \"Michael J. Fox\", \"character\": \"Marty McFly\"},\n{\"actor\": \"Christopher Lloyd\", \"character\": \"Dr. Emmett Brown\"},\n],\n}\nclass PrettyExample(App):\ndef compose(self) -> ComposeResult:\nyield Pretty(DATA)\napp = PrettyExample()\nif __name__ == \"__main__\":\napp.run()\n
"},{"location":"widgets/pretty/#reactive-attributes","title":"Reactive Attributes","text":"

This widget has no reactive attributes.

"},{"location":"widgets/pretty/#messages","title":"Messages","text":"

This widget posts no messages.

"},{"location":"widgets/pretty/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/pretty/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/pretty/#textual.widgets.Pretty","title":"textual.widgets.Pretty class","text":"
def __init__(self, object, *, name=None, id=None, classes=None):\n

Bases: Widget

A pretty-printing widget.

Used to pretty-print any object.

Parameters Name Type Description Default object Any

The object to pretty-print.

required name str | None

The name of the pretty widget.

None id str | None

The ID of the pretty in the DOM.

None classes str | None

The CSS classes of the pretty.

None"},{"location":"widgets/pretty/#textual.widgets._pretty.Pretty.update","title":"update method","text":"
def update(self, object):\n

Update the content of the pretty widget.

Parameters Name Type Description Default object Any

The object to pretty-print.

required"},{"location":"widgets/progress_bar/","title":"ProgressBar","text":"

A widget that displays progress on a time-consuming task.

  • Focusable
  • Container
"},{"location":"widgets/progress_bar/#examples","title":"Examples","text":""},{"location":"widgets/progress_bar/#progress-bar-in-isolation","title":"Progress Bar in Isolation","text":"

The example below shows a progress bar in isolation. It shows the progress bar in:

  • its indeterminate state, when the total progress hasn't been set yet;
  • the middle of the progress; and
  • the completed state.
Indeterminate state39% doneCompletedprogress_bar_isolated.py

IndeterminateProgressBar \u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501--%--:--:-- \u00a0S\u00a0\u00a0Start\u00a0

IndeterminateProgressBar \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u250139%00:00:07 \u00a0S\u00a0\u00a0Start\u00a0

IndeterminateProgressBar \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501100%--:--:-- \u00a0S\u00a0\u00a0Start\u00a0

from textual.app import App, ComposeResult\nfrom textual.containers import Center, Middle\nfrom textual.timer import Timer\nfrom textual.widgets import Footer, ProgressBar\nclass IndeterminateProgressBar(App[None]):\nBINDINGS = [(\"s\", \"start\", \"Start\")]\nprogress_timer: Timer\n\"\"\"Timer to simulate progress happening.\"\"\"\ndef compose(self) -> ComposeResult:\nwith Center():\nwith Middle():\nyield ProgressBar()\nyield Footer()\ndef on_mount(self) -> None:\n\"\"\"Set up a timer to simulate progess happening.\"\"\"\nself.progress_timer = self.set_interval(1 / 10, self.make_progress, pause=True)\ndef make_progress(self) -> None:\n\"\"\"Called automatically to advance the progress bar.\"\"\"\nself.query_one(ProgressBar).advance(1)\ndef action_start(self) -> None:\n\"\"\"Start the progress tracking.\"\"\"\nself.query_one(ProgressBar).update(total=100)\nself.progress_timer.resume()\nif __name__ == \"__main__\":\nIndeterminateProgressBar().run()\n
"},{"location":"widgets/progress_bar/#complete-app-example","title":"Complete App Example","text":"

The example below shows a simple app with a progress bar that is keeping track of a fictitious funding level for an organisation.

OutputOutput (partial funding)Output (full funding)progress_bar.pyprogress_bar.tcss

Funding\u00a0tracking \u2b58Funding\u00a0tracking Funding:\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u25010% \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258a$$$\u258e\u00a0Donate\u00a0 \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

Funding\u00a0tracking \u2b58Funding\u00a0tracking Funding:\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u250135% \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258a$$$\u258e\u00a0Donate\u00a0 \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 Donation\u00a0for\u00a0$15\u00a0received! Donation\u00a0for\u00a0$20\u00a0received!

Funding\u00a0tracking \u2b58Funding\u00a0tracking Funding:\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501100% \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258a$$$\u258e\u00a0Donate\u00a0 \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 Donation\u00a0for\u00a0$15\u00a0received! Donation\u00a0for\u00a0$20\u00a0received! Donation\u00a0for\u00a0$65\u00a0received!

from textual.app import App, ComposeResult\nfrom textual.containers import Center, VerticalScroll\nfrom textual.widgets import Button, Header, Input, Label, ProgressBar\nclass FundingProgressApp(App[None]):\nCSS_PATH = \"progress_bar.tcss\"\nTITLE = \"Funding tracking\"\ndef compose(self) -> ComposeResult:\nyield Header()\nwith Center():\nyield Label(\"Funding: \")\nyield ProgressBar(total=100, show_eta=False)  # (1)!\nwith Center():\nyield Input(placeholder=\"$$$\")\nyield Button(\"Donate\")\nyield VerticalScroll(id=\"history\")\ndef on_button_pressed(self) -> None:\nself.add_donation()\ndef on_input_submitted(self) -> None:\nself.add_donation()\ndef add_donation(self) -> None:\ntext_value = self.query_one(Input).value\ntry:\nvalue = int(text_value)\nexcept ValueError:\nreturn\nself.query_one(ProgressBar).advance(value)\nself.query_one(VerticalScroll).mount(Label(f\"Donation for ${value} received!\"))\nself.query_one(Input).value = \"\"\nif __name__ == \"__main__\":\nFundingProgressApp().run()\n
  1. We create a progress bar with a total of 100 steps and we hide the ETA countdown because we are not keeping track of a continuous, uninterrupted task.
Container {\noverflow: hidden hidden;\nheight: auto;\n}\nCenter {\nmargin-top: 1;\nmargin-bottom: 1;\nlayout: horizontal;\n}\nProgressBar {\npadding-left: 3;\n}\nInput {\nwidth: 16;\n}\nVerticalScroll {\nheight: auto;\n}\n
"},{"location":"widgets/progress_bar/#custom-styling","title":"Custom Styling","text":"

This shows a progress bar with custom styling. Refer to the section below for more information.

Indeterminate state39% doneCompletedprogress_bar_styled.pyprogress_bar_styled.tcss

StyledProgressBar \u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501--%--:--:-- \u00a0S\u00a0\u00a0Start\u00a0

StyledProgressBar \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u250139%00:00:07 \u00a0S\u00a0\u00a0Start\u00a0

StyledProgressBar \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501100%--:--:-- \u00a0S\u00a0\u00a0Start\u00a0

from textual.app import App, ComposeResult\nfrom textual.containers import Center, Middle\nfrom textual.timer import Timer\nfrom textual.widgets import Footer, ProgressBar\nclass StyledProgressBar(App[None]):\nBINDINGS = [(\"s\", \"start\", \"Start\")]\nCSS_PATH = \"progress_bar_styled.tcss\"\nprogress_timer: Timer\n\"\"\"Timer to simulate progress happening.\"\"\"\ndef compose(self) -> ComposeResult:\nwith Center():\nwith Middle():\nyield ProgressBar()\nyield Footer()\ndef on_mount(self) -> None:\n\"\"\"Set up a timer to simulate progess happening.\"\"\"\nself.progress_timer = self.set_interval(1 / 10, self.make_progress, pause=True)\ndef make_progress(self) -> None:\n\"\"\"Called automatically to advance the progress bar.\"\"\"\nself.query_one(ProgressBar).advance(1)\ndef action_start(self) -> None:\n\"\"\"Start the progress tracking.\"\"\"\nself.query_one(ProgressBar).update(total=100)\nself.progress_timer.resume()\nif __name__ == \"__main__\":\nStyledProgressBar().run()\n
Bar > .bar--indeterminate {\ncolor: $primary;\nbackground: $secondary;\n}\nBar > .bar--bar {\ncolor: $primary;\nbackground: $primary 30%;\n}\nBar > .bar--complete {\ncolor: $error;\n}\nPercentageStatus {\ntext-style: reverse;\ncolor: $secondary;\n}\nETAStatus {\ntext-style: underline;\n}\n
"},{"location":"widgets/progress_bar/#styling-the-progress-bar","title":"Styling the Progress Bar","text":"

The progress bar is composed of three sub-widgets that can be styled independently:

Widget name ID Description Bar #bar The bar that visually represents the progress made. PercentageStatus #percentage Label that shows the percentage of completion. ETAStatus #eta Label that shows the estimated time to completion."},{"location":"widgets/progress_bar/#bar-component-classes","title":"Bar Component Classes","text":"

The bar sub-widget provides the component classes that follow.

These component classes let you modify the foreground and background color of the bar in its different states.

Class Description bar--bar Style of the bar (may be used to change the color). bar--complete Style of the bar when it's complete. bar--indeterminate Style of the bar when it's in an indeterminate state."},{"location":"widgets/progress_bar/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description percentage float | None The read-only percentage of progress that has been made. This is None if the total hasn't been set. progress float 0 The number of steps of progress already made. total float | None The total number of steps that we are keeping track of."},{"location":"widgets/progress_bar/#messages","title":"Messages","text":"

This widget posts no messages.

"},{"location":"widgets/progress_bar/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/progress_bar/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar","title":"textual.widgets.ProgressBar class","text":"
def __init__(\nself,\ntotal=None,\n*,\nshow_bar=True,\nshow_percentage=True,\nshow_eta=True,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: Widget

A progress bar widget.

The progress bar uses \"steps\" as the measurement unit.

Example
class MyApp(App):\ndef compose(self):\nyield ProgressBar(total=100)\ndef key_space(self):\nself.query_one(ProgressBar).advance(5)\n
Parameters Name Type Description Default total float | None

The total number of steps in the progress if known.

None show_bar bool

Whether to show the bar portion of the progress bar.

True show_percentage bool

Whether to show the percentage status of the bar.

True show_eta bool

Whether to show the ETA countdown of the progress bar.

True name str | None

The name of the widget.

None id str | None

The ID of the widget in the DOM.

None classes str | None

The CSS classes for the widget.

None disabled bool

Whether the widget is disabled or not.

False"},{"location":"widgets/progress_bar/#textual.widgets._progress_bar.ProgressBar.percentage","title":"percentage class-attribute instance-attribute","text":"
percentage: reactive[float | None] = reactive[\nOptional[float]\n](None)\n

The percentage of progress that has been completed.

The percentage is a value between 0 and 1 and the returned value is only None if the total progress of the bar hasn't been set yet.

Example
progress_bar = ProgressBar()\nprint(progress_bar.percentage)  # None\nprogress_bar.update(total=100)\nprogress_bar.advance(50)\nprint(progress_bar.percentage)  # 0.5\n
"},{"location":"widgets/progress_bar/#textual.widgets._progress_bar.ProgressBar.progress","title":"progress class-attribute instance-attribute","text":"
progress: reactive[float] = reactive(0.0)\n

The progress so far, in number of steps.

"},{"location":"widgets/progress_bar/#textual.widgets._progress_bar.ProgressBar.total","title":"total class-attribute instance-attribute","text":"
total: reactive[float | None] = total\n

The total number of steps associated with this progress bar, when known.

The value None will render an indeterminate progress bar.

"},{"location":"widgets/progress_bar/#textual.widgets._progress_bar.ProgressBar.advance","title":"advance method","text":"
def advance(self, advance=1):\n

Advance the progress of the progress bar by the given amount.

Example
progress_bar.advance(10)  # Advance 10 steps.\n
Parameters Name Type Description Default advance float

Number of steps to advance progress by.

1"},{"location":"widgets/progress_bar/#textual.widgets._progress_bar.ProgressBar.compute_percentage","title":"compute_percentage method","text":"
def compute_percentage(self):\n

Keep the percentage of progress updated automatically.

This will report a percentage of 1 if the total is zero.

"},{"location":"widgets/progress_bar/#textual.widgets._progress_bar.ProgressBar.update","title":"update method","text":"
def update(\nself, *, total=UNUSED, progress=UNUSED, advance=UNUSED\n):\n

Update the progress bar with the given options.

Example
progress_bar.update(\ntotal=200,  # Set new total to 200 steps.\nprogress=50,  # Set the progress to 50 (out of 200).\n)\n
Parameters Name Type Description Default total None | float | UnusedParameter

New total number of steps.

UNUSED progress float | UnusedParameter

Set the progress to the given number of steps.

UNUSED advance float | UnusedParameter

Advance the progress by this number of steps.

UNUSED"},{"location":"widgets/progress_bar/#textual.widgets._progress_bar.ProgressBar.validate_progress","title":"validate_progress method","text":"
def validate_progress(self, progress):\n

Clamp the progress between 0 and the maximum total.

"},{"location":"widgets/progress_bar/#textual.widgets._progress_bar.ProgressBar.validate_total","title":"validate_total method","text":"
def validate_total(self, total):\n

Ensure the total is not negative.

"},{"location":"widgets/progress_bar/#textual.widgets._progress_bar.ProgressBar.watch_total","title":"watch_total method","text":"
def watch_total(self, total):\n

Re-validate progress.

"},{"location":"widgets/radiobutton/","title":"RadioButton","text":"

Added in version 0.13.0

A simple radio button which stores a boolean value.

  • Focusable
  • Container

A radio button is best used with others inside a RadioSet.

"},{"location":"widgets/radiobutton/#example","title":"Example","text":"

The example below shows radio buttons, used within a RadioSet.

Outputradio_button.pyradio_button.tcss

RadioChoicesApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590\u25cf\u258cBattlestar\u00a0Galactica\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a01984\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a02021\u258e \u258a\u2590\u25cf\u258c\u00a0Serenity\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Trek:\u00a0The\u00a0Motion\u00a0Picture\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Wars:\u00a0A\u00a0New\u00a0Hope\u258e \u258a\u2590\u25cf\u258c\u00a0The\u00a0Last\u00a0Starfighter\u258e \u258a\u2590\u25cf\u258c\u00a0Total\u00a0Recall\u00a0\ud83d\udc49\u00a0\ud83d\udd34\u258e \u258a\u2590\u25cf\u258c\u00a0Wing\u00a0Commander\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

from textual.app import App, ComposeResult\nfrom textual.widgets import RadioButton, RadioSet\nclass RadioChoicesApp(App[None]):\nCSS_PATH = \"radio_button.tcss\"\ndef compose(self) -> ComposeResult:\nwith RadioSet():\nyield RadioButton(\"Battlestar Galactica\")\nyield RadioButton(\"Dune 1984\")\nyield RadioButton(\"Dune 2021\", id=\"focus_me\")\nyield RadioButton(\"Serenity\", value=True)\nyield RadioButton(\"Star Trek: The Motion Picture\")\nyield RadioButton(\"Star Wars: A New Hope\")\nyield RadioButton(\"The Last Starfighter\")\nyield RadioButton(\n\"Total Recall :backhand_index_pointing_right: :red_circle:\"\n)\nyield RadioButton(\"Wing Commander\")\ndef on_mount(self) -> None:\nself.query_one(RadioSet).focus()\nif __name__ == \"__main__\":\nRadioChoicesApp().run()\n
Screen {\nalign: center middle;\n}\nRadioSet {\nwidth: 50%;\n}\n
"},{"location":"widgets/radiobutton/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description value bool False The value of the radio button."},{"location":"widgets/radiobutton/#messages","title":"Messages","text":"
  • RadioButton.Changed
"},{"location":"widgets/radiobutton/#bindings","title":"Bindings","text":"

The radio button widget defines the following bindings:

Key(s) Description enter, space Toggle the value."},{"location":"widgets/radiobutton/#component-classes","title":"Component Classes","text":"

The checkbox widget inherits the following component classes:

Class Description toggle--button Targets the toggle button itself. toggle--label Targets the text label of the toggle button."},{"location":"widgets/radiobutton/#see-also","title":"See Also","text":"
  • RadioSet
"},{"location":"widgets/radiobutton/#textual.widgets.RadioButton","title":"textual.widgets.RadioButton class","text":"

Bases: ToggleButton

A radio button widget that represents a boolean value.

Note

A RadioButton is best used within a RadioSet.

"},{"location":"widgets/radiobutton/#textual.widgets._radio_button.RadioButton.BUTTON_INNER","title":"BUTTON_INNER class-attribute instance-attribute","text":"
BUTTON_INNER = '\u25cf'\n

The character used for the inside of the button.

"},{"location":"widgets/radiobutton/#textual.widgets._radio_button.RadioButton.Changed","title":"Changed class","text":"

Bases: ToggleButton.Changed

Posted when the value of the radio button changes.

This message can be handled using an on_radio_button_changed method.

"},{"location":"widgets/radiobutton/#textual.widgets._radio_button.RadioButton.Changed.control","title":"control property","text":"
control: RadioButton\n

Alias for Changed.radio_button.

"},{"location":"widgets/radiobutton/#textual.widgets._radio_button.RadioButton.Changed.radio_button","title":"radio_button property","text":"
radio_button: RadioButton\n

The radio button that was changed.

"},{"location":"widgets/radioset/","title":"RadioSet","text":"

Added in version 0.13.0

A container widget that groups RadioButtons together.

  • Focusable
  • Container
"},{"location":"widgets/radioset/#example","title":"Example","text":""},{"location":"widgets/radioset/#simple-example","title":"Simple example","text":"

The example below shows two radio sets, one built using a collection of radio buttons, the other a collection of simple strings.

Outputradio_set.pyradio_set.tcss

RadioChoicesApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590\u25cf\u258cBattlestar\u00a0Galactica\u258e\u258a\u2590\u25cf\u258c\u00a0Amanda\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a01984\u258e\u258a\u2590\u25cf\u258c\u00a0Connor\u00a0MacLeod\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a02021\u258e\u258a\u2590\u25cf\u258c\u00a0Duncan\u00a0MacLeod\u258e \u258a\u2590\u25cf\u258c\u00a0Serenity\u258e\u258a\u2590\u25cf\u258c\u00a0Heather\u00a0MacLeod\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Trek:\u00a0The\u00a0Motion\u00a0Picture\u258e\u258a\u2590\u25cf\u258c\u00a0Joe\u00a0Dawson\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Wars:\u00a0A\u00a0New\u00a0Hope\u258e\u258a\u2590\u25cf\u258c\u00a0Kurgan,\u00a0The\u258e \u258a\u2590\u25cf\u258c\u00a0The\u00a0Last\u00a0Starfighter\u258e\u258a\u2590\u25cf\u258c\u00a0Methos\u258e \u258a\u2590\u25cf\u258c\u00a0Total\u00a0Recall\u00a0\ud83d\udc49\u00a0\ud83d\udd34\u258e\u258a\u2590\u25cf\u258c\u00a0Rachel\u00a0Ellenstein\u258e \u258a\u2590\u25cf\u258c\u00a0Wing\u00a0Commander\u258e\u258a\u2590\u25cf\u258c\u00a0Ram\u00edrez\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.widgets import RadioButton, RadioSet\nclass RadioChoicesApp(App[None]):\nCSS_PATH = \"radio_set.tcss\"\ndef compose(self) -> ComposeResult:\nwith Horizontal():\n# A RadioSet built up from RadioButtons.\nwith RadioSet(id=\"focus_me\"):\nyield RadioButton(\"Battlestar Galactica\")\nyield RadioButton(\"Dune 1984\")\nyield RadioButton(\"Dune 2021\")\nyield RadioButton(\"Serenity\", value=True)\nyield RadioButton(\"Star Trek: The Motion Picture\")\nyield RadioButton(\"Star Wars: A New Hope\")\nyield RadioButton(\"The Last Starfighter\")\nyield RadioButton(\n\"Total Recall :backhand_index_pointing_right: :red_circle:\"\n)\nyield RadioButton(\"Wing Commander\")\n# A RadioSet built up from a collection of strings.\nyield RadioSet(\n\"Amanda\",\n\"Connor MacLeod\",\n\"Duncan MacLeod\",\n\"Heather MacLeod\",\n\"Joe Dawson\",\n\"Kurgan, [bold italic red]The[/]\",\n\"Methos\",\n\"Rachel Ellenstein\",\n\"Ram\u00edrez\",\n)\ndef on_mount(self) -> None:\nself.query_one(\"#focus_me\").focus()\nif __name__ == \"__main__\":\nRadioChoicesApp().run()\n
Screen {\nalign: center middle;\n}\nHorizontal {\nalign: center middle;\nheight: auto;\n}\nRadioSet {\nwidth: 45%;\n}\n
"},{"location":"widgets/radioset/#reacting-to-changes-in-a-radio-set","title":"Reacting to Changes in a Radio Set","text":"

Here is an example of using the message to react to changes in a RadioSet:

Outputradio_set_changed.pyradio_set_changed.tcss

RadioSetChangedApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590\u25cf\u258cBattlestar\u00a0Galactica\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a01984\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a02021\u258e \u258a\u2590\u25cf\u258c\u00a0Serenity\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Trek:\u00a0The\u00a0Motion\u00a0Picture\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Wars:\u00a0A\u00a0New\u00a0Hope\u258e \u258a\u2590\u25cf\u258c\u00a0The\u00a0Last\u00a0Starfighter\u258e \u258a\u2590\u25cf\u258c\u00a0Total\u00a0Recall\u00a0\ud83d\udc49\u00a0\ud83d\udd34\u258e \u258a\u2590\u25cf\u258c\u00a0Wing\u00a0Commander\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e Pressed\u00a0button\u00a0label:\u00a0Battlestar\u00a0Galactica Pressed\u00a0button\u00a0index:\u00a00

from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal, VerticalScroll\nfrom textual.widgets import Label, RadioButton, RadioSet\nclass RadioSetChangedApp(App[None]):\nCSS_PATH = \"radio_set_changed.tcss\"\ndef compose(self) -> ComposeResult:\nwith VerticalScroll():\nwith Horizontal():\nwith RadioSet(id=\"focus_me\"):\nyield RadioButton(\"Battlestar Galactica\")\nyield RadioButton(\"Dune 1984\")\nyield RadioButton(\"Dune 2021\")\nyield RadioButton(\"Serenity\", value=True)\nyield RadioButton(\"Star Trek: The Motion Picture\")\nyield RadioButton(\"Star Wars: A New Hope\")\nyield RadioButton(\"The Last Starfighter\")\nyield RadioButton(\n\"Total Recall :backhand_index_pointing_right: :red_circle:\"\n)\nyield RadioButton(\"Wing Commander\")\nwith Horizontal():\nyield Label(id=\"pressed\")\nwith Horizontal():\nyield Label(id=\"index\")\ndef on_mount(self) -> None:\nself.query_one(RadioSet).focus()\ndef on_radio_set_changed(self, event: RadioSet.Changed) -> None:\nself.query_one(\"#pressed\", Label).update(\nf\"Pressed button label: {event.pressed.label}\"\n)\nself.query_one(\"#index\", Label).update(\nf\"Pressed button index: {event.radio_set.pressed_index}\"\n)\nif __name__ == \"__main__\":\nRadioSetChangedApp().run()\n
VerticalScroll {\nalign: center middle;\n}\nHorizontal {\nalign: center middle;\nheight: auto;\n}\nRadioSet {\nwidth: 45%;\n}\n
"},{"location":"widgets/radioset/#messages","title":"Messages","text":"
  • RadioSet.Changed
"},{"location":"widgets/radioset/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/radioset/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/radioset/#see-also","title":"See Also","text":"
  • RadioButton
"},{"location":"widgets/radioset/#textual.widgets.RadioSet","title":"textual.widgets.RadioSet class","text":"
def __init__(\nself,\n*buttons,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: Container

Widget for grouping a collection of radio buttons into a set.

When a collection of RadioButtons are grouped with this widget, they will be treated as a mutually-exclusive grouping. If one button is turned on, the previously-on button will be turned off.

Parameters Name Type Description Default buttons str | RadioButton

The labels or RadioButtons to group together.

() name str | None

The name of the radio set.

None id str | None

The ID of the radio set in the DOM.

None classes str | None

The CSS classes of the radio set.

None disabled bool

Whether the radio set is disabled or not.

False Note

When a str label is provided, a RadioButton will be created from it.

"},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.BINDINGS","title":"BINDINGS class-attribute","text":"
BINDINGS: list[BindingType] = [\nBinding(\"down,right\", \"next_button\", \"\", show=False),\nBinding(\"enter,space\", \"toggle\", \"Toggle\", show=False),\nBinding(\"up,left\", \"previous_button\", \"\", show=False),\n]\n
Key(s) Description enter, space Toggle the currently-selected button. left, up Select the previous radio button in the set. right, down Select the next radio button in the set."},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.pressed_button","title":"pressed_button property","text":"
pressed_button: RadioButton | None\n

The currently-pressed RadioButton, or None if none are pressed.

"},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.pressed_index","title":"pressed_index property","text":"
pressed_index: int\n

The index of the currently-pressed RadioButton, or -1 if none are pressed.

"},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.Changed","title":"Changed class","text":"
def __init__(self, radio_set, pressed):\n

Bases: Message

Posted when the pressed button in the set changes.

This message can be handled using an on_radio_set_changed method.

Parameters Name Type Description Default pressed RadioButton

The radio button that was pressed.

required"},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.Changed.ALLOW_SELECTOR_MATCH","title":"ALLOW_SELECTOR_MATCH class-attribute instance-attribute","text":"
ALLOW_SELECTOR_MATCH = {'pressed'}\n

Additional message attributes that can be used with the on decorator.

"},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.Changed.control","title":"control property","text":"
control: RadioSet\n

A reference to the RadioSet that was changed.

This is an alias for Changed.radio_set and is used by the on decorator.

"},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.Changed.index","title":"index instance-attribute","text":"
index = radio_set.pressed_index\n

The index of the RadioButton that was pressed to make the change.

"},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.Changed.pressed","title":"pressed instance-attribute","text":"
pressed = pressed\n

The RadioButton that was pressed to make the change.

"},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.Changed.radio_set","title":"radio_set instance-attribute","text":"
radio_set = radio_set\n

A reference to the RadioSet that was changed.

"},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.action_next_button","title":"action_next_button method","text":"
def action_next_button(self):\n

Navigate to the next button in the set.

Note that this will wrap around to the start if at the end.

"},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.action_previous_button","title":"action_previous_button method","text":"
def action_previous_button(self):\n

Navigate to the previous button in the set.

Note that this will wrap around to the end if at the start.

"},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.action_toggle","title":"action_toggle method","text":"
def action_toggle(self):\n

Toggle the state of the currently-selected button.

"},{"location":"widgets/rich_log/","title":"RichLog","text":"

A RichLog is a widget which displays scrollable content that may be appended to in realtime.

Call RichLog.write with a string or Rich Renderable to write content to the end of the RichLog. Call RichLog.clear to clear the content.

Tip

See also Log which is an alternative to RichLog but specialized for simple text.

  • Focusable
  • Container
"},{"location":"widgets/rich_log/#example","title":"Example","text":"

The example below shows an application showing a RichLog with different kinds of data logged.

Outputrich_log.py

RichLogApp \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0previous_value=next(iter_values) \u2502\u00a0\u00a0\u00a0exceptStopIteration: \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0return \u2502\u00a0\u00a0\u00a0first=True\u2585\u2585 \u2502\u00a0\u00a0\u00a0forvalueiniter_values: \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0yieldfirst,False,previous_value \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0first=False \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0previous_value=value \u2502\u00a0\u00a0\u00a0yieldfirst,True,previous_value \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503lane\u2503swimmer\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503country\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503time\u00a0\u2503 \u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529 \u25024\u00a0\u00a0\u00a0\u2502Joseph\u00a0Schooling\u00a0\u00a0\u00a0\u00a0\u2502Singapore\u00a0\u00a0\u00a0\u00a0\u250250.39\u2502 \u25022\u00a0\u00a0\u00a0\u2502Michael\u00a0Phelps\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502United\u00a0States\u250251.14\u2502 \u25025\u00a0\u00a0\u00a0\u2502Chad\u00a0le\u00a0Clos\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502South\u00a0Africa\u00a0\u250251.14\u2502 \u25026\u00a0\u00a0\u00a0\u2502L\u00e1szl\u00f3\u00a0Cseh\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502Hungary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250251.14\u2502 \u25023\u00a0\u00a0\u00a0\u2502Li\u00a0Zhuhao\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502China\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250251.26\u2502 \u25028\u00a0\u00a0\u00a0\u2502Mehdy\u00a0Metella\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502France\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250251.58\u2502 \u25027\u00a0\u00a0\u00a0\u2502Tom\u00a0Shields\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502United\u00a0States\u250251.73\u2502 \u25021\u00a0\u00a0\u00a0\u2502Aleksandr\u00a0Sadovnikov\u2502Russia\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250251.84\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 Write\u00a0text\u00a0or\u00a0any\u00a0Rich\u00a0renderable! Key(key='H',\u00a0character='H',\u00a0name='upper_h',\u00a0is_printable=True) Key(key='i',\u00a0character='i',\u00a0name='i',\u00a0is_printable=True)

import csv\nimport io\nfrom rich.syntax import Syntax\nfrom rich.table import Table\nfrom textual import events\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import RichLog\nCSV = \"\"\"lane,swimmer,country,time\n4,Joseph Schooling,Singapore,50.39\n2,Michael Phelps,United States,51.14\n5,Chad le Clos,South Africa,51.14\n6,L\u00e1szl\u00f3 Cseh,Hungary,51.14\n3,Li Zhuhao,China,51.26\n8,Mehdy Metella,France,51.58\n7,Tom Shields,United States,51.73\n1,Aleksandr Sadovnikov,Russia,51.84\"\"\"\nCODE = '''\\\ndef loop_first_last(values: Iterable[T]) -> Iterable[tuple[bool, bool, T]]:\n    \"\"\"Iterate and generate a tuple with a flag for first and last value.\"\"\"\n    iter_values = iter(values)\n    try:\n        previous_value = next(iter_values)\n    except StopIteration:\n        return\n    first = True\n    for value in iter_values:\n        yield first, False, previous_value\n        first = False\n        previous_value = value\n    yield first, True, previous_value\\\n'''\nclass RichLogApp(App):\ndef compose(self) -> ComposeResult:\nyield RichLog(highlight=True, markup=True)\ndef on_ready(self) -> None:\n\"\"\"Called  when the DOM is ready.\"\"\"\ntext_log = self.query_one(RichLog)\ntext_log.write(Syntax(CODE, \"python\", indent_guides=True))\nrows = iter(csv.reader(io.StringIO(CSV)))\ntable = Table(*next(rows))\nfor row in rows:\ntable.add_row(*row)\ntext_log.write(table)\ntext_log.write(\"[bold magenta]Write text or any Rich renderable!\")\ndef on_key(self, event: events.Key) -> None:\n\"\"\"Write Key events to log.\"\"\"\ntext_log = self.query_one(RichLog)\ntext_log.write(event)\nif __name__ == \"__main__\":\napp = RichLogApp()\napp.run()\n
"},{"location":"widgets/rich_log/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description highlight bool False Automatically highlight content. markup bool False Apply Rich console markup. max_lines int None Maximum number of lines in the log or None for no maximum. min_width int 78 Minimum width of renderables. wrap bool False Enable word wrapping."},{"location":"widgets/rich_log/#messages","title":"Messages","text":"

This widget sends no messages.

"},{"location":"widgets/rich_log/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/rich_log/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/rich_log/#textual.widgets.RichLog","title":"textual.widgets.RichLog class","text":"
def __init__(\nself,\n*,\nmax_lines=None,\nmin_width=78,\nwrap=False,\nhighlight=False,\nmarkup=False,\nauto_scroll=True,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: ScrollView

A widget for logging text.

Parameters Name Type Description Default max_lines int | None

Maximum number of lines in the log or None for no maximum.

None min_width int

Minimum width of renderables.

78 wrap bool

Enable word wrapping (default is off).

False highlight bool

Automatically highlight content.

False markup bool

Apply Rich console markup.

False auto_scroll bool

Enable automatic scrolling to end.

True name str | None

The name of the text log.

None id str | None

The ID of the text log in the DOM.

None classes str | None

The CSS classes of the text log.

None disabled bool

Whether the text log is disabled or not.

False"},{"location":"widgets/rich_log/#textual.widgets._rich_log.RichLog.auto_scroll","title":"auto_scroll class-attribute instance-attribute","text":"
auto_scroll: var[bool] = auto_scroll\n

Automatically scroll to the end on write.

"},{"location":"widgets/rich_log/#textual.widgets._rich_log.RichLog.highlight","title":"highlight class-attribute instance-attribute","text":"
highlight: var[bool] = highlight\n

Automatically highlight content.

"},{"location":"widgets/rich_log/#textual.widgets._rich_log.RichLog.markup","title":"markup class-attribute instance-attribute","text":"
markup: var[bool] = markup\n

Apply Rich console markup.

"},{"location":"widgets/rich_log/#textual.widgets._rich_log.RichLog.max_lines","title":"max_lines class-attribute instance-attribute","text":"
max_lines: var[int | None] = max_lines\n

Maximum number of lines in the log or None for no maximum.

"},{"location":"widgets/rich_log/#textual.widgets._rich_log.RichLog.min_width","title":"min_width class-attribute instance-attribute","text":"
min_width: var[int] = min_width\n

Minimum width of renderables.

"},{"location":"widgets/rich_log/#textual.widgets._rich_log.RichLog.wrap","title":"wrap class-attribute instance-attribute","text":"
wrap: var[bool] = wrap\n

Enable word wrapping.

"},{"location":"widgets/rich_log/#textual.widgets._rich_log.RichLog.clear","title":"clear method","text":"
def clear(self):\n

Clear the text log.

Returns Type Description Self

The RichLog instance.

"},{"location":"widgets/rich_log/#textual.widgets._rich_log.RichLog.write","title":"write method","text":"
def write(\nself,\ncontent,\nwidth=None,\nexpand=False,\nshrink=True,\nscroll_end=None,\n):\n

Write text or a rich renderable.

Parameters Name Type Description Default content RenderableType | object

Rich renderable (or text).

required width int | None

Width to render or None to use optimal width.

None expand bool

Enable expand to widget width, or False to use width.

False shrink bool

Enable shrinking of content to fit width.

True scroll_end bool | None

Enable automatic scroll to end, or None to use self.auto_scroll.

None Returns Type Description Self

The RichLog instance.

"},{"location":"widgets/rule/","title":"Rule","text":"

A rule widget to separate content, similar to a <hr> HTML tag.

  • Focusable
  • Container
"},{"location":"widgets/rule/#examples","title":"Examples","text":""},{"location":"widgets/rule/#horizontal-rule","title":"Horizontal Rule","text":"

The default orientation of a rule is horizontal.

The example below shows horizontal rules with all the available line styles.

Outputhorizontal_rules.pyhorizontal_rules.tcss

HorizontalRulesApp \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0solid\u00a0(default)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0heavy\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0thick\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0dashed\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0double\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0ascii\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 ----------------------------------------------------------------

from textual.app import App, ComposeResult\nfrom textual.containers import Vertical\nfrom textual.widgets import Label, Rule\nclass HorizontalRulesApp(App):\nCSS_PATH = \"horizontal_rules.tcss\"\ndef compose(self) -> ComposeResult:\nwith Vertical():\nyield Label(\"solid (default)\")\nyield Rule()\nyield Label(\"heavy\")\nyield Rule(line_style=\"heavy\")\nyield Label(\"thick\")\nyield Rule(line_style=\"thick\")\nyield Label(\"dashed\")\nyield Rule(line_style=\"dashed\")\nyield Label(\"double\")\nyield Rule(line_style=\"double\")\nyield Label(\"ascii\")\nyield Rule(line_style=\"ascii\")\nif __name__ == \"__main__\":\napp = HorizontalRulesApp()\napp.run()\n
Screen {\nalign: center middle;\n}\nVertical {\nheight: auto;\nwidth: 80%;\n}\nLabel {\nwidth: 100%;\ntext-align: center;\n}\n
"},{"location":"widgets/rule/#vertical-rule","title":"Vertical Rule","text":"

The example below shows vertical rules with all the available line styles.

Outputvertical_rules.pyvertical_rules.tcss

VerticalRulesApp solid\u00a0\u2502heavy\u00a0\u2503thick\u00a0\u2588dashed\u254fdouble\u2551ascii\u00a0| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551|

from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.widgets import Label, Rule\nclass VerticalRulesApp(App):\nCSS_PATH = \"vertical_rules.tcss\"\ndef compose(self) -> ComposeResult:\nwith Horizontal():\nyield Label(\"solid\")\nyield Rule(orientation=\"vertical\")\nyield Label(\"heavy\")\nyield Rule(orientation=\"vertical\", line_style=\"heavy\")\nyield Label(\"thick\")\nyield Rule(orientation=\"vertical\", line_style=\"thick\")\nyield Label(\"dashed\")\nyield Rule(orientation=\"vertical\", line_style=\"dashed\")\nyield Label(\"double\")\nyield Rule(orientation=\"vertical\", line_style=\"double\")\nyield Label(\"ascii\")\nyield Rule(orientation=\"vertical\", line_style=\"ascii\")\nif __name__ == \"__main__\":\napp = VerticalRulesApp()\napp.run()\n
Screen {\nalign: center middle;\n}\nHorizontal {\nwidth: auto;\nheight: 80%;\n}\nLabel {\nwidth: 6;\nheight: 100%;\ntext-align: center;\n}\n
"},{"location":"widgets/rule/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description orientation RuleOrientation \"horizontal\" The orientation of the rule. line_style LineStyle \"solid\" The line style of the rule."},{"location":"widgets/rule/#messages","title":"Messages","text":"

This widget sends no messages.

"},{"location":"widgets/rule/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/rule/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/rule/#textual.widgets.Rule","title":"textual.widgets.Rule class","text":"
def __init__(\nself,\norientation=\"horizontal\",\nline_style=\"solid\",\n*,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: Widget

A rule widget to separate content, similar to a <hr> HTML tag.

Parameters Name Type Description Default orientation RuleOrientation

The orientation of the rule.

'horizontal' line_style LineStyle

The line style of the rule.

'solid' name str | None

The name of the widget.

None id str | None

The ID of the widget in the DOM.

None classes str | None

The CSS classes of the widget.

None disabled bool

Whether the widget is disabled or not.

False"},{"location":"widgets/rule/#textual.widgets._rule.Rule.line_style","title":"line_style class-attribute instance-attribute","text":"
line_style: Reactive[LineStyle] = line_style\n

The line style of the rule.

"},{"location":"widgets/rule/#textual.widgets._rule.Rule.orientation","title":"orientation class-attribute instance-attribute","text":"
orientation: Reactive[RuleOrientation] = orientation\n

The orientation of the rule.

"},{"location":"widgets/rule/#textual.widgets._rule.Rule.horizontal","title":"horizontal classmethod","text":"
def horizontal(\ncls,\nline_style=\"solid\",\nname=None,\nid=None,\nclasses=None,\ndisabled=False,\n):\n

Utility constructor for creating a horizontal rule.

Parameters Name Type Description Default line_style LineStyle

The line style of the rule.

'solid' name str | None

The name of the widget.

None id str | None

The ID of the widget in the DOM.

None classes str | None

The CSS classes of the widget.

None disabled bool

Whether the widget is disabled or not.

False Returns Type Description Rule

A rule widget with horizontal orientation.

"},{"location":"widgets/rule/#textual.widgets._rule.Rule.vertical","title":"vertical classmethod","text":"
def vertical(\ncls,\nline_style=\"solid\",\nname=None,\nid=None,\nclasses=None,\ndisabled=False,\n):\n

Utility constructor for creating a vertical rule.

Parameters Name Type Description Default line_style LineStyle

The line style of the rule.

'solid' name str | None

The name of the widget.

None id str | None

The ID of the widget in the DOM.

None classes str | None

The CSS classes of the widget.

None disabled bool

Whether the widget is disabled or not.

False Returns Type Description Rule

A rule widget with vertical orientation.

"},{"location":"widgets/rule/#textual.widgets.rule","title":"textual.widgets.rule","text":""},{"location":"widgets/rule/#textual.widgets.rule.LineStyle","title":"LineStyle module-attribute","text":"
LineStyle = Literal[\n\"ascii\",\n\"blank\",\n\"dashed\",\n\"double\",\n\"heavy\",\n\"hidden\",\n\"none\",\n\"solid\",\n\"thick\",\n]\n

The valid line styles of the rule widget.

"},{"location":"widgets/rule/#textual.widgets.rule.RuleOrientation","title":"RuleOrientation module-attribute","text":"
RuleOrientation = Literal['horizontal', 'vertical']\n

The valid orientations of the rule widget.

"},{"location":"widgets/rule/#textual.widgets.rule.InvalidLineStyle","title":"InvalidLineStyle class","text":"

Bases: Exception

Exception raised for an invalid rule line style.

"},{"location":"widgets/rule/#textual.widgets.rule.InvalidRuleOrientation","title":"InvalidRuleOrientation class","text":"

Bases: Exception

Exception raised for an invalid rule orientation.

"},{"location":"widgets/select/","title":"Select","text":"

Added in version 0.24.0

A Select widget is a compact control to allow the user to select between a number of possible options.

  • Focusable
  • Container

The options in a select control may be passed in to the constructor or set later with set_options. Options should be given as a sequence of tuples consisting of two values: the first is the string (or Rich Renderable) to display in the control and list of options, the second is the value of option.

The value of the currently selected option is stored in the value attribute of the widget, and the value attribute of the Changed message.

"},{"location":"widgets/select/#typing","title":"Typing","text":"

The Select control is a typing Generic which allows you to set the type of the option values. For instance, if the data type for your values is an integer, you would type the widget as follows:

options = [(\"First\", 1), (\"Second\", 2)]\nmy_select: Select[int] =  Select(options)\n

Note

Typing is entirely optional.

If you aren't familiar with typing or don't want to worry about it right now, feel free to ignore it.

"},{"location":"widgets/select/#example","title":"Example","text":"

The following example presents a Select with a number of options.

OutputOutput (expanded)select_widget.pyselect.tcss

SelectApp \u2b58SelectApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aSelect\u25bc\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

SelectApp \u2b58SelectApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aSelect\u25b2\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aSelect\u258e \u258aI\u00a0must\u00a0not\u00a0fear.\u258e \u258aFear\u00a0is\u00a0the\u00a0mind-killer.\u258e \u258aFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0\u258e \u258aobliteration.\u258e \u258aI\u00a0will\u00a0face\u00a0my\u00a0fear.\u258e \u258aI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

from textual import on\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Header, Select\nLINES = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\"\"\".splitlines()\nclass SelectApp(App):\nCSS_PATH = \"select.tcss\"\ndef compose(self) -> ComposeResult:\nyield Header()\nyield Select((line, line) for line in LINES)\n@on(Select.Changed)\ndef select_changed(self, event: Select.Changed) -> None:\nself.title = str(event.value)\nif __name__ == \"__main__\":\napp = SelectApp()\napp.run()\n
Screen {\nalign: center top;\n}\nSelect {\nwidth: 60;\nmargin: 2;\n}\n
"},{"location":"widgets/select/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description expanded bool False True to expand the options overlay. value SelectType | None None Current value of the Select."},{"location":"widgets/select/#messages","title":"Messages","text":"
  • Select.Changed
"},{"location":"widgets/select/#bindings","title":"Bindings","text":"

The Select widget defines the following bindings:

Key(s) Description enter,down,space,up Activate the overlay"},{"location":"widgets/select/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/select/#textual.widgets.Select","title":"textual.widgets.Select class","text":"
def __init__(\nself,\noptions,\n*,\nprompt=\"Select\",\nallow_blank=True,\nvalue=None,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: Generic[SelectType], Vertical

Widget to select from a list of possible options.

A Select displays the current selection. When activated with Enter the widget displays an overlay with a list of all possible options.

Parameters Name Type Description Default options Iterable[tuple[str, SelectType]]

Options to select from.

required prompt str

Text to show in the control when no option is select.

'Select' allow_blank bool

Allow the selection of a blank option.

True value SelectType | None

Initial value (should be one of the values in options).

None name str | None

The name of the select control.

None id str | None

The ID of the control the DOM.

None classes str | None

The CSS classes of the control.

None disabled bool

Whether the control is disabled or not.

False"},{"location":"widgets/select/#textual.widgets._select.Select.BINDINGS","title":"BINDINGS class-attribute instance-attribute","text":"
BINDINGS = [('enter,down,space,up', 'show_overlay')]\n
Key(s) Description enter,down,space,up Activate the overlay"},{"location":"widgets/select/#textual.widgets._select.Select.expanded","title":"expanded class-attribute instance-attribute","text":"
expanded: var[bool] = var(False, init=False)\n

True to show the overlay, otherwise False.

"},{"location":"widgets/select/#textual.widgets._select.Select.prompt","title":"prompt class-attribute instance-attribute","text":"
prompt: var[str] = prompt\n

The prompt to show when no value is selected.

"},{"location":"widgets/select/#textual.widgets._select.Select.value","title":"value class-attribute instance-attribute","text":"
value: var[SelectType | None] = var[Optional[SelectType]](\nNone\n)\n

The value of the select.

"},{"location":"widgets/select/#textual.widgets._select.Select.Changed","title":"Changed class","text":"
def __init__(self, select, value):\n

Bases: Message

Posted when the select value was changed.

This message can be handled using a on_select_changed method.

"},{"location":"widgets/select/#textual.widgets._select.Select.Changed.control","title":"control property","text":"
control: Select\n

The Select that sent the message.

"},{"location":"widgets/select/#textual.widgets._select.Select.Changed.select","title":"select instance-attribute","text":"
select = select\n

The select widget.

"},{"location":"widgets/select/#textual.widgets._select.Select.Changed.value","title":"value instance-attribute","text":"
value = value\n

The value of the Select when it changed.

"},{"location":"widgets/select/#textual.widgets._select.Select.action_show_overlay","title":"action_show_overlay method","text":"
def action_show_overlay(self):\n

Show the overlay.

"},{"location":"widgets/select/#textual.widgets._select.Select.set_options","title":"set_options method","text":"
def set_options(self, options):\n

Set the options for the Select.

Parameters Name Type Description Default options Iterable[tuple[RenderableType, SelectType]]

An iterable of tuples containing (STRING, VALUE).

required"},{"location":"widgets/selection_list/","title":"SelectionList","text":"

Added in version 0.27.0

A widget for showing a vertical list of selectable options.

  • Focusable
  • Container
"},{"location":"widgets/selection_list/#typing","title":"Typing","text":"

The SelectionList control is a Generic, which allows you to set the type of the selection values. For instance, if the data type for your values is an integer, you would type the widget as follows:

selections = [(\"First\", 1), (\"Second\", 2)]\nmy_selection_list: SelectionList[int] =  SelectionList(*selections)\n

Note

Typing is entirely optional.

If you aren't familiar with typing or don't want to worry about it right now, feel free to ignore it.

"},{"location":"widgets/selection_list/#examples","title":"Examples","text":"

A selection list is designed to be built up of single-line prompts (which can be Rich renderables) and an associated unique value.

"},{"location":"widgets/selection_list/#selections-as-tuples","title":"Selections as tuples","text":"

A selection list can be built with tuples, either of two or three values in length. Each tuple must contain a prompt and a value, and it can also optionally contain a flag for the initial selected state of the option.

Outputselection_list_tuples.pyselection_list.tcss

SelectionListApp \u2b58SelectionListApp \u250c\u2500\u00a0Shall\u00a0we\u00a0play\u00a0some\u00a0games?\u00a0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502 \u2502\u2590X\u258cFalken's\u00a0Maze\u2502 \u2502\u2590X\u258cBlack\u00a0Jack\u2502 \u2502\u2590X\u258cGin\u00a0Rummy\u2502 \u2502\u2590X\u258cHearts\u2502 \u2502\u2590X\u258cBridge\u2502 \u2502\u2590X\u258cCheckers\u2502 \u2502\u2590X\u258cChess\u2502 \u2502\u2590X\u258cPoker\u2502 \u2502\u2590X\u258cFighter\u00a0Combat\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

from textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Header, SelectionList\nclass SelectionListApp(App[None]):\nCSS_PATH = \"selection_list.tcss\"\ndef compose(self) -> ComposeResult:\nyield Header()\nyield SelectionList[int](  # (1)!\n(\"Falken's Maze\", 0, True),\n(\"Black Jack\", 1),\n(\"Gin Rummy\", 2),\n(\"Hearts\", 3),\n(\"Bridge\", 4),\n(\"Checkers\", 5),\n(\"Chess\", 6, True),\n(\"Poker\", 7),\n(\"Fighter Combat\", 8, True),\n)\nyield Footer()\ndef on_mount(self) -> None:\nself.query_one(SelectionList).border_title = \"Shall we play some games?\"\nif __name__ == \"__main__\":\nSelectionListApp().run()\n
  1. Note that the SelectionList is typed as int, for the type of the values.
Screen {\nalign: center middle;\n}\nSelectionList {\npadding: 1;\nborder: solid $accent;\nwidth: 80%;\nheight: 80%;\n}\n
"},{"location":"widgets/selection_list/#selections-as-selection-objects","title":"Selections as Selection objects","text":"

Alternatively, selections can be passed in as Selections:

Outputselection_list_selections.pyselection_list.tcss

SelectionListApp \u2b58SelectionListApp \u250c\u2500\u00a0Shall\u00a0we\u00a0play\u00a0some\u00a0games?\u00a0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502 \u2502\u2590X\u258cFalken's\u00a0Maze\u2502 \u2502\u2590X\u258cBlack\u00a0Jack\u2502 \u2502\u2590X\u258cGin\u00a0Rummy\u2502 \u2502\u2590X\u258cHearts\u2502 \u2502\u2590X\u258cBridge\u2502 \u2502\u2590X\u258cCheckers\u2502 \u2502\u2590X\u258cChess\u2502 \u2502\u2590X\u258cPoker\u2502 \u2502\u2590X\u258cFighter\u00a0Combat\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

from textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Header, SelectionList\nfrom textual.widgets.selection_list import Selection\nclass SelectionListApp(App[None]):\nCSS_PATH = \"selection_list.tcss\"\ndef compose(self) -> ComposeResult:\nyield Header()\nyield SelectionList[int](  # (1)!\nSelection(\"Falken's Maze\", 0, True),\nSelection(\"Black Jack\", 1),\nSelection(\"Gin Rummy\", 2),\nSelection(\"Hearts\", 3),\nSelection(\"Bridge\", 4),\nSelection(\"Checkers\", 5),\nSelection(\"Chess\", 6, True),\nSelection(\"Poker\", 7),\nSelection(\"Fighter Combat\", 8, True),\n)\nyield Footer()\ndef on_mount(self) -> None:\nself.query_one(SelectionList).border_title = \"Shall we play some games?\"\nif __name__ == \"__main__\":\nSelectionListApp().run()\n
  1. Note that the SelectionList is typed as int, for the type of the values.
Screen {\nalign: center middle;\n}\nSelectionList {\npadding: 1;\nborder: solid $accent;\nwidth: 80%;\nheight: 80%;\n}\n
"},{"location":"widgets/selection_list/#handling-changes-to-the-selections","title":"Handling changes to the selections","text":"

Most of the time, when using the SelectionList, you will want to know when the collection of selected items has changed; this is ideally done using the SelectedChanged message. Here is an example of using that message to update a Pretty with the collection of selected values:

Outputselection_list_selections.pyselection_list.tcss

SelectionListApp \u2b58SelectionListApp \u250c\u2500\u00a0Shall\u00a0we\u00a0play\u00a0some\u00a0games?\u00a0\u2500\u2500\u2510\u250c\u2500\u00a0Selected\u00a0games\u00a0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502[\u2502 \u2502\u2590X\u258cFalken's\u00a0Maze\u2502\u2502'secret_back_door',\u2502 \u2502\u2590X\u258cBlack\u00a0Jack\u2502\u2502'a_nice_game_of_chess',\u2502 \u2502\u2590X\u258cGin\u00a0Rummy\u2502\u2502'fighter_combat'\u2502 \u2502\u2590X\u258cHearts\u2502\u2502]\u2502 \u2502\u2590X\u258cBridge\u2502\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502\u2590X\u258cCheckers\u2502 \u2502\u2590X\u258cChess\u2502 \u2502\u2590X\u258cPoker\u2502 \u2502\u2590X\u258cFighter\u00a0Combat\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

from textual import on\nfrom textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.events import Mount\nfrom textual.widgets import Footer, Header, Pretty, SelectionList\nfrom textual.widgets.selection_list import Selection\nclass SelectionListApp(App[None]):\nCSS_PATH = \"selection_list_selected.tcss\"\ndef compose(self) -> ComposeResult:\nyield Header()\nwith Horizontal():\nyield SelectionList[str](  # (1)!\nSelection(\"Falken's Maze\", \"secret_back_door\", True),\nSelection(\"Black Jack\", \"black_jack\"),\nSelection(\"Gin Rummy\", \"gin_rummy\"),\nSelection(\"Hearts\", \"hearts\"),\nSelection(\"Bridge\", \"bridge\"),\nSelection(\"Checkers\", \"checkers\"),\nSelection(\"Chess\", \"a_nice_game_of_chess\", True),\nSelection(\"Poker\", \"poker\"),\nSelection(\"Fighter Combat\", \"fighter_combat\", True),\n)\nyield Pretty([])\nyield Footer()\ndef on_mount(self) -> None:\nself.query_one(SelectionList).border_title = \"Shall we play some games?\"\nself.query_one(Pretty).border_title = \"Selected games\"\n@on(Mount)\n@on(SelectionList.SelectedChanged)\ndef update_selected_view(self) -> None:\nself.query_one(Pretty).update(self.query_one(SelectionList).selected)\nif __name__ == \"__main__\":\nSelectionListApp().run()\n
  1. Note that the SelectionList is typed as str, for the type of the values.
Screen {\nalign: center middle;\n}\nHorizontal {\nwidth: 80%;\nheight: 80%;\n}\nSelectionList {\npadding: 1;\nborder: solid $accent;\nwidth: 1fr;\n}\nPretty {\nwidth: 1fr;\nborder: solid $accent;\n}\n
"},{"location":"widgets/selection_list/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description highlighted int | None None The index of the highlighted selection. None means nothing is highlighted."},{"location":"widgets/selection_list/#messages","title":"Messages","text":"

The following messages will be posted as the user interacts with the list:

  • SelectionList.SelectionHighlighted
  • SelectionList.SelectionToggled

The following message will be posted if the content of selected changes, either by user interaction or by API calls:

  • SelectionList.SelectedChanged
"},{"location":"widgets/selection_list/#bindings","title":"Bindings","text":"

The selection list widget defines the following bindings:

Key(s) Description space Toggle the state of the highlighted selection.

It inherits from OptionList and so also inherits the following bindings:

Key(s) Description down Move the highlight down. end Move the highlight to the last option. enter Select the current option. home Move the highlight to the first option. pagedown Move the highlight down a page of options. pageup Move the highlight up a page of options. up Move the highlight up."},{"location":"widgets/selection_list/#component-classes","title":"Component Classes","text":"

The selection list provides the following component classes:

Class Description selection-list--button Target the default button style. selection-list--button-selected Target a selected button style. selection-list--button-highlighted Target a highlighted button style. selection-list--button-selected-highlighted Target a highlighted selected button style.

It inherits from OptionList and so also makes use of the following component classes:

Class Description option-list--option-disabled Target disabled options. option-list--option-highlighted Target the highlighted option. option-list--option-highlighted-disabled Target a disabled option that is also highlighted. option-list--option-hover Target an option that has the mouse over it. option-list--option-hover-disabled Target a disabled option that has the mouse over it. option-list--option-hover-highlighted Target a highlighted option that has the mouse over it. option-list--option-hover-highlighted-disabled Target a disabled highlighted option that has the mouse over it. option-list--separator Target the separators."},{"location":"widgets/selection_list/#textual.widgets.SelectionList","title":"textual.widgets.SelectionList class","text":"
def __init__(\nself,\n*selections,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: Generic[SelectionType], OptionList

A vertical selection list that allows making multiple selections.

Parameters Name Type Description Default *selections Selection | tuple[TextType, SelectionType] | tuple[TextType, SelectionType, bool]

The content for the selection list.

() name str | None

The name of the selection list.

None id str | None

The ID of the selection list in the DOM.

None classes str | None

The CSS classes of the selection list.

None disabled bool

Whether the selection list is disabled or not.

False"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.BINDINGS","title":"BINDINGS class-attribute instance-attribute","text":"
BINDINGS = [Binding('space', 'select')]\n
Key(s) Description space Toggle the state of the highlighted selection."},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
COMPONENT_CLASSES: set[str] = {\n\"selection-list--button\",\n\"selection-list--button-selected\",\n\"selection-list--button-highlighted\",\n\"selection-list--button-selected-highlighted\",\n}\n
Class Description selection-list--button Target the default button style. selection-list--button-selected Target a selected button style. selection-list--button-highlighted Target a highlighted button style. selection-list--button-selected-highlighted Target a highlighted selected button style."},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.selected","title":"selected property","text":"
selected: list[SelectionType]\n

The selected values.

This is a list of all of the values associated with selections in the list that are currently in the selected state.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectedChanged","title":"SelectedChanged class","text":"

Bases: Generic[MessageSelectionType], Message

Message sent when the collection of selected values changes.

This message is sent when any change to the collection of selected values takes place; either by user interaction or by API calls.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectedChanged.control","title":"control property","text":"
control: SelectionList[MessageSelectionType]\n

An alias for selection_list.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectedChanged.selection_list","title":"selection_list instance-attribute","text":"
selection_list: SelectionList[MessageSelectionType]\n

The SelectionList that sent the message.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectionHighlighted","title":"SelectionHighlighted class","text":"

Bases: SelectionMessage

Message sent when a selection is highlighted.

Can be handled using on_selection_list_selection_highlighted in a subclass of SelectionList or in a parent node in the DOM.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectionMessage","title":"SelectionMessage class","text":"
def __init__(self, selection_list, index):\n

Bases: Generic[MessageSelectionType], Message

Base class for all selection messages.

Parameters Name Type Description Default selection_list SelectionList

The selection list that owns the selection.

required index int

The index of the selection that the message relates to.

required"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectionMessage.control","title":"control property","text":"
control: OptionList\n

The selection list that sent the message.

This is an alias for SelectionMessage.selection_list and is used by the on decorator.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectionMessage.selection","title":"selection instance-attribute","text":"
selection: Selection[\nMessageSelectionType\n] = selection_list.get_option_at_index(index)\n

The highlighted selection.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectionMessage.selection_index","title":"selection_index instance-attribute","text":"
selection_index: int = index\n

The index of the selection that the message relates to.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectionMessage.selection_list","title":"selection_list instance-attribute","text":"
selection_list: SelectionList[\nMessageSelectionType\n] = selection_list\n

The selection list that sent the message.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectionToggled","title":"SelectionToggled class","text":"

Bases: SelectionMessage

Message sent when a selection is toggled.

Can be handled using on_selection_list_selection_toggled in a subclass of SelectionList or in a parent node in the DOM.

Note

This message is only sent if the selection is toggled by user interaction. See SelectedChanged for a message sent when any change (selected or deselected, either by user interaction or by API calls) is made to the selected values.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.add_option","title":"add_option method","text":"
def add_option(self, item=None):\n

Add a new selection option to the end of the list.

Parameters Name Type Description Default item NewOptionListContent | Selection | tuple[TextType, SelectionType] | tuple[TextType, SelectionType, bool]

The new item to add.

None Returns Type Description Self

The SelectionList instance.

Raises Type Description DuplicateID

If there is an attempt to use a duplicate ID.

SelectionError

If the selection option is of the wrong form.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.add_options","title":"add_options method","text":"
def add_options(self, items):\n

Add new selection options to the end of the list.

Parameters Name Type Description Default items Iterable[NewOptionListContent | Selection | tuple[TextType, SelectionType] | tuple[TextType, SelectionType, bool]]

The new items to add.

required Returns Type Description Self

The SelectionList instance.

Raises Type Description DuplicateID

If there is an attempt to use a duplicate ID.

SelectionError

If one of the selection options is of the wrong form.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.clear_options","title":"clear_options method","text":"
def clear_options(self):\n

Clear the content of the selection list.

Returns Type Description Self

The SelectionList instance.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.deselect","title":"deselect method","text":"
def deselect(self, selection):\n

Mark the given selection as not selected.

Parameters Name Type Description Default selection Selection[SelectionType] | SelectionType

The selection to mark as not selected.

required Returns Type Description Self

The SelectionList instance.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.deselect_all","title":"deselect_all method","text":"
def deselect_all(self):\n

Deselect all items.

Returns Type Description Self

The SelectionList instance.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.get_option","title":"get_option method","text":"
def get_option(self, option_id):\n

Get the selection option with the given ID.

Parameters Name Type Description Default option_id str

The ID of the selection option to get.

required Returns Type Description Selection[SelectionType]

The selection option with the ID.

Raises Type Description OptionDoesNotExist

If no selection option has the given ID.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.get_option_at_index","title":"get_option_at_index method","text":"
def get_option_at_index(self, index):\n

Get the selection option at the given index.

Parameters Name Type Description Default index int

The index of the selection option to get.

required Returns Type Description Selection[SelectionType]

The selection option at that index.

Raises Type Description OptionDoesNotExist

If there is no selection option with the index.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.select","title":"select method","text":"
def select(self, selection):\n

Mark the given selection as selected.

Parameters Name Type Description Default selection Selection[SelectionType] | SelectionType

The selection to mark as selected.

required Returns Type Description Self

The SelectionList instance.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.select_all","title":"select_all method","text":"
def select_all(self):\n

Select all items.

Returns Type Description Self

The SelectionList instance.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.toggle","title":"toggle method","text":"
def toggle(self, selection):\n

Toggle the selected state of the given selection.

Parameters Name Type Description Default selection Selection[SelectionType] | SelectionType

The selection to toggle.

required Returns Type Description Self

The SelectionList instance.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.toggle_all","title":"toggle_all method","text":"
def toggle_all(self):\n

Toggle all items.

Returns Type Description Self

The SelectionList instance.

"},{"location":"widgets/selection_list/#textual.widgets.selection_list.MessageSelectionType","title":"MessageSelectionType module-attribute","text":"
MessageSelectionType = TypeVar('MessageSelectionType')\n

The type for the value of a Selection in a SelectionList message.

"},{"location":"widgets/selection_list/#textual.widgets.selection_list.SelectionType","title":"SelectionType module-attribute","text":"
SelectionType = TypeVar('SelectionType')\n

The type for the value of a Selection in a SelectionList

"},{"location":"widgets/selection_list/#textual.widgets.selection_list.Selection","title":"Selection class","text":"
def __init__(\nself,\nprompt,\nvalue,\ninitial_state=False,\nid=None,\ndisabled=False,\n):\n

Bases: Generic[SelectionType], Option

A selection for a SelectionList.

Parameters Name Type Description Default prompt TextType

The prompt for the selection.

required value SelectionType

The value for the selection.

required initial_state bool

The initial selected state of the selection.

False id str | None

The optional ID for the selection.

None disabled bool

The initial enabled/disabled state. Enabled by default.

False"},{"location":"widgets/selection_list/#textual.widgets._selection_list.Selection.initial_state","title":"initial_state property","text":"
initial_state: bool\n

The initial selected state for the selection.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.Selection.value","title":"value property","text":"
value: SelectionType\n

The value for this selection.

"},{"location":"widgets/selection_list/#textual.widgets.selection_list.SelectionError","title":"SelectionError class","text":"

Bases: TypeError

Type of an error raised if a selection is badly-formed.

"},{"location":"widgets/sparkline/","title":"Sparkline","text":"

Added in version 0.27.0

A widget that is used to visually represent numerical data.

  • Focusable
  • Container
"},{"location":"widgets/sparkline/#examples","title":"Examples","text":""},{"location":"widgets/sparkline/#basic-example","title":"Basic example","text":"

The example below illustrates the relationship between the data, its length, the width of the sparkline, and the number of bars displayed.

Tip

The sparkline data is split into equally-sized chunks. Each chunk is represented by a bar and the width of the sparkline dictates how many bars there are.

Outputsparkline_basic.pysparkline_basic.tcss

SparklineBasicApp \u2582\u2584\u2588

from textual.app import App, ComposeResult\nfrom textual.widgets import Sparkline\ndata = [1, 2, 2, 1, 1, 4, 3, 1, 1, 8, 8, 2]  # (1)!\nclass SparklineBasicApp(App[None]):\nCSS_PATH = \"sparkline_basic.tcss\"\ndef compose(self) -> ComposeResult:\nyield Sparkline(  # (2)!\ndata,  # (3)!\nsummary_function=max,  # (4)!\n)\napp = SparklineBasicApp()\nif __name__ == \"__main__\":\napp.run()\n
  1. We have 12 data points.
  2. This sparkline will have its width set to 3 via CSS.
  3. The data (12 numbers) will be split across 3 bars, so 4 data points are associated with each bar.
  4. Each bar will represent its largest value. The largest value of each chunk is 2, 4, and 8, respectively. That explains why the first bar is half the height of the second and the second bar is half the height of the third.
Screen {\nalign: center middle;\n}\nSparkline {\nwidth: 3;  /* (1)! */\nmargin: 2;\n}\n
  1. By setting the width to 3 we get three buckets.
"},{"location":"widgets/sparkline/#different-summary-functions","title":"Different summary functions","text":"

The example below shows a sparkline widget with different summary functions. The summary function is what determines the height of each bar.

Outputsparkline.pysparkline.tcss

SparklineSummaryFunctionApp \u2582\u2584\u2582\u2584\u2583\u2583\u2586\u2585\u2583\u2582\u2583\u2582\u2583\u2582\u2584\u2587\u2583\u2583\u2587\u2585\u2584\u2583\u2584\u2584\u2583\u2582\u2583\u2582\u2583\u2584\u2584\u2588\u2586\u2582\u2583\u2583\u2585\u2583\u2583\u2584\u2583\u2587\u2583\u2583\u2583\u2584\u2584\u2586\u2583\u2583\u2585\u2582\u2585\u2583\u2584\u2583\u2583\u2584\u2583\u2585\u2586\u2582\u2582\u2583\u2586\u2582\u2583\u2584\u2585\u2584\u2583\u2584\u2584\u2581\u2583\u2582 \u2581\u2582\u2581\u2582\u2581\u2581\u2582\u2582\u2581\u2581\u2581\u2581\u2582\u2581\u2581\u2582\u2581\u2581\u2582\u2582\u2582\u2582\u2582\u2582\u2581\u2581\u2581\u2581\u2581\u2582\u2581\u2582\u2582\u2581\u2581\u2581\u2582\u2581\u2581\u2582\u2581\u2582\u2581\u2581\u2581\u2581\u2582\u2582\u2581\u2581\u2581\u2581\u2582\u2581\u2581\u2582\u2581\u2582\u2581\u2581\u2582\u2581\u2581\u2581\u2581\u2581\u2581\u2582\u2582\u2582\u2581\u2582\u2581\u2581\u2581\u2581 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

import random\nfrom statistics import mean\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Sparkline\nrandom.seed(73)\ndata = [random.expovariate(1 / 3) for _ in range(1000)]\nclass SparklineSummaryFunctionApp(App[None]):\nCSS_PATH = \"sparkline.tcss\"\ndef compose(self) -> ComposeResult:\nyield Sparkline(data, summary_function=max)  # (1)!\nyield Sparkline(data, summary_function=mean)  # (2)!\nyield Sparkline(data, summary_function=min)  # (3)!\napp = SparklineSummaryFunctionApp()\nif __name__ == \"__main__\":\napp.run()\n
  1. Each bar will show the largest value of that bucket.
  2. Each bar will show the mean value of that bucket.
  3. Each bar will show the smaller value of that bucket.
Sparkline {\nwidth: 100%;\nmargin: 2;\n}\n
"},{"location":"widgets/sparkline/#changing-the-colors","title":"Changing the colors","text":"

The example below shows how to use component classes to change the colors of the sparkline.

Outputsparkline_colors.pysparkline_colors.tcss

SparklineColorsApp \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582 \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582 \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582 \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582 \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582 \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582 \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582 \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582 \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582 \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582

from math import sin\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Sparkline\nclass SparklineColorsApp(App[None]):\nCSS_PATH = \"sparkline_colors.tcss\"\ndef compose(self) -> ComposeResult:\nnums = [abs(sin(x / 3.14)) for x in range(0, 360 * 6, 20)]\nyield Sparkline(nums, summary_function=max, id=\"fst\")\nyield Sparkline(nums, summary_function=max, id=\"snd\")\nyield Sparkline(nums, summary_function=max, id=\"trd\")\nyield Sparkline(nums, summary_function=max, id=\"frt\")\nyield Sparkline(nums, summary_function=max, id=\"fft\")\nyield Sparkline(nums, summary_function=max, id=\"sxt\")\nyield Sparkline(nums, summary_function=max, id=\"svt\")\nyield Sparkline(nums, summary_function=max, id=\"egt\")\nyield Sparkline(nums, summary_function=max, id=\"nnt\")\nyield Sparkline(nums, summary_function=max, id=\"tnt\")\napp = SparklineColorsApp()\nif __name__ == \"__main__\":\napp.run()\n
Sparkline {\nwidth: 100%;\nmargin: 1;\n}\n#fst > .sparkline--max-color {\ncolor: $success;\n}\n#fst > .sparkline--min-color {\ncolor: $warning;\n}\n#snd > .sparkline--max-color {\ncolor: $warning;\n}\n#snd > .sparkline--min-color {\ncolor: $success;\n}\n#trd > .sparkline--max-color {\ncolor: $error;\n}\n#trd > .sparkline--min-color {\ncolor: $warning;\n}\n#frt > .sparkline--max-color {\ncolor: $warning;\n}\n#frt > .sparkline--min-color {\ncolor: $error;\n}\n#fft > .sparkline--max-color {\ncolor: $accent;\n}\n#fft > .sparkline--min-color {\ncolor: $accent 30%;\n}\n#sxt > .sparkline--max-color {\ncolor: $accent 30%;\n}\n#sxt > .sparkline--min-color {\ncolor: $accent;\n}\n#svt > .sparkline--max-color {\ncolor: $error;\n}\n#svt > .sparkline--min-color {\ncolor: $error 30%;\n}\n#egt > .sparkline--max-color {\ncolor: $error 30%;\n}\n#egt > .sparkline--min-color {\ncolor: $error;\n}\n#nnt > .sparkline--max-color {\ncolor: $success;\n}\n#nnt > .sparkline--min-color {\ncolor: $success 30%;\n}\n#tnt > .sparkline--max-color {\ncolor: $success 30%;\n}\n#tnt > .sparkline--min-color {\ncolor: $success;\n}\n
"},{"location":"widgets/sparkline/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description data Sequence[float] | None None The data represented by the sparkline. summary_function Callable[[Sequence[float]], float] max The function that computes the height of each bar."},{"location":"widgets/sparkline/#messages","title":"Messages","text":"

This widget posts no messages.

"},{"location":"widgets/sparkline/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/sparkline/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/sparkline/#textual.widgets.Sparkline","title":"textual.widgets.Sparkline class","text":"
def __init__(\nself,\ndata=None,\n*,\nsummary_function=None,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: Widget

A sparkline widget to display numerical data.

Parameters Name Type Description Default data Sequence[float] | None

The initial data to populate the sparkline with.

None summary_function Callable[[Sequence[float]], float] | None

Summarises bar values into a single value used to represent each bar.

None name str | None

The name of the widget.

None id str | None

The ID of the widget in the DOM.

None classes str | None

The CSS classes for the widget.

None disabled bool

Whether the widget is disabled or not.

False"},{"location":"widgets/sparkline/#textual.widgets._sparkline.Sparkline.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
COMPONENT_CLASSES: set[str] = {\n\"sparkline--max-color\",\n\"sparkline--min-color\",\n}\n

Use these component classes to define the two colors that the sparkline interpolates to represent its numerical data.

Note

These two component classes are used exclusively for the color of the sparkline widget. Setting any style other than color will have no effect.

Class Description sparkline--max-color The color used for the larger values in the data. sparkline--min-color The colour used for the smaller values in the data."},{"location":"widgets/sparkline/#textual.widgets._sparkline.Sparkline.data","title":"data class-attribute instance-attribute","text":"
data = data\n

The data that populates the sparkline.

"},{"location":"widgets/sparkline/#textual.widgets._sparkline.Sparkline.summary_function","title":"summary_function class-attribute instance-attribute","text":"
summary_function = reactive[\nCallable[[Sequence[float]], float]\n](_max_factory)\n

The function that computes the value that represents each bar.

"},{"location":"widgets/static/","title":"Static","text":"

A widget which displays static content. Can be used for Rich renderables and can also be the base for other types of widgets.

  • Focusable
  • Container
"},{"location":"widgets/static/#example","title":"Example","text":"

The example below shows how you can use a Static widget as a simple text label (but see Label as a way of displaying text).

Outputstatic.py

StaticApp Hello,\u00a0world!

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass StaticApp(App):\ndef compose(self) -> ComposeResult:\nyield Static(\"Hello, world!\")\nif __name__ == \"__main__\":\napp = StaticApp()\napp.run()\n
"},{"location":"widgets/static/#reactive-attributes","title":"Reactive Attributes","text":"

This widget has no reactive attributes.

"},{"location":"widgets/static/#messages","title":"Messages","text":"

This widget posts no messages.

"},{"location":"widgets/static/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/static/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/static/#see-also","title":"See Also","text":"
  • Label
  • Pretty
"},{"location":"widgets/static/#textual.widgets.Static","title":"textual.widgets.Static class","text":"
def __init__(\nself,\nrenderable=\"\",\n*,\nexpand=False,\nshrink=False,\nmarkup=True,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: Widget

A widget to display simple static content, or use as a base class for more complex widgets.

Parameters Name Type Description Default renderable RenderableType

A Rich renderable, or string containing console markup.

'' expand bool

Expand content if required to fill container.

False shrink bool

Shrink content if required to fill container.

False markup bool

True if markup should be parsed and rendered.

True name str | None

Name of widget.

None id str | None

ID of Widget.

None classes str | None

Space separated list of class names.

None disabled bool

Whether the static is disabled or not.

False"},{"location":"widgets/static/#textual.widgets._static.Static.update","title":"update method","text":"
def update(self, renderable=''):\n

Update the widget's content area with new text or Rich renderable.

Parameters Name Type Description Default renderable RenderableType

A new rich renderable. Defaults to empty renderable;

''"},{"location":"widgets/switch/","title":"Switch","text":"

A simple switch widget which stores a boolean value.

  • Focusable
  • Container
"},{"location":"widgets/switch/#example","title":"Example","text":"

The example below shows switches in various states.

Outputswitch.pyswitch.tcss

SwitchApp Example\u00a0switches \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e off:\u00a0\u00a0\u00a0\u00a0\u00a0\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e on:\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e focused:\u00a0\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e custom:\u00a0\u00a0\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.widgets import Static, Switch\nclass SwitchApp(App):\ndef compose(self) -> ComposeResult:\nyield Static(\"[b]Example switches\\n\", classes=\"label\")\nyield Horizontal(\nStatic(\"off:     \", classes=\"label\"),\nSwitch(animate=False),\nclasses=\"container\",\n)\nyield Horizontal(\nStatic(\"on:      \", classes=\"label\"),\nSwitch(value=True),\nclasses=\"container\",\n)\nfocused_switch = Switch()\nfocused_switch.focus()\nyield Horizontal(\nStatic(\"focused: \", classes=\"label\"), focused_switch, classes=\"container\"\n)\nyield Horizontal(\nStatic(\"custom:  \", classes=\"label\"),\nSwitch(id=\"custom-design\"),\nclasses=\"container\",\n)\napp = SwitchApp(css_path=\"switch.tcss\")\nif __name__ == \"__main__\":\napp.run()\n
Screen {\nalign: center middle;\n}\n.container {\nheight: auto;\nwidth: auto;\n}\nSwitch {\nheight: auto;\nwidth: auto;\n}\n.label {\nheight: 3;\ncontent-align: center middle;\nwidth: auto;\n}\n#custom-design {\nbackground: darkslategrey;\n}\n#custom-design > .switch--slider {\ncolor: dodgerblue;\nbackground: darkslateblue;\n}\n
"},{"location":"widgets/switch/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description value bool False The value of the switch."},{"location":"widgets/switch/#messages","title":"Messages","text":"
  • Switch.Changed
"},{"location":"widgets/switch/#bindings","title":"Bindings","text":"

The switch widget defines the following bindings:

Key(s) Description enter,space Toggle the switch state."},{"location":"widgets/switch/#component-classes","title":"Component Classes","text":"

The switch widget provides the following component classes:

Class Description switch--slider Targets the slider of the switch."},{"location":"widgets/switch/#additional-notes","title":"Additional Notes","text":"
  • To remove the spacing around a Switch, set border: none; and padding: 0;.
"},{"location":"widgets/switch/#textual.widgets.Switch","title":"textual.widgets.Switch class","text":"
def __init__(\nself,\nvalue=False,\n*,\nanimate=True,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: Widget

A switch widget that represents a boolean value.

Can be toggled by clicking on it or through its bindings.

The switch widget also contains component classes that enable more customization.

Parameters Name Type Description Default value bool

The initial value of the switch.

False animate bool

True if the switch should animate when toggled.

True name str | None

The name of the switch.

None id str | None

The ID of the switch in the DOM.

None classes str | None

The CSS classes of the switch.

None disabled bool

Whether the switch is disabled or not.

False"},{"location":"widgets/switch/#textual.widgets._switch.Switch.BINDINGS","title":"BINDINGS class-attribute","text":"
BINDINGS: list[BindingType] = [\nBinding(\"enter,space\", \"toggle\", \"Toggle\", show=False)\n]\n
Key(s) Description enter,space Toggle the switch state."},{"location":"widgets/switch/#textual.widgets._switch.Switch.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
COMPONENT_CLASSES: set[str] = {'switch--slider'}\n
Class Description switch--slider Targets the slider of the switch."},{"location":"widgets/switch/#textual.widgets._switch.Switch.slider_pos","title":"slider_pos class-attribute instance-attribute","text":"
slider_pos = reactive(0.0)\n

The position of the slider.

"},{"location":"widgets/switch/#textual.widgets._switch.Switch.value","title":"value class-attribute instance-attribute","text":"
value = reactive(False, init=False)\n

The value of the switch; True for on and False for off.

"},{"location":"widgets/switch/#textual.widgets._switch.Switch.Changed","title":"Changed class","text":"
def __init__(self, switch, value):\n

Bases: Message

Posted when the status of the switch changes.

Can be handled using on_switch_changed in a subclass of Switch or in a parent widget in the DOM.

Attributes Name Type Description value bool

The value that the switch was changed to.

switch Switch

The Switch widget that was changed.

"},{"location":"widgets/switch/#textual.widgets._switch.Switch.Changed.control","title":"control property","text":"
control: Switch\n

Alias for self.switch.

"},{"location":"widgets/switch/#textual.widgets._switch.Switch.action_toggle","title":"action_toggle method","text":"
def action_toggle(self):\n

Toggle the state of the switch.

"},{"location":"widgets/switch/#textual.widgets._switch.Switch.toggle","title":"toggle method","text":"
def toggle(self):\n

Toggle the switch value.

As a result of the value changing, a Switch.Changed message will be posted.

Returns Type Description Self

The Switch instance.

"},{"location":"widgets/tabbed_content/","title":"TabbedContent","text":"

Added in version 0.16.0

Switch between mutually exclusive content panes via a row of tabs.

  • Focusable
  • Container

This widget combines the Tabs and ContentSwitcher widgets to create a convenient way of navigating content.

Only a single child of TabbedContent is visible at once. Each child has an associated tab which will make it visible and hide the others.

"},{"location":"widgets/tabbed_content/#composing","title":"Composing","text":"

There are two ways to provide the titles for the tab. You can pass them as positional arguments to the TabbedContent constructor:

def compose(self) -> ComposeResult:\nwith TabbedContent(\"Leto\", \"Jessica\", \"Paul\"):\nyield Markdown(LETO)\nyield Markdown(JESSICA)\nyield Markdown(PAUL)\n

Alternatively you can wrap the content in a TabPane widget, which takes the tab title as the first parameter:

def compose(self) -> ComposeResult:\nwith TabbedContent():\nwith TabPane(\"Leto\"):\nyield Markdown(LETO)\nwith TabPane(\"Jessica\"):\nyield Markdown(JESSICA)\nwith TabPane(\"Paul\"):\nyield Markdown(PAUL)\n
"},{"location":"widgets/tabbed_content/#switching-tabs","title":"Switching tabs","text":"

If you need to programmatically switch tabs, you should provide an id attribute to the TabPanes.

def compose(self) -> ComposeResult:\nwith TabbedContent():\nwith TabPane(\"Leto\", id=\"leto\"):\nyield Markdown(LETO)\nwith TabPane(\"Jessica\", id=\"jessica\"):\nyield Markdown(JESSICA)\nwith TabPane(\"Paul\", id=\"paul\"):\nyield Markdown(PAUL)\n

You can then switch tabs by setting the active reactive attribute:

# Switch to Jessica tab\nself.query_one(TabbedContent).active = \"jessica\"\n

Note

If you don't provide id attributes to the tab panes, they will be assigned sequentially starting at tab-1 (then tab-2 etc).

"},{"location":"widgets/tabbed_content/#initial-tab","title":"Initial tab","text":"

The first child of TabbedContent will be the initial active tab by default. You can pick a different initial tab by setting the initial argument to the id of the tab:

def compose(self) -> ComposeResult:\nwith TabbedContent(initial=\"jessica\"):\nwith TabPane(\"Leto\", id=\"leto\"):\nyield Markdown(LETO)\nwith TabPane(\"Jessica\", id=\"jessica\"):\nyield Markdown(JESSICA)\nwith TabPane(\"Paul\", id=\"paul\"):\nyield Markdown(PAUL)\n
"},{"location":"widgets/tabbed_content/#example","title":"Example","text":"

The following example contains a TabbedContent with three tabs.

Outputtabbed_content.py

TabbedApp LetoJessicaPaul \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eLady\u00a0Jessica\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Bene\u00a0Gesserit\u00a0and\u00a0concubine\u00a0of\u00a0Leto,\u00a0and\u00a0mother\u00a0of\u00a0Paul\u00a0and\u00a0Alia. PaulAlia \u2501\u2578\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 First\u00a0child \u00a0L\u00a0\u00a0Leto\u00a0\u00a0J\u00a0\u00a0Jessica\u00a0\u00a0P\u00a0\u00a0Paul\u00a0

from textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Label, Markdown, TabbedContent, TabPane\nLETO = \"\"\"\n# Duke Leto I Atreides\nHead of House Atreides.\n\"\"\"\nJESSICA = \"\"\"\n# Lady Jessica\nBene Gesserit and concubine of Leto, and mother of Paul and Alia.\n\"\"\"\nPAUL = \"\"\"\n# Paul Atreides\nSon of Leto and Jessica.\n\"\"\"\nclass TabbedApp(App):\n\"\"\"An example of tabbed content.\"\"\"\nBINDINGS = [\n(\"l\", \"show_tab('leto')\", \"Leto\"),\n(\"j\", \"show_tab('jessica')\", \"Jessica\"),\n(\"p\", \"show_tab('paul')\", \"Paul\"),\n]\ndef compose(self) -> ComposeResult:\n\"\"\"Compose app with tabbed content.\"\"\"\n# Footer to show keys\nyield Footer()\n# Add the TabbedContent widget\nwith TabbedContent(initial=\"jessica\"):\nwith TabPane(\"Leto\", id=\"leto\"):  # First tab\nyield Markdown(LETO)  # Tab content\nwith TabPane(\"Jessica\", id=\"jessica\"):\nyield Markdown(JESSICA)\nwith TabbedContent(\"Paul\", \"Alia\"):\nyield TabPane(\"Paul\", Label(\"First child\"))\nyield TabPane(\"Alia\", Label(\"Second child\"))\nwith TabPane(\"Paul\", id=\"paul\"):\nyield Markdown(PAUL)\ndef action_show_tab(self, tab: str) -> None:\n\"\"\"Switch to a new tab.\"\"\"\nself.get_child_by_type(TabbedContent).active = tab\nif __name__ == \"__main__\":\napp = TabbedApp()\napp.run()\n
"},{"location":"widgets/tabbed_content/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description active str \"\" The id attribute of the active tab. Set this to switch tabs."},{"location":"widgets/tabbed_content/#messages","title":"Messages","text":"
  • TabbedContent.TabActivated
"},{"location":"widgets/tabbed_content/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/tabbed_content/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/tabbed_content/#see-also","title":"See also","text":"
  • Tabs
  • ContentSwitcher
"},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent","title":"textual.widgets.TabbedContent class","text":"
def __init__(\nself,\n*titles,\ninitial=\"\",\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: Widget

A container with associated tabs to toggle content visibility.

Parameters Name Type Description Default *titles TextType

Positional argument will be used as title.

() initial str

The id of the initial tab, or empty string to select the first tab.

'' name str | None

The name of the button.

None id str | None

The ID of the button in the DOM.

None classes str | None

The CSS classes of the button.

None disabled bool

Whether the button is disabled or not.

False"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.active","title":"active class-attribute instance-attribute","text":"
active: reactive[str] = reactive('', init=False)\n

The ID of the active tab, or empty string if none are active.

"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.tab_count","title":"tab_count property","text":"
tab_count: int\n

Total number of tabs.

"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.Cleared","title":"Cleared class","text":"
def __init__(self, tabbed_content):\n

Bases: Message

Posted when there are no more tab panes.

Parameters Name Type Description Default tabbed_content TabbedContent

The TabbedContent widget.

required"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.Cleared.control","title":"control property","text":"
control: TabbedContent\n

The TabbedContent widget that was cleared of all tab panes.

This is an alias for Cleared.tabbed_content and is used by the on decorator.

"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.Cleared.tabbed_content","title":"tabbed_content instance-attribute","text":"
tabbed_content = tabbed_content\n

The TabbedContent widget that contains the tab activated.

"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.TabActivated","title":"TabActivated class","text":"
def __init__(self, tabbed_content, tab):\n

Bases: Message

Posted when the active tab changes.

Parameters Name Type Description Default tabbed_content TabbedContent

The TabbedContent widget.

required tab Tab

The Tab widget that was selected (contains the tab label).

required"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.TabActivated.ALLOW_SELECTOR_MATCH","title":"ALLOW_SELECTOR_MATCH class-attribute instance-attribute","text":"
ALLOW_SELECTOR_MATCH = {'tab'}\n

Additional message attributes that can be used with the on decorator.

"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.TabActivated.control","title":"control property","text":"
control: TabbedContent\n

The TabbedContent widget that contains the tab activated.

This is an alias for TabActivated.tabbed_content and is used by the on decorator.

"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.TabActivated.tab","title":"tab instance-attribute","text":"
tab = tab\n

The Tab widget that was selected (contains the tab label).

"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.TabActivated.tabbed_content","title":"tabbed_content instance-attribute","text":"
tabbed_content = tabbed_content\n

The TabbedContent widget that contains the tab activated.

"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.add_pane","title":"add_pane method","text":"
def add_pane(self, pane, *, before=None, after=None):\n

Add a new pane to the tabbed content.

Parameters Name Type Description Default pane TabPane

The pane to add.

required before TabPane | str | None

Optional pane or pane ID to add the pane before.

None after TabPane | str | None

Optional pane or pane ID to add the pane after.

None Returns Type Description AwaitTabbedContent

An awaitable object that waits for the pane to be added.

Raises Type Description Tabs.TabError

If there is a problem with the addition request.

Note

Only one of before or after can be provided. If both are provided a Tabs.TabError will be raised.

"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.clear_panes","title":"clear_panes method","text":"
def clear_panes(self):\n

Remove all the panes in the tabbed content.

"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.disable_tab","title":"disable_tab method","text":"
def disable_tab(self, tab_id):\n

Disables the tab with the given ID.

Parameters Name Type Description Default tab_id str

The ID of the TabPane to disable.

required Raises Type Description Tabs.TabError

If there are any issues with the request.

"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.enable_tab","title":"enable_tab method","text":"
def enable_tab(self, tab_id):\n

Enables the tab with the given ID.

Parameters Name Type Description Default tab_id str

The ID of the TabPane to enable.

required Raises Type Description Tabs.TabError

If there are any issues with the request.

"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.hide_tab","title":"hide_tab method","text":"
def hide_tab(self, tab_id):\n

Hides the tab with the given ID.

Parameters Name Type Description Default tab_id str

The ID of the TabPane to hide.

required Raises Type Description Tabs.TabError

If there are any issues with the request.

"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.remove_pane","title":"remove_pane method","text":"
def remove_pane(self, pane_id):\n

Remove a given pane from the tabbed content.

Parameters Name Type Description Default pane_id str

The ID of the pane to remove.

required Returns Type Description AwaitTabbedContent

An awaitable object that waits for the pane to be removed.

"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.show_tab","title":"show_tab method","text":"
def show_tab(self, tab_id):\n

Shows the tab with the given ID.

Parameters Name Type Description Default tab_id str

The ID of the TabPane to show.

required Raises Type Description Tabs.TabError

If there are any issues with the request.

"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.validate_active","title":"validate_active method","text":"
def validate_active(self, active):\n

It doesn't make sense for active to be an empty string.

Parameters Name Type Description Default active str

Attribute to be validated.

required Returns Type Description str

Value of active.

Raises Type Description ValueError

If the active attribute is set to empty string when there are tabs available.

"},{"location":"widgets/tabbed_content/#textual.widgets.TabPane","title":"textual.widgets.TabPane class","text":"
def __init__(\nself,\ntitle,\n*children,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: Widget

A container for switchable content, with additional title.

This widget is intended to be used with TabbedContent.

Parameters Name Type Description Default title TextType

Title of the TabPane (will be displayed in a tab label).

required *children Widget

Widget to go inside the TabPane.

() name str | None

Optional name for the TabPane.

None id str | None

Optional ID for the TabPane.

None classes str | None

Optional initial classes for the widget.

None disabled bool

Whether the TabPane is disabled or not.

False"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabPane.Disabled","title":"Disabled class","text":"

Bases: TabPaneMessage

Sent when a tab pane is disabled via its reactive disabled.

"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabPane.Enabled","title":"Enabled class","text":"

Bases: TabPaneMessage

Sent when a tab pane is enabled via its reactive disabled.

"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabPane.TabPaneMessage","title":"TabPaneMessage class","text":"

Bases: Message

Base class for TabPane messages.

"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabPane.TabPaneMessage.control","title":"control property","text":"
control: TabPane\n

The tab pane that is the object of this message.

This is an alias for the attribute tab_pane and is used by the on decorator.

"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabPane.TabPaneMessage.tab_pane","title":"tab_pane instance-attribute","text":"
tab_pane: TabPane\n

The TabPane that is he object of this message.

"},{"location":"widgets/tabs/","title":"Tabs","text":"

Added in version 0.15.0

Displays a number of tab headers which may be activated with a click or navigated with cursor keys.

  • Focusable
  • Container

Construct a Tabs widget with strings or Text objects as positional arguments, which will set the labels in the tabs. Here's an example with three tabs:

def compose(self) -> ComposeResult:\nyield Tabs(\"First tab\", \"Second tab\", Text.from_markup(\"[u]Third[/u] tab\"))\n

This will create Tab widgets internally, with auto-incrementing id attributes (\"tab-1\", \"tab-2\" etc). You can also supply Tab objects directly in the constructor, which will allow you to explicitly set an id. Here's an example:

def compose(self) -> ComposeResult:\nyield Tabs(\nTab(\"First tab\", id=\"one\"),\nTab(\"Second tab\", id=\"two\"),\n)\n

When the user switches to a tab by clicking or pressing keys, then Tabs will send a Tabs.TabActivated message which contains the tab that was activated. You can then use event.tab.id attribute to perform any related actions.

"},{"location":"widgets/tabs/#clearing-tabs","title":"Clearing tabs","text":"

Clear tabs by calling the clear method. Clearing the tabs will send a Tabs.TabActivated message with the tab attribute set to None.

"},{"location":"widgets/tabs/#adding-tabs","title":"Adding tabs","text":"

Tabs may be added dynamically with the add_tab method, which accepts strings, Text, or Tab objects.

"},{"location":"widgets/tabs/#example","title":"Example","text":"

The following example adds a Tabs widget above a text label. Press A to add a tab, C to clear the tabs.

Outputtabs.py

TabsApp \u00a0AtreidiesDuke\u00a0Leto\u00a0AtreidesLady\u00a0JessicaGurney\u00a0HalleckBaron\u00a0Vladimir \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258aLady\u00a0Jessica\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u00a0A\u00a0\u00a0Add\u00a0tab\u00a0\u00a0R\u00a0\u00a0Remove\u00a0active\u00a0tab\u00a0\u00a0C\u00a0\u00a0Clear\u00a0tabs\u00a0

from textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Label, Tabs\nNAMES = [\n\"Paul Atreidies\",\n\"Duke Leto Atreides\",\n\"Lady Jessica\",\n\"Gurney Halleck\",\n\"Baron Vladimir Harkonnen\",\n\"Glossu Rabban\",\n\"Chani\",\n\"Silgar\",\n]\nclass TabsApp(App):\n\"\"\"Demonstrates the Tabs widget.\"\"\"\nCSS = \"\"\"\n    Tabs {\n        dock: top;\n    }\n    Screen {\n        align: center middle;\n    }\n    Label {\n        margin:1 1;\n        width: 100%;\n        height: 100%;\n        background: $panel;\n        border: tall $primary;\n        content-align: center middle;\n    }\n    \"\"\"\nBINDINGS = [\n(\"a\", \"add\", \"Add tab\"),\n(\"r\", \"remove\", \"Remove active tab\"),\n(\"c\", \"clear\", \"Clear tabs\"),\n]\ndef compose(self) -> ComposeResult:\nyield Tabs(NAMES[0])\nyield Label()\nyield Footer()\ndef on_mount(self) -> None:\n\"\"\"Focus the tabs when the app starts.\"\"\"\nself.query_one(Tabs).focus()\ndef on_tabs_tab_activated(self, event: Tabs.TabActivated) -> None:\n\"\"\"Handle TabActivated message sent by Tabs.\"\"\"\nlabel = self.query_one(Label)\nif event.tab is None:\n# When the tabs are cleared, event.tab will be None\nlabel.visible = False\nelse:\nlabel.visible = True\nlabel.update(event.tab.label)\ndef action_add(self) -> None:\n\"\"\"Add a new tab.\"\"\"\ntabs = self.query_one(Tabs)\n# Cycle the names\nNAMES[:] = [*NAMES[1:], NAMES[0]]\ntabs.add_tab(NAMES[0])\ndef action_remove(self) -> None:\n\"\"\"Remove active tab.\"\"\"\ntabs = self.query_one(Tabs)\nactive_tab = tabs.active_tab\nif active_tab is not None:\ntabs.remove_tab(active_tab.id)\ndef action_clear(self) -> None:\n\"\"\"Clear the tabs.\"\"\"\nself.query_one(Tabs).clear()\nif __name__ == \"__main__\":\napp = TabsApp()\napp.run()\n
"},{"location":"widgets/tabs/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description active str \"\" The ID of the active tab. Set this attribute to a tab ID to change the active tab."},{"location":"widgets/tabs/#messages","title":"Messages","text":"
  • Tabs.TabActivated
  • Tabs.Cleared
"},{"location":"widgets/tabs/#bindings","title":"Bindings","text":"

The Tabs widget defines the following bindings:

Key(s) Description left Move to the previous tab. right Move to the next tab."},{"location":"widgets/tabs/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/tabs/#textual.widgets.Tabs","title":"textual.widgets.Tabs class","text":"
def __init__(\nself,\n*tabs,\nactive=None,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: Widget

A row of tabs.

Parameters Name Type Description Default *tabs Tab | TextType

Positional argument should be explicit Tab objects, or a str or Text.

() active str | None

ID of the tab which should be active on start.

None name str | None

Optional name for the input widget.

None id str | None

Optional ID for the widget.

None classes str | None

Optional initial classes for the widget.

None disabled bool

Whether the input is disabled or not.

False"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.BINDINGS","title":"BINDINGS class-attribute","text":"
BINDINGS: list[BindingType] = [\nBinding(\n\"left\", \"previous_tab\", \"Previous tab\", show=False\n),\nBinding(\"right\", \"next_tab\", \"Next tab\", show=False),\n]\n
Key(s) Description left Move to the previous tab. right Move to the next tab."},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.active","title":"active class-attribute instance-attribute","text":"
active: reactive[str] = reactive('', init=False)\n

The ID of the active tab, or empty string if none are active.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.active_tab","title":"active_tab property","text":"
active_tab: Tab | None\n

The currently active tab, or None if there are no active tabs.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.tab_count","title":"tab_count property","text":"
tab_count: int\n

Total number of tabs.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.Cleared","title":"Cleared class","text":"
def __init__(self, tabs):\n

Bases: Message

Sent when there are no active tabs.

Parameters Name Type Description Default tabs Tabs

The tabs widget.

required"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.Cleared.control","title":"control property","text":"
control: Tabs\n

The tabs widget which was cleared.

This is an alias for Cleared.tabs which is used by the on decorator.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.Cleared.tabs","title":"tabs instance-attribute","text":"
tabs: Tabs = tabs\n

The tabs widget which was cleared.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabActivated","title":"TabActivated class","text":"

Bases: TabMessage

Sent when a new tab is activated.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabDisabled","title":"TabDisabled class","text":"

Bases: TabMessage

Sent when a tab is disabled.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabEnabled","title":"TabEnabled class","text":"

Bases: TabMessage

Sent when a tab is enabled.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabError","title":"TabError class","text":"

Bases: Exception

Exception raised when there is an error relating to tabs.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabHidden","title":"TabHidden class","text":"

Bases: TabMessage

Sent when a tab is hidden.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabMessage","title":"TabMessage class","text":"
def __init__(self, tabs, tab):\n

Bases: Message

Parent class for all messages that have to do with a specific tab.

Parameters Name Type Description Default tabs Tabs

The Tabs widget.

required tab Tab

The tab that is the object of this message.

required"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabMessage.ALLOW_SELECTOR_MATCH","title":"ALLOW_SELECTOR_MATCH class-attribute instance-attribute","text":"
ALLOW_SELECTOR_MATCH = {'tab'}\n

Additional message attributes that can be used with the on decorator.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabMessage.control","title":"control property","text":"
control: Tabs\n

The tabs widget containing the tab that is the object of this message.

This is an alias for the attribute tabs and is used by the on decorator.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabMessage.tab","title":"tab instance-attribute","text":"
tab: Tab = tab\n

The tab that is the object of this message.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabMessage.tabs","title":"tabs instance-attribute","text":"
tabs: Tabs = tabs\n

The tabs widget containing the tab.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabShown","title":"TabShown class","text":"

Bases: TabMessage

Sent when a tab is shown.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.action_next_tab","title":"action_next_tab method","text":"
def action_next_tab(self):\n

Make the next tab active.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.action_previous_tab","title":"action_previous_tab method","text":"
def action_previous_tab(self):\n

Make the previous tab active.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.add_tab","title":"add_tab method","text":"
def add_tab(self, tab, *, before=None, after=None):\n

Add a new tab to the end of the tab list.

Parameters Name Type Description Default tab Tab | str | Text

A new tab object, or a label (str or Text).

required before Tab | str | None

Optional tab or tab ID to add the tab before.

None after Tab | str | None

Optional tab or tab ID to add the tab after.

None Returns Type Description AwaitMount

An awaitable object that waits for the tab to be mounted.

Raises Type Description Tabs.TabError

If there is a problem with the addition request.

Note

Only one of before or after can be provided. If both are provided a Tabs.TabError will be raised.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.clear","title":"clear method","text":"
def clear(self):\n

Clear all the tabs.

Returns Type Description AwaitRemove

An awaitable object that waits for the tabs to be removed.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.disable","title":"disable method","text":"
def disable(self, tab_id):\n

Disable the indicated tab.

Parameters Name Type Description Default tab_id str

The ID of the Tab to disable.

required Returns Type Description Tab

The Tab that was targeted.

Raises Type Description TabError

If there are any issues with the request.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.enable","title":"enable method","text":"
def enable(self, tab_id):\n

Enable the indicated tab.

Parameters Name Type Description Default tab_id str

The ID of the Tab to enable.

required Returns Type Description Tab

The Tab that was targeted.

Raises Type Description TabError

If there are any issues with the request.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.hide","title":"hide method","text":"
def hide(self, tab_id):\n

Hide the indicated tab.

Parameters Name Type Description Default tab_id str

The ID of the Tab to hide.

required Returns Type Description Tab

The Tab that was targeted.

Raises Type Description TabError

If there are any issues with the request.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.remove_tab","title":"remove_tab method","text":"
def remove_tab(self, tab_or_id):\n

Remove a tab.

Parameters Name Type Description Default tab_or_id Tab | str | None

The Tab to remove or its id.

required Returns Type Description AwaitRemove

An awaitable object that waits for the tab to be removed.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.show","title":"show method","text":"
def show(self, tab_id):\n

Show the indicated tab.

Parameters Name Type Description Default tab_id str

The ID of the Tab to show.

required Returns Type Description Tab

The Tab that was targeted.

Raises Type Description TabError

If there are any issues with the request.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.validate_active","title":"validate_active method","text":"
def validate_active(self, active):\n

Check id assigned to active attribute is a valid tab.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.watch_active","title":"watch_active method","text":"
def watch_active(self, previously_active, active):\n

Handle a change to the active tab.

"},{"location":"widgets/tabs/#textual.widgets.Tab","title":"textual.widgets.Tab class","text":"
def __init__(\nself, label, *, id=None, classes=None, disabled=False\n):\n

Bases: Static

A Widget to manage a single tab within a Tabs widget.

Parameters Name Type Description Default label TextType

The label to use in the tab.

required id str | None

Optional ID for the widget.

None classes str | None

Space separated list of class names.

None disabled bool

Whether the tab is disabled or not.

False"},{"location":"widgets/tabs/#textual.widgets._tabs.Tab.label_text","title":"label_text property","text":"
label_text: str\n

Undecorated text of the label.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tab.Clicked","title":"Clicked class","text":"

Bases: TabMessage

A tab was clicked.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tab.Disabled","title":"Disabled class","text":"

Bases: TabMessage

A tab was disabled.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tab.Enabled","title":"Enabled class","text":"

Bases: TabMessage

A tab was enabled.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tab.TabMessage","title":"TabMessage class","text":"

Bases: Message

Tab-related messages.

These are mostly intended for internal use when interacting with Tabs.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tab.TabMessage.control","title":"control property","text":"
control: Tab\n

The tab that is the object of this message.

This is an alias for the attribute tab and is used by the on decorator.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tab.TabMessage.tab","title":"tab instance-attribute","text":"
tab: Tab\n

The tab that is the object of this message.

"},{"location":"widgets/text_area/","title":"TextArea","text":"

Added in version 0.38.0

A widget for editing text which may span multiple lines. Supports syntax highlighting for a selection of languages.

  • Focusable
  • Container
"},{"location":"widgets/text_area/#guide","title":"Guide","text":""},{"location":"widgets/text_area/#loading-text","title":"Loading text","text":"

In this example we load some initial text into the TextArea, and set the language to \"python\" to enable syntax highlighting.

Outputtext_area_example.py

TextAreaExample 1\u00a0\u00a0defhello(name):\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 2\u00a0\u00a0print(\"hello\"+\u00a0name)\u00a0\u00a0\u00a0 3\u00a0\u00a0 4\u00a0\u00a0defgoodbye(name):\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 5\u00a0\u00a0print(\"goodbye\"+\u00a0name)\u00a0 6\u00a0\u00a0

from textual.app import App, ComposeResult\nfrom textual.widgets import TextArea\nTEXT = \"\"\"\\\ndef hello(name):\n    print(\"hello\" + name)\ndef goodbye(name):\n    print(\"goodbye\" + name)\n\"\"\"\nclass TextAreaExample(App):\ndef compose(self) -> ComposeResult:\nyield TextArea(TEXT, language=\"python\")\napp = TextAreaExample()\nif __name__ == \"__main__\":\napp.run()\n

To load content into the TextArea after it has already been created, use the load_text method.

To update the parser used for syntax highlighting, set the language reactive attribute:

# Set the language to Markdown\ntext_area.language = \"markdown\"\n

Note

Syntax highlighting is unavailable on Python 3.7.

Note

More built-in languages will be added in the future. For now, you can add your own.

"},{"location":"widgets/text_area/#reading-content-from-textarea","title":"Reading content from TextArea","text":"

There are a number of ways to retrieve content from the TextArea:

  • The TextArea.text property returns all content in the text area as a string.
  • The TextArea.selected_text property returns the text corresponding to the current selection.
  • The TextArea.get_text_range method returns the text between two locations.

In all cases, when multiple lines of text are retrieved, the document line separator will be used.

"},{"location":"widgets/text_area/#editing-content-inside-textarea","title":"Editing content inside TextArea","text":"

The content of the TextArea can be updated using the replace method. This method is the programmatic equivalent of selecting some text and then pasting.

Some other convenient methods are available, such as insert, delete, and clear.

"},{"location":"widgets/text_area/#working-with-the-cursor","title":"Working with the cursor","text":""},{"location":"widgets/text_area/#moving-the-cursor","title":"Moving the cursor","text":"

The cursor location is available via the cursor_location property, which represents the location of the cursor as a tuple (row_index, column_index). These indices are zero-based. Writing a new value to cursor_location will immediately update the location of the cursor.

>>> text_area = TextArea()\n>>> text_area.cursor_location\n(0, 0)\n>>> text_area.cursor_location = (0, 4)\n>>> text_area.cursor_location\n(0, 4)\n

cursor_location is a simple way to move the cursor programmatically, but it doesn't let us select text.

"},{"location":"widgets/text_area/#selecting-text","title":"Selecting text","text":"

To select text, we can use the selection reactive attribute. Let's select the first two lines of text in a document by adding text_area.selection = Selection(start=(0, 0), end=(2, 0)) to our code:

Outputtext_area_selection.py

TextAreaSelection 1\u00a0\u00a0defhello(name): 2\u00a0\u00a0print(\"hello\"+\u00a0name) 3\u00a0\u00a0 4\u00a0\u00a0defgoodbye(name):\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 5\u00a0\u00a0print(\"goodbye\"+\u00a0name)\u00a0 6\u00a0\u00a0

from textual.app import App, ComposeResult\nfrom textual.widgets import TextArea\nfrom textual.widgets.text_area import Selection\nTEXT = \"\"\"\\\ndef hello(name):\n    print(\"hello\" + name)\ndef goodbye(name):\n    print(\"goodbye\" + name)\n\"\"\"\nclass TextAreaSelection(App):\ndef compose(self) -> ComposeResult:\ntext_area = TextArea(TEXT, language=\"python\")\ntext_area.selection = Selection(start=(0, 0), end=(2, 0))  # (1)!\nyield text_area\napp = TextAreaSelection()\nif __name__ == \"__main__\":\napp.run()\n
  1. Selects the first two lines of text.

Note that selections can happen in both directions, so Selection((2, 0), (0, 0)) is also valid.

Tip

The end attribute of the selection is always equal to TextArea.cursor_location. In other words, the cursor_location attribute is simply a convenience for accessing text_area.selection.end.

"},{"location":"widgets/text_area/#more-cursor-utilities","title":"More cursor utilities","text":"

There are a number of additional utility methods available for interacting with the cursor.

"},{"location":"widgets/text_area/#location-information","title":"Location information","text":"

A number of properties exist on TextArea which give information about the current cursor location. These properties begin with cursor_at_, and return booleans. For example, cursor_at_start_of_line tells us if the cursor is at a start of line.

We can also check the location the cursor would arrive at if we were to move it. For example, get_cursor_right_location returns the location the cursor would move to if it were to move right. A number of similar methods exist, with names like get_cursor_*_location.

"},{"location":"widgets/text_area/#cursor-movement-methods","title":"Cursor movement methods","text":"

The move_cursor method allows you to move the cursor to a new location while selecting text, or move the cursor and scroll to keep it centered.

# Move the cursor from its current location to row index 4,\n# column index 8, while selecting all the text between.\ntext_area.move_cursor((4, 8), select=True)\n

The move_cursor_relative method offers a very similar interface, but moves the cursor relative to its current location.

"},{"location":"widgets/text_area/#common-selections","title":"Common selections","text":"

There are some methods available which make common selections easier:

  • select_line selects a line by index. Bound to F6 by default.
  • select_all selects all text. Bound to F7 by default.
"},{"location":"widgets/text_area/#themes","title":"Themes","text":"

TextArea ships with some builtin themes, and you can easily add your own.

Themes give you control over the look and feel, including syntax highlighting, the cursor, selection, gutter, and more.

"},{"location":"widgets/text_area/#using-builtin-themes","title":"Using builtin themes","text":"

The initial theme of the TextArea is determined by the theme parameter.

# Create a TextArea with the 'dracula' theme.\nyield TextArea(\"print(123)\", language=\"python\", theme=\"dracula\")\n

You can check which themes are available using the available_themes property.

>>> text_area = TextArea()\n>>> print(text_area.available_themes)\n{'dracula', 'github_light', 'monokai', 'vscode_dark'}\n

After creating a TextArea, you can change the theme by setting the theme attribute to one of the available themes.

text_area.theme = \"vscode_dark\"\n

On setting this attribute the TextArea will immediately refresh to display the updated theme.

"},{"location":"widgets/text_area/#custom-themes","title":"Custom themes","text":"

Using custom (non-builtin) themes is two-step process:

  1. Create an instance of TextAreaTheme.
  2. Register it using TextArea.register_theme.
"},{"location":"widgets/text_area/#1-creating-a-theme","title":"1. Creating a theme","text":"

Let's create a simple theme, \"my_cool_theme\", which colors the cursor blue, and the cursor line yellow. Our theme will also syntax highlight strings as red, and comments as magenta.

from rich.style import Style\nfrom textual.widgets.text_area import TextAreaTheme\n# ...\nmy_theme = TextAreaTheme(\n# This name will be used to refer to the theme...\nname=\"my_cool_theme\",\n# Basic styles such as background, cursor, selection, gutter, etc...\ncursor_style=Style(color=\"white\", bgcolor=\"blue\"),\ncursor_line_style=Style(bgcolor=\"yellow\"),\n# `syntax_styles` is for syntax highlighting.\n# It maps tokens parsed from the document to Rich styles.\nsyntax_styles={\n\"string\": Style(color=\"red\"),\n\"comment\": Style(color=\"magenta\"),\n}\n)\n

Attributes like cursor_style and cursor_line_style apply general language-agnostic styling to the widget.

The syntax_styles attribute of TextAreaTheme is used for syntax highlighting and depends on the language currently in use. For more details, see syntax highlighting.

If you wish to build on an existing theme, you can obtain a reference to it using the TextAreaTheme.get_builtin_theme classmethod:

from textual.widgets.text_area import TextAreaTheme\nmonokai = TextAreaTheme.get_builtin_theme(\"monokai\")\n
"},{"location":"widgets/text_area/#2-registering-a-theme","title":"2. Registering a theme","text":"

Our theme can now be registered with the TextArea instance.

text_area.register_theme(my_theme)\n

After registering a theme, it'll appear in the available_themes:

>>> print(text_area.available_themes)\n{'dracula', 'github_light', 'monokai', 'vscode_dark', 'my_cool_theme'}\n

We can now switch to it:

text_area.theme = \"my_cool_theme\"\n

This immediately updates the appearance of the TextArea:

TextAreaCustomThemes 1\u00a0\u00a0#\u00a0says\u00a0hello 2\u00a0\u00a0def\u00a0hello(name):\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0print(\"hello\"\u00a0+\u00a0name)\u00a0\u00a0\u00a0 4\u00a0\u00a0 5\u00a0\u00a0#\u00a0says\u00a0goodbye 6\u00a0\u00a0def\u00a0goodbye(name):\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 7\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0print(\"goodbye\"\u00a0+\u00a0name)\u00a0 8\u00a0\u00a0

"},{"location":"widgets/text_area/#indentation","title":"Indentation","text":"

The character(s) inserted when you press tab is controlled by setting the indent_type attribute to either tabs or spaces.

If indent_type == \"spaces\", pressing Tab will insert up to indent_width spaces in order to align with the next tab stop.

"},{"location":"widgets/text_area/#line-separators","title":"Line separators","text":"

When content is loaded into TextArea, the content is scanned from beginning to end and the first occurrence of a line separator is recorded.

This separator will then be used when content is later read from the TextArea via the text property. The TextArea widget does not support exporting text which contains mixed line endings.

Similarly, newline characters pasted into the TextArea will be converted.

You can check the line separator of the current document by inspecting TextArea.document.newline:

>>> text_area = TextArea()\n>>> text_area.document.newline\n'\\n'\n
"},{"location":"widgets/text_area/#line-numbers","title":"Line numbers","text":"

The gutter (column on the left containing line numbers) can be toggled by setting the show_line_numbers attribute to True or False.

Setting this attribute will immediately repaint the TextArea to reflect the new value.

"},{"location":"widgets/text_area/#extending-textarea","title":"Extending TextArea","text":"

Sometimes, you may wish to subclass TextArea to add some extra functionality. In this section, we'll briefly explore how we can extend the widget to achieve common goals.

"},{"location":"widgets/text_area/#hooking-into-key-presses","title":"Hooking into key presses","text":"

You may wish to hook into certain key presses to inject some functionality. This can be done by over-riding _on_key and adding the required functionality.

"},{"location":"widgets/text_area/#example-closing-parentheses-automatically","title":"Example - closing parentheses automatically","text":"

Let's extend TextArea to add a feature which automatically closes parentheses and moves the cursor to a sensible location.

from textual import events\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import TextArea\nclass ExtendedTextArea(TextArea):\n\"\"\"A subclass of TextArea with parenthesis-closing functionality.\"\"\"\ndef _on_key(self, event: events.Key) -> None:\nif event.character == \"(\":\nself.insert(\"()\")\nself.move_cursor_relative(columns=-1)\nevent.prevent_default()\nclass TextAreaKeyPressHook(App):\ndef compose(self) -> ComposeResult:\nyield ExtendedTextArea(language=\"python\")\napp = TextAreaKeyPressHook()\nif __name__ == \"__main__\":\napp.run()\n

This intercepts the key handler when \"(\" is pressed, and inserts \"()\" instead. It then moves the cursor so that it lands between the open and closing parentheses.

Typing def hello( into the TextArea results in the bracket automatically being closed:

TextAreaKeyPressHook 1\u00a0\u00a0def\u00a0hello()

"},{"location":"widgets/text_area/#advanced-concepts","title":"Advanced concepts","text":""},{"location":"widgets/text_area/#syntax-highlighting","title":"Syntax highlighting","text":"

Syntax highlighting inside the TextArea is powered by a library called tree-sitter.

Each time you update the document in a TextArea, an internal syntax tree is updated. This tree is frequently queried to find location ranges relevant to syntax highlighting. We give these ranges names, and ultimately map them to Rich styles inside TextAreaTheme.syntax_styles.

To illustrate how this works, lets look at how the \"Monokai\" TextAreaTheme highlights Markdown files.

When the language attribute is set to \"markdown\", a highlight query similar to the one below is used (trimmed for brevity).

(heading_content) @heading\n(link) @link\n

This highlight query maps heading_content nodes returned by the Markdown parser to the name @heading, and link nodes to the name @link.

Inside our TextAreaTheme.syntax_styles dict, we can map the name @heading to a Rich style. Here's a snippet from the \"Monokai\" theme which does just that:

TextAreaTheme(\nname=\"monokai\",\nbase_style=Style(color=\"#f8f8f2\", bgcolor=\"#272822\"),\ngutter_style=Style(color=\"#90908a\", bgcolor=\"#272822\"),\n# ...\nsyntax_styles={\n# Colorise @heading and make them bold\n\"heading\": Style(color=\"#F92672\", bold=True),\n# Colorise and underline @link\n\"link\": Style(color=\"#66D9EF\", underline=True),\n# ...\n},\n)\n

To understand which names can be mapped inside syntax_styles, we recommend looking at the existing themes and highlighting queries (.scm files) in the Textual repository.

Tip

You may also wish to take a look at the contents of TextArea._highlights on an active TextArea instance to see which highlights have been generated for the open document.

"},{"location":"widgets/text_area/#adding-support-for-custom-languages","title":"Adding support for custom languages","text":"

To add support for a language to a TextArea, use the register_language method.

To register a language, we require two things:

  1. A tree-sitter Language object which contains the grammar for the language.
  2. A highlight query which is used for syntax highlighting.
"},{"location":"widgets/text_area/#example-adding-java-support","title":"Example - adding Java support","text":"

The easiest way to obtain a Language object is using the py-tree-sitter-languages package. Here's how we can use this package to obtain a reference to a Language object representing Java:

from tree_sitter_languages import get_language\njava_language = get_language(\"java\")\n

Note

py-tree-sitter-languages may not be available on some architectures (e.g. Macbooks with Apple Silicon running Python 3.7).

The exact version of the parser used when you call get_language can be checked via the repos.txt file in the version of py-tree-sitter-languages you're using. This file contains links to the GitHub repos and commit hashes of the tree-sitter parsers. In these repos you can often find pre-made highlight queries at queries/highlights.scm, and a file showing all the available node types which can be used in highlight queries at src/node-types.json.

Since we're adding support for Java, lets grab the Java highlight query from the repo by following these steps:

  1. Open repos.txt file from the py-tree-sitter-languages repo.
  2. Find the link corresponding to tree-sitter-java and go to the repo on GitHub (you may also need to go to the specific commit referenced in repos.txt).
  3. Go to queries/highlights.scm to see the example highlight query for Java.

Be sure to check the license in the repo to ensure it can be freely copied.

Warning

It's important to use a highlight query which is compatible with the parser in use, so pay attention to the commit hash when visiting the repo via repos.txt.

We now have our Language and our highlight query, so we can register Java as a language.

from pathlib import Path\nfrom tree_sitter_languages import get_language\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import TextArea\njava_language = get_language(\"java\")\njava_highlight_query = (Path(__file__).parent / \"java_highlights.scm\").read_text()\njava_code = \"\"\"\\\nclass HelloWorld {\n    public static void main(String[] args) {\n        System.out.println(\"Hello, World!\");\n    }\n}\n\"\"\"\nclass TextAreaCustomLanguage(App):\ndef compose(self) -> ComposeResult:\ntext_area = TextArea(text=java_code)\ntext_area.cursor_blink = False\n# Register the Java language and highlight query\ntext_area.register_language(java_language, java_highlight_query)\n# Switch to Java\ntext_area.language = \"java\"\nyield text_area\napp = TextAreaCustomLanguage()\nif __name__ == \"__main__\":\napp.run()\n

Running our app, we can see that the Java code is highlighted. We can freely edit the text, and the syntax highlighting will update immediately.

TextAreaCustomLanguage 1\u00a0\u00a0class\u00a0HelloWorld\u00a0{\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 2\u00a0\u00a0publicstatic\u00a0void\u00a0main(String[]\u00a0args)\u00a0{\u00a0 3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0System.out.println(\"Hello,\u00a0World!\");\u00a0 4\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 5\u00a0\u00a0}\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 6\u00a0\u00a0

Recall that we map names (like @heading) from the tree-sitter highlight query to Rich style objects inside the TextAreaTheme.syntax_styles dictionary. If you notice some highlights are missing after registering a language, the issue may be:

  1. The current TextAreaTheme doesn't contain a mapping for the name in the highlight query. Adding a new to syntax_styles should resolve the issue.
  2. The highlight query doesn't assign a name to the pattern you expect to be highlighted. In this case you'll need to update the highlight query to assign to the name.

Tip

The names assigned in tree-sitter highlight queries are often reused across multiple languages. For example, @string is used in many languages to highlight strings.

"},{"location":"widgets/text_area/#reactive-attributes","title":"Reactive attributes","text":"Name Type Default Description language str | None None The language to use for syntax highlighting. theme str | None TextAreaTheme.default() The theme to use for syntax highlighting. selection Selection Selection() The current selection. show_line_numbers bool True Show or hide line numbers. indent_width int 4 The number of spaces to indent and width of tabs. match_cursor_bracket bool True Enable/disable highlighting matching brackets under cursor. cursor_blink bool True Enable/disable blinking of the cursor when the widget has focus."},{"location":"widgets/text_area/#bindings","title":"Bindings","text":"

The TextArea widget defines the following bindings:

Key(s) Description escape Focus on the next item. up Move the cursor up. down Move the cursor down. left Move the cursor left. ctrl+left Move the cursor to the start of the word. ctrl+shift+left Move the cursor to the start of the word and select. right Move the cursor right. ctrl+right Move the cursor to the end of the word. ctrl+shift+right Move the cursor to the end of the word and select. home,ctrl+a Move the cursor to the start of the line. end,ctrl+e Move the cursor to the end of the line. shift+home Move the cursor to the start of the line and select. shift+end Move the cursor to the end of the line and select. pageup Move the cursor one page up. pagedown Move the cursor one page down. shift+up Select while moving the cursor up. shift+down Select while moving the cursor down. shift+left Select while moving the cursor left. shift+right Select while moving the cursor right. backspace Delete character to the left of cursor. ctrl+w Delete from cursor to start of the word. delete,ctrl+d Delete character to the right of cursor. ctrl+f Delete from cursor to end of the word. ctrl+x Delete the current line. ctrl+u Delete from cursor to the start of the line. ctrl+k Delete from cursor to the end of the line. f6 Select the current line. f7 Select all text in the document."},{"location":"widgets/text_area/#component-classes","title":"Component classes","text":"

The TextArea widget defines no component classes.

Styling should be done exclusively via TextAreaTheme.

"},{"location":"widgets/text_area/#see-also","title":"See also","text":"
  • Input - for single-line text input.
  • TextAreaTheme - for theming the TextArea.
  • The tree-sitter documentation website.
  • The tree-sitter Python bindings repository.
  • py-tree-sitter-languages repository (provides binary wheels for a large variety of tree-sitter languages).
"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea","title":"textual.widgets._text_area.TextArea class","text":"
def __init__(\nself,\ntext=\"\",\n*,\nlanguage=None,\ntheme=None,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: ScrollView

Parameters Name Type Description Default text str

The initial text to load into the TextArea.

'' language str | None

The language to use.

None theme str | None

The theme to use.

None name str | None

The name of the TextArea widget.

None id str | None

The ID of the widget, used to refer to it from Textual CSS.

None classes str | None

One or more Textual CSS compatible class names separated by spaces.

None disabled bool

True if the widget is disabled.

False"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.BINDINGS","title":"BINDINGS class-attribute instance-attribute","text":"
BINDINGS = [\nBinding(\n\"escape\",\n\"screen.focus_next\",\n\"Shift Focus\",\nshow=False,\n),\nBinding(\"up\", \"cursor_up\", \"cursor up\", show=False),\nBinding(\n\"down\", \"cursor_down\", \"cursor down\", show=False\n),\nBinding(\n\"left\", \"cursor_left\", \"cursor left\", show=False\n),\nBinding(\n\"right\", \"cursor_right\", \"cursor right\", show=False\n),\nBinding(\n\"ctrl+left\",\n\"cursor_word_left\",\n\"cursor word left\",\nshow=False,\n),\nBinding(\n\"ctrl+right\",\n\"cursor_word_right\",\n\"cursor word right\",\nshow=False,\n),\nBinding(\n\"home,ctrl+a\",\n\"cursor_line_start\",\n\"cursor line start\",\nshow=False,\n),\nBinding(\n\"end,ctrl+e\",\n\"cursor_line_end\",\n\"cursor line end\",\nshow=False,\n),\nBinding(\n\"pageup\",\n\"cursor_page_up\",\n\"cursor page up\",\nshow=False,\n),\nBinding(\n\"pagedown\",\n\"cursor_page_down\",\n\"cursor page down\",\nshow=False,\n),\nBinding(\n\"ctrl+shift+left\",\n\"cursor_word_left(True)\",\n\"cursor left word select\",\nshow=False,\n),\nBinding(\n\"ctrl+shift+right\",\n\"cursor_word_right(True)\",\n\"cursor right word select\",\nshow=False,\n),\nBinding(\n\"shift+home\",\n\"cursor_line_start(True)\",\n\"cursor line start select\",\nshow=False,\n),\nBinding(\n\"shift+end\",\n\"cursor_line_end(True)\",\n\"cursor line end select\",\nshow=False,\n),\nBinding(\n\"shift+up\",\n\"cursor_up(True)\",\n\"cursor up select\",\nshow=False,\n),\nBinding(\n\"shift+down\",\n\"cursor_down(True)\",\n\"cursor down select\",\nshow=False,\n),\nBinding(\n\"shift+left\",\n\"cursor_left(True)\",\n\"cursor left select\",\nshow=False,\n),\nBinding(\n\"shift+right\",\n\"cursor_right(True)\",\n\"cursor right select\",\nshow=False,\n),\nBinding(\"f6\", \"select_line\", \"select line\", show=False),\nBinding(\"f7\", \"select_all\", \"select all\", show=False),\nBinding(\n\"backspace\",\n\"delete_left\",\n\"delete left\",\nshow=False,\n),\nBinding(\n\"ctrl+w\",\n\"delete_word_left\",\n\"delete left to start of word\",\nshow=False,\n),\nBinding(\n\"delete,ctrl+d\",\n\"delete_right\",\n\"delete right\",\nshow=False,\n),\nBinding(\n\"ctrl+f\",\n\"delete_word_right\",\n\"delete right to start of word\",\nshow=False,\n),\nBinding(\n\"ctrl+x\", \"delete_line\", \"delete line\", show=False\n),\nBinding(\n\"ctrl+u\",\n\"delete_to_start_of_line\",\n\"delete to line start\",\nshow=False,\n),\nBinding(\n\"ctrl+k\",\n\"delete_to_end_of_line\",\n\"delete to line end\",\nshow=False,\n),\n]\n
Key(s) Description escape Focus on the next item. up Move the cursor up. down Move the cursor down. left Move the cursor left. ctrl+left Move the cursor to the start of the word. ctrl+shift+left Move the cursor to the start of the word and select. right Move the cursor right. ctrl+right Move the cursor to the end of the word. ctrl+shift+right Move the cursor to the end of the word and select. home,ctrl+a Move the cursor to the start of the line. end,ctrl+e Move the cursor to the end of the line. shift+home Move the cursor to the start of the line and select. shift+end Move the cursor to the end of the line and select. pageup Move the cursor one page up. pagedown Move the cursor one page down. shift+up Select while moving the cursor up. shift+down Select while moving the cursor down. shift+left Select while moving the cursor left. shift+right Select while moving the cursor right. backspace Delete character to the left of cursor. ctrl+w Delete from cursor to start of the word. delete,ctrl+d Delete character to the right of cursor. ctrl+f Delete from cursor to end of the word. ctrl+x Delete the current line. ctrl+u Delete from cursor to the start of the line. ctrl+k Delete from cursor to the end of the line. f6 Select the current line. f7 Select all text in the document."},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.available_languages","title":"available_languages property","text":"
available_languages: set[str]\n

A list of the names of languages available to the TextArea.

The values in this list can be assigned to the language reactive attribute of TextArea.

The returned list contains the builtin languages plus those registered via the register_language method. Builtin languages will be listed before user-registered languages, but there are no other ordering guarantees.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.available_themes","title":"available_themes property","text":"
available_themes: set[str]\n

A list of the names of the themes available to the TextArea.

The values in this list can be assigned theme reactive attribute of TextArea.

You can retrieve the full specification for a theme by passing one of the strings from this list into TextAreaTheme.get_by_name(theme_name: str).

Alternatively, you can directly retrieve a list of TextAreaTheme objects (which contain the full theme specification) by calling TextAreaTheme.builtin_themes().

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cursor_at_end_of_line","title":"cursor_at_end_of_line property","text":"
cursor_at_end_of_line: bool\n

True if and only if the cursor is at the end of a row.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cursor_at_end_of_text","title":"cursor_at_end_of_text property","text":"
cursor_at_end_of_text: bool\n

True if and only if the cursor is at the very end of the document.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cursor_at_first_line","title":"cursor_at_first_line property","text":"
cursor_at_first_line: bool\n

True if and only if the cursor is on the first line.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cursor_at_last_line","title":"cursor_at_last_line property","text":"
cursor_at_last_line: bool\n

True if and only if the cursor is on the last line.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cursor_at_start_of_line","title":"cursor_at_start_of_line property","text":"
cursor_at_start_of_line: bool\n

True if and only if the cursor is at column 0.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cursor_at_start_of_text","title":"cursor_at_start_of_text property","text":"
cursor_at_start_of_text: bool\n

True if and only if the cursor is at location (0, 0)

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cursor_blink","title":"cursor_blink class-attribute instance-attribute","text":"
cursor_blink: Reactive[bool] = reactive(True)\n

True if the cursor should blink.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cursor_location","title":"cursor_location property writable","text":"
cursor_location: Location\n

The current location of the cursor in the document.

This is a utility for accessing the end of TextArea.selection.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.document","title":"document instance-attribute","text":"
document: DocumentBase = Document(text)\n

The document this widget is currently editing.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.gutter_width","title":"gutter_width property","text":"
gutter_width: int\n

The width of the gutter (the left column containing line numbers).

Returns Type Description int

The cell-width of the line number column. If show_line_numbers is False returns 0.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.indent_type","title":"indent_type instance-attribute","text":"
indent_type: Literal['tabs', 'spaces'] = 'spaces'\n

Whether to indent using tabs or spaces.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.indent_width","title":"indent_width class-attribute instance-attribute","text":"
indent_width: Reactive[int] = reactive(4)\n

The width of tabs or the multiple of spaces to align to on pressing the tab key.

If the document currently open contains tabs that are currently visible on screen, altering this value will immediately change the display width of the visible tabs.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.is_syntax_aware","title":"is_syntax_aware property","text":"
is_syntax_aware: bool\n

True if the TextArea is currently syntax aware - i.e. it's parsing document content.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.language","title":"language class-attribute instance-attribute","text":"
language: Reactive[str | None] = language\n

The language of the TextArea.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.match_cursor_bracket","title":"match_cursor_bracket class-attribute instance-attribute","text":"
match_cursor_bracket: Reactive[bool] = reactive(True)\n

If the cursor is at a bracket, highlight the matching bracket (if found).

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.selected_text","title":"selected_text property","text":"
selected_text: str\n

The text between the start and end points of the current selection.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.selection","title":"selection class-attribute instance-attribute","text":"
selection: Reactive[Selection] = reactive(\nSelection(), always_update=True\n)\n

The selection start and end locations (zero-based line_index, offset).

This represents the cursor location and the current selection.

The Selection.end always refers to the cursor location.

If no text is selected, then Selection.end == Selection.start is True.

The text selected in the document is available via the TextArea.selected_text property.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.show_line_numbers","title":"show_line_numbers class-attribute instance-attribute","text":"
show_line_numbers: Reactive[bool] = reactive(True)\n

True to show the line number column on the left edge, otherwise False.

Changing this value will immediately re-render the TextArea.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.text","title":"text property","text":"
text: str\n

The entire text content of the document.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.theme","title":"theme class-attribute instance-attribute","text":"
theme: str | None = theme\n

The name of the theme of the TextArea as set by the user.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_down","title":"action_cursor_down method","text":"
def action_cursor_down(self, select=False):\n

Move the cursor down one cell.

Parameters Name Type Description Default select bool

If True, select the text while moving.

False"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_left","title":"action_cursor_left method","text":"
def action_cursor_left(self, select=False):\n

Move the cursor one location to the left.

If the cursor is at the left edge of the document, try to move it to the end of the previous line.

Parameters Name Type Description Default select bool

If True, select the text while moving.

False"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_line_end","title":"action_cursor_line_end method","text":"
def action_cursor_line_end(self, select=False):\n

Move the cursor to the end of the line.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_line_start","title":"action_cursor_line_start method","text":"
def action_cursor_line_start(self, select=False):\n

Move the cursor to the start of the line.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_page_down","title":"action_cursor_page_down method","text":"
def action_cursor_page_down(self):\n

Move the cursor and scroll down one page.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_page_up","title":"action_cursor_page_up method","text":"
def action_cursor_page_up(self):\n

Move the cursor and scroll up one page.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_right","title":"action_cursor_right method","text":"
def action_cursor_right(self, select=False):\n

Move the cursor one location to the right.

If the cursor is at the end of a line, attempt to go to the start of the next line.

Parameters Name Type Description Default select bool

If True, select the text while moving.

False"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_up","title":"action_cursor_up method","text":"
def action_cursor_up(self, select=False):\n

Move the cursor up one cell.

Parameters Name Type Description Default select bool

If True, select the text while moving.

False"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_word_left","title":"action_cursor_word_left method","text":"
def action_cursor_word_left(self, select=False):\n

Move the cursor left by a single word, skipping trailing whitespace.

Parameters Name Type Description Default select bool

Whether to select while moving the cursor.

False"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_word_right","title":"action_cursor_word_right method","text":"
def action_cursor_word_right(self, select=False):\n

Move the cursor right by a single word, skipping leading whitespace.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_delete_left","title":"action_delete_left method","text":"
def action_delete_left(self):\n

Deletes the character to the left of the cursor and updates the cursor location.

If there's a selection, then the selected range is deleted.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_delete_line","title":"action_delete_line method","text":"
def action_delete_line(self):\n

Deletes the lines which intersect with the selection.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_delete_right","title":"action_delete_right method","text":"
def action_delete_right(self):\n

Deletes the character to the right of the cursor and keeps the cursor at the same location.

If there's a selection, then the selected range is deleted.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_delete_to_end_of_line","title":"action_delete_to_end_of_line method","text":"
def action_delete_to_end_of_line(self):\n

Deletes from the cursor location to the end of the line.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_delete_to_start_of_line","title":"action_delete_to_start_of_line method","text":"
def action_delete_to_start_of_line(self):\n

Deletes from the cursor location to the start of the line.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_delete_word_left","title":"action_delete_word_left method","text":"
def action_delete_word_left(self):\n

Deletes the word to the left of the cursor and updates the cursor location.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_delete_word_right","title":"action_delete_word_right method","text":"
def action_delete_word_right(self):\n

Deletes the word to the right of the cursor and keeps the cursor at the same location.

Note that the location that we delete to using this action is not the same as the location we move to when we move the cursor one word to the right. This action does not skip leading whitespace, whereas cursor movement does.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_select_all","title":"action_select_all method","text":"
def action_select_all(self):\n

Select all the text in the document.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_select_line","title":"action_select_line method","text":"
def action_select_line(self):\n

Select all the text on the current line.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cell_width_to_column_index","title":"cell_width_to_column_index method","text":"
def cell_width_to_column_index(self, cell_width, row_index):\n

Return the column that the cell width corresponds to on the given row.

Parameters Name Type Description Default cell_width int

The cell width to convert.

required row_index int

The index of the row to examine.

required Returns Type Description int

The column corresponding to the cell width on that row.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.clamp_visitable","title":"clamp_visitable method","text":"
def clamp_visitable(self, location):\n

Clamp the given location to the nearest visitable location.

Parameters Name Type Description Default location Location

The location to clamp.

required Returns Type Description Location

The nearest location that we could conceivably navigate to using the cursor.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.clear","title":"clear method","text":"
def clear(self):\n

Delete all text from the document.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.delete","title":"delete method","text":"
def delete(self, start, end, *, maintain_selection_offset=True):\n

Delete the text between two locations in the document.

Parameters Name Type Description Default start Location

The start location.

required end Location

The end location.

required maintain_selection_offset bool

If True, the active Selection will be updated such that the same text is selected before and after the selection, if possible. Otherwise, the cursor will jump to the end point of the edit.

True Returns Type Description EditResult

An EditResult containing information about the edit.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.edit","title":"edit method","text":"
def edit(self, edit):\n

Perform an Edit.

Parameters Name Type Description Default edit Edit

The Edit to perform.

required Returns Type Description Any

Data relating to the edit that may be useful. The data returned

Any

may be different depending on the edit performed.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.find_matching_bracket","title":"find_matching_bracket method","text":"
def find_matching_bracket(self, bracket, search_from):\n

If the character is a bracket, find the matching bracket.

Parameters Name Type Description Default bracket str

The character we're searching for the matching bracket of.

required search_from Location

The location to start the search.

required Returns Type Description Location | None

The Location of the matching bracket, or None if it's not found.

Location | None

If the character is not available for bracket matching, None is returned.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_column_width","title":"get_column_width method","text":"
def get_column_width(self, row, column):\n

Get the cell offset of the column from the start of the row.

Parameters Name Type Description Default row int

The row index.

required column int

The column index (codepoint offset from start of row).

required Returns Type Description int

The cell width of the column relative to the start of the row.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_cursor_down_location","title":"get_cursor_down_location method","text":"
def get_cursor_down_location(self):\n

Get the location the cursor will move to if it moves down.

Returns Type Description Location

The location the cursor will move to if it moves down.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_cursor_left_location","title":"get_cursor_left_location method","text":"
def get_cursor_left_location(self):\n

Get the location the cursor will move to if it moves left.

Returns Type Description Location

The location of the cursor if it moves left.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_cursor_line_end_location","title":"get_cursor_line_end_location method","text":"
def get_cursor_line_end_location(self):\n

Get the location of the end of the current line.

Returns Type Description Location

The (row, column) location of the end of the cursors current line.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_cursor_line_start_location","title":"get_cursor_line_start_location method","text":"
def get_cursor_line_start_location(self):\n

Get the location of the start of the current line.

Returns Type Description Location

The (row, column) location of the start of the cursors current line.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_cursor_right_location","title":"get_cursor_right_location method","text":"
def get_cursor_right_location(self):\n

Get the location the cursor will move to if it moves right.

Returns Type Description Location

the location the cursor will move to if it moves right.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_cursor_up_location","title":"get_cursor_up_location method","text":"
def get_cursor_up_location(self):\n

Get the location the cursor will move to if it moves up.

Returns Type Description Location

The location the cursor will move to if it moves up.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_cursor_word_left_location","title":"get_cursor_word_left_location method","text":"
def get_cursor_word_left_location(self):\n

Get the location the cursor will jump to if it goes 1 word left.

Returns Type Description Location

The location the cursor will jump on \"jump word left\".

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_cursor_word_right_location","title":"get_cursor_word_right_location method","text":"
def get_cursor_word_right_location(self):\n

Get the location the cursor will jump to if it goes 1 word right.

Returns Type Description Location

The location the cursor will jump on \"jump word right\".

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_target_document_location","title":"get_target_document_location method","text":"
def get_target_document_location(self, event):\n

Given a MouseEvent, return the row and column offset of the event in document-space.

Parameters Name Type Description Default event MouseEvent

The MouseEvent.

required Returns Type Description Location

The location of the mouse event within the document.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_text_range","title":"get_text_range method","text":"
def get_text_range(self, start, end):\n

Get the text between a start and end location.

Parameters Name Type Description Default start Location

The start location.

required end Location

The end location.

required Returns Type Description str

The text between start and end.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.insert","title":"insert method","text":"
def insert(\nself,\ntext,\nlocation=None,\n*,\nmaintain_selection_offset=True\n):\n

Insert text into the document.

Parameters Name Type Description Default text str

The text to insert.

required location Location | None

The location to insert text, or None to use the cursor location.

None maintain_selection_offset bool

If True, the active Selection will be updated such that the same text is selected before and after the selection, if possible. Otherwise, the cursor will jump to the end point of the edit.

True Returns Type Description EditResult

An EditResult containing information about the edit.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.load_document","title":"load_document method","text":"
def load_document(self, document):\n

Load a document into the TextArea.

Parameters Name Type Description Default document DocumentBase

The document to load into the TextArea.

required"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.load_text","title":"load_text method","text":"
def load_text(self, text):\n

Load text into the TextArea.

This will replace the text currently in the TextArea.

Parameters Name Type Description Default text str

The text to load into the TextArea.

required"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.move_cursor","title":"move_cursor method","text":"
def move_cursor(\nself,\nlocation,\nselect=False,\ncenter=False,\nrecord_width=True,\n):\n

Move the cursor to a location.

Parameters Name Type Description Default location Location

The location to move the cursor to.

required select bool

If True, select text between the old and new location.

False center bool

If True, scroll such that the cursor is centered.

False record_width bool

If True, record the cursor column cell width after navigating so that we jump back to the same width the next time we move to a row that is wide enough.

True"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.move_cursor_relative","title":"move_cursor_relative method","text":"
def move_cursor_relative(\nself,\nrows=0,\ncolumns=0,\nselect=False,\ncenter=False,\nrecord_width=True,\n):\n

Move the cursor relative to its current location.

Parameters Name Type Description Default rows int

The number of rows to move down by (negative to move up)

0 columns int

The number of columns to move right by (negative to move left)

0 select bool

If True, select text between the old and new location.

False center bool

If True, scroll such that the cursor is centered.

False record_width bool

If True, record the cursor column cell width after navigating so that we jump back to the same width the next time we move to a row that is wide enough.

True"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.record_cursor_width","title":"record_cursor_width method","text":"
def record_cursor_width(self):\n

Record the current cell width of the cursor.

This is used where we navigate up and down through rows. If we're in the middle of a row, and go down to a row with no content, then we go down to another row, we want our cursor to jump back to the same offset that we were originally at.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.register_language","title":"register_language method","text":"
def register_language(self, language, highlight_query):\n

Register a language and corresponding highlight query.

Calling this method does not change the language of the TextArea. On switching to this language (via the language reactive attribute), syntax highlighting will be performed using the given highlight query.

If a string name is supplied for a builtin supported language, then this method will update the default highlight query for that language.

Registering a language only registers it to this instance of TextArea.

Parameters Name Type Description Default language str | 'Language'

A string referring to a builtin language or a tree-sitter Language object.

required highlight_query str

The highlight query to use for syntax highlighting this language.

required"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.register_theme","title":"register_theme method","text":"
def register_theme(self, theme):\n

Register a theme for use by the TextArea.

After registering a theme, you can set themes by assigning the theme name to the TextArea.theme reactive attribute. For example text_area.theme = \"my_custom_theme\" where \"my_custom_theme\" is the name of the theme you registered.

If you supply a theme with a name that already exists that theme will be overwritten.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.replace","title":"replace method","text":"
def replace(\nself,\ninsert,\nstart,\nend,\n*,\nmaintain_selection_offset=True\n):\n

Replace text in the document with new text.

Parameters Name Type Description Default insert str

The text to insert.

required start Location

The start location

required end Location

The end location.

required maintain_selection_offset bool

If True, the active Selection will be updated such that the same text is selected before and after the selection, if possible. Otherwise, the cursor will jump to the end point of the edit.

True Returns Type Description EditResult

An EditResult containing information about the edit.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.scroll_cursor_visible","title":"scroll_cursor_visible method","text":"
def scroll_cursor_visible(self, center=False, animate=False):\n

Scroll the TextArea such that the cursor is visible on screen.

Parameters Name Type Description Default center bool

True if the cursor should be scrolled to the center.

False animate bool

True if we should animate while scrolling.

False Returns Type Description Offset

The offset that was scrolled to bring the cursor into view.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.select_all","title":"select_all method","text":"
def select_all(self):\n

Select all of the text in the TextArea.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.select_line","title":"select_line method","text":"
def select_line(self, index):\n

Select all the text in the specified line.

Parameters Name Type Description Default index int

The index of the line to select (starting from 0).

required"},{"location":"widgets/text_area/#textual.widgets.text_area.Highlight","title":"Highlight module-attribute","text":"
Highlight = Tuple[StartColumn, EndColumn, HighlightName]\n

A tuple representing a syntax highlight within one line.

"},{"location":"widgets/text_area/#textual.widgets.text_area.Location","title":"Location module-attribute","text":"
Location = Tuple[int, int]\n

A location (row, column) within the document. Indexing starts at 0.

"},{"location":"widgets/text_area/#textual.widgets.text_area.Document","title":"Document class","text":"
def __init__(self, text):\n

Bases: DocumentBase

A document which can be opened in a TextArea.

"},{"location":"widgets/text_area/#textual.document._document.Document.line_count","title":"line_count property","text":"
line_count: int\n

Returns the number of lines in the document.

"},{"location":"widgets/text_area/#textual.document._document.Document.lines","title":"lines property","text":"
lines: list[str]\n

Get the document as a list of strings, where each string represents a line.

Newline characters are not included in at the end of the strings.

The newline character used in this document can be found via the Document.newline property.

"},{"location":"widgets/text_area/#textual.document._document.Document.newline","title":"newline property","text":"
newline: Newline\n

Get the Newline used in this document (e.g. ' ', ' '. etc.)

"},{"location":"widgets/text_area/#textual.document._document.Document.text","title":"text property","text":"
text: str\n

Get the text from the document.

"},{"location":"widgets/text_area/#textual.document._document.Document.get_line","title":"get_line method","text":"
def get_line(self, index):\n

Returns the line with the given index from the document.

Parameters Name Type Description Default index int

The index of the line in the document.

required Returns Type Description str

The string representing the line.

"},{"location":"widgets/text_area/#textual.document._document.Document.get_size","title":"get_size method","text":"
def get_size(self, tab_width):\n

The Size of the document, taking into account the tab rendering width.

Parameters Name Type Description Default tab_width int

The width to use for tab indents.

required Returns Type Description Size

The size (width, height) of the document.

"},{"location":"widgets/text_area/#textual.document._document.Document.get_text_range","title":"get_text_range method","text":"
def get_text_range(self, start, end):\n

Get the text that falls between the start and end locations.

Returns the text between start and end, including the appropriate line separator character as specified by Document._newline. Note that _newline is set automatically to the first line separator character found in the document.

Parameters Name Type Description Default start Location

The start location of the selection.

required end Location

The end location of the selection.

required Returns Type Description str

The text between start (inclusive) and end (exclusive).

"},{"location":"widgets/text_area/#textual.document._document.Document.replace_range","title":"replace_range method","text":"
def replace_range(self, start, end, text):\n

Replace text at the given range.

Parameters Name Type Description Default start Location

A tuple (row, column) where the edit starts.

required end Location

A tuple (row, column) where the edit ends.

required text str

The text to insert between start and end.

required Returns Type Description EditResult

The EditResult containing information about the completed replace operation.

"},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase","title":"DocumentBase class","text":"

Bases: ABC

Describes the minimum functionality a Document implementation must provide in order to be used by the TextArea widget.

"},{"location":"widgets/text_area/#textual.document._document.DocumentBase.line_count","title":"line_count property abstractmethod","text":"
line_count: int\n

Returns the number of lines in the document.

"},{"location":"widgets/text_area/#textual.document._document.DocumentBase.newline","title":"newline property abstractmethod","text":"
newline: Newline\n

Return the line separator used in the document.

"},{"location":"widgets/text_area/#textual.document._document.DocumentBase.text","title":"text property abstractmethod","text":"
text: str\n

The text from the document as a string.

"},{"location":"widgets/text_area/#textual.document._document.DocumentBase.get_line","title":"get_line abstractmethod","text":"
def get_line(self, index):\n

Returns the line with the given index from the document.

This is used in rendering lines, and will be called by the TextArea for each line that is rendered.

Parameters Name Type Description Default index int

The index of the line in the document.

required Returns Type Description str

The str instance representing the line.

"},{"location":"widgets/text_area/#textual.document._document.DocumentBase.get_size","title":"get_size abstractmethod","text":"
def get_size(self, indent_width):\n

Get the size of the document.

The height is generally the number of lines, and the width is generally the maximum cell length of all the lines.

Parameters Name Type Description Default indent_width int

The width to use for tab characters.

required Returns Type Description Size

The Size of the document bounding box.

"},{"location":"widgets/text_area/#textual.document._document.DocumentBase.get_text_range","title":"get_text_range abstractmethod","text":"
def get_text_range(self, start, end):\n

Get the text that falls between the start and end locations.

Parameters Name Type Description Default start Location

The start location of the selection.

required end Location

The end location of the selection.

required Returns Type Description str

The text between start (inclusive) and end (exclusive).

"},{"location":"widgets/text_area/#textual.document._document.DocumentBase.query_syntax_tree","title":"query_syntax_tree method","text":"
def query_syntax_tree(\nself, query, start_point=None, end_point=None\n):\n

Query the tree-sitter syntax tree.

The default implementation always returns an empty list.

To support querying in a subclass, this must be implemented.

Parameters Name Type Description Default query 'Query'

The tree-sitter Query to perform.

required start_point tuple[int, int] | None

The (row, column byte) to start the query at.

None end_point tuple[int, int] | None

The (row, column byte) to end the query at.

None Returns Type Description list[tuple['Node', str]]

A tuple containing the nodes and text captured by the query.

"},{"location":"widgets/text_area/#textual.document._document.DocumentBase.replace_range","title":"replace_range abstractmethod","text":"
def replace_range(self, start, end, text):\n

Replace the text at the given range.

Parameters Name Type Description Default start Location

A tuple (row, column) where the edit starts.

required end Location

A tuple (row, column) where the edit ends.

required text str

The text to insert between start and end.

required Returns Type Description EditResult

The new end location after the edit is complete.

"},{"location":"widgets/text_area/#textual.widgets.text_area.Edit","title":"Edit class","text":"

Implements the Undoable protocol to replace text at some range within a document.

"},{"location":"widgets/text_area/#textual.widgets._text_area.Edit.from_location","title":"from_location instance-attribute","text":"
from_location: Location\n

The start location of the insert.

"},{"location":"widgets/text_area/#textual.widgets._text_area.Edit.maintain_selection_offset","title":"maintain_selection_offset instance-attribute","text":"
maintain_selection_offset: bool\n

If True, the selection will maintain its offset to the replacement range.

"},{"location":"widgets/text_area/#textual.widgets._text_area.Edit.text","title":"text instance-attribute","text":"
text: str\n

The text to insert. An empty string is equivalent to deletion.

"},{"location":"widgets/text_area/#textual.widgets._text_area.Edit.to_location","title":"to_location instance-attribute","text":"
to_location: Location\n

The end location of the insert

"},{"location":"widgets/text_area/#textual.widgets._text_area.Edit.after","title":"after method","text":"
def after(self, text_area):\n

Possibly update the cursor location after the widget has been refreshed.

Parameters Name Type Description Default text_area TextArea

The TextArea this operation was performed on.

required"},{"location":"widgets/text_area/#textual.widgets._text_area.Edit.do","title":"do method","text":"
def do(self, text_area):\n

Perform the edit operation.

Parameters Name Type Description Default text_area TextArea

The TextArea to perform the edit on.

required Returns Type Description EditResult

An EditResult containing information about the replace operation.

"},{"location":"widgets/text_area/#textual.widgets._text_area.Edit.undo","title":"undo method","text":"
def undo(self, text_area):\n

Undo the edit operation.

Parameters Name Type Description Default text_area TextArea

The TextArea to undo the insert operation on.

required Returns Type Description EditResult

An EditResult containing information about the replace operation.

"},{"location":"widgets/text_area/#textual.widgets.text_area.EditResult","title":"EditResult class","text":"

Contains information about an edit that has occurred.

"},{"location":"widgets/text_area/#textual.document._document.EditResult.end_location","title":"end_location instance-attribute","text":"
end_location: Location\n

The new end Location after the edit is complete.

"},{"location":"widgets/text_area/#textual.document._document.EditResult.replaced_text","title":"replaced_text instance-attribute","text":"
replaced_text: str\n

The text that was replaced.

"},{"location":"widgets/text_area/#textual.widgets.text_area.LanguageDoesNotExist","title":"LanguageDoesNotExist class","text":"

Bases: Exception

Raised when the user tries to use a language which does not exist. This means a language which is not builtin, or has not been registered.

"},{"location":"widgets/text_area/#textual.widgets.text_area.Selection","title":"Selection class","text":"

Bases: NamedTuple

A range of characters within a document from a start point to the end point. The location of the cursor is always considered to be the end point of the selection. The selection is inclusive of the minimum point and exclusive of the maximum point.

"},{"location":"widgets/text_area/#textual.document._document.Selection.end","title":"end class-attribute instance-attribute","text":"
end: Location = (0, 0)\n

The end location of the selection.

If you were to click and drag a selection inside a text-editor, this is where you finished dragging.

"},{"location":"widgets/text_area/#textual.document._document.Selection.is_empty","title":"is_empty property","text":"
is_empty: bool\n

Return True if the selection has 0 width, i.e. it's just a cursor.

"},{"location":"widgets/text_area/#textual.document._document.Selection.start","title":"start class-attribute instance-attribute","text":"
start: Location = (0, 0)\n

The start location of the selection.

If you were to click and drag a selection inside a text-editor, this is where you started dragging.

"},{"location":"widgets/text_area/#textual.document._document.Selection.cursor","title":"cursor classmethod","text":"
def cursor(cls, location):\n

Create a Selection with the same start and end point - a \"cursor\".

Parameters Name Type Description Default location Location

The location to create the zero-width Selection.

required"},{"location":"widgets/text_area/#textual.widgets.text_area.SyntaxAwareDocument","title":"SyntaxAwareDocument class","text":"
def __init__(self, text, language):\n

Bases: Document

A wrapper around a Document which also maintains a tree-sitter syntax tree when the document is edited.

The primary reason for this split is actually to keep tree-sitter stuff separate, since it isn't supported in Python 3.7. By having the tree-sitter code isolated in this subclass, it makes it easier to conditionally import. However, it does come with other design flaws (e.g. Document is required to have methods which only really make sense on SyntaxAwareDocument).

If you're reading this and Python 3.7 is no longer supported by Textual, consider merging this subclass into the Document superclass.

Parameters Name Type Description Default text str

The initial text contained in the document.

required language str | Language

The language to use. You can pass a string to use a supported language, or pass in your own tree-sitter Language object.

required"},{"location":"widgets/text_area/#textual.document._syntax_aware_document.SyntaxAwareDocument.language","title":"language instance-attribute","text":"
language: Language | None = None\n

The tree-sitter Language or None if tree-sitter is unavailable.

"},{"location":"widgets/text_area/#textual.document._syntax_aware_document.SyntaxAwareDocument.get_line","title":"get_line method","text":"
def get_line(self, line_index):\n

Return the string representing the line, not including new line characters.

Parameters Name Type Description Default line_index int

The index of the line.

required Returns Type Description str

The string representing the line.

"},{"location":"widgets/text_area/#textual.document._syntax_aware_document.SyntaxAwareDocument.prepare_query","title":"prepare_query method","text":"
def prepare_query(self, query):\n

Prepare a tree-sitter tree query.

Queries should be prepared once, then reused.

To execute a query, call query_syntax_tree.

Returns Type Description Query | None

The prepared query.

"},{"location":"widgets/text_area/#textual.document._syntax_aware_document.SyntaxAwareDocument.query_syntax_tree","title":"query_syntax_tree method","text":"
def query_syntax_tree(\nself, query, start_point=None, end_point=None\n):\n

Query the tree-sitter syntax tree.

The default implementation always returns an empty list.

To support querying in a subclass, this must be implemented.

Parameters Name Type Description Default query Query

The tree-sitter Query to perform.

required start_point tuple[int, int] | None

The (row, column byte) to start the query at.

None end_point tuple[int, int] | None

The (row, column byte) to end the query at.

None Returns Type Description list[tuple['Node', str]]

A tuple containing the nodes and text captured by the query.

"},{"location":"widgets/text_area/#textual.document._syntax_aware_document.SyntaxAwareDocument.replace_range","title":"replace_range method","text":"
def replace_range(self, start, end, text):\n

Replace text at the given range.

Parameters Name Type Description Default start Location

A tuple (row, column) where the edit starts.

required end Location

A tuple (row, column) where the edit ends.

required text str

The text to insert between start and end.

required Returns Type Description EditResult

The new end location after the edit is complete.

"},{"location":"widgets/text_area/#textual.widgets.text_area.TextAreaTheme","title":"TextAreaTheme class","text":"

A theme for the TextArea widget.

Allows theming the general widget (gutter, selections, cursor, and so on) and mapping of tree-sitter tokens to Rich styles.

For example, consider the following snippet from the markdown.scm highlight query file. We've assigned the heading_content token type to the name heading.

(heading_content) @heading\n

Now, we can map this heading name to a Rich style, and it will be styled as such in the TextArea, assuming a parser which returns a heading_content node is used (as will be the case when language=\"markdown\").

TextAreaTheme('my_theme', syntax_styles={'heading': Style(color='cyan', bold=True)})\n

We can register this theme with our TextArea using the TextArea.register_theme method, and headings in our markdown files will be styled bold cyan.

"},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.base_style","title":"base_style class-attribute instance-attribute","text":"
base_style: Style | None = None\n

The background style of the text area. If None the parent style will be used.

"},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.bracket_matching_style","title":"bracket_matching_style class-attribute instance-attribute","text":"
bracket_matching_style: Style | None = None\n

The style to apply to matching brackets. If None, a legible Style will be generated.

"},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.cursor_line_gutter_style","title":"cursor_line_gutter_style class-attribute instance-attribute","text":"
cursor_line_gutter_style: Style | None = None\n

The style to apply to the gutter of the line the cursor is on. If None, a legible Style will be generated.

"},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.cursor_line_style","title":"cursor_line_style class-attribute instance-attribute","text":"
cursor_line_style: Style | None = None\n

The style to apply to the line the cursor is on.

"},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.cursor_style","title":"cursor_style class-attribute instance-attribute","text":"
cursor_style: Style | None = None\n

The style of the cursor. If None, a legible Style will be generated.

"},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.gutter_style","title":"gutter_style class-attribute instance-attribute","text":"
gutter_style: Style | None = None\n

The style of the gutter. If None, a legible Style will be generated.

"},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.name","title":"name instance-attribute","text":"
name: str\n

The name of the theme.

"},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.selection_style","title":"selection_style class-attribute instance-attribute","text":"
selection_style: Style | None = None\n

The style of the selection. If None a default selection Style will be generated.

"},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.syntax_styles","title":"syntax_styles class-attribute instance-attribute","text":"
syntax_styles: dict[str, Style] = field(\ndefault_factory=dict\n)\n

The mapping of tree-sitter names from the highlight_query to Rich styles.

"},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.builtin_themes","title":"builtin_themes classmethod","text":"
def builtin_themes(cls):\n

Get a list of all builtin TextAreaThemes.

Returns Type Description list[TextAreaTheme]

A list of all builtin TextAreaThemes.

"},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.default","title":"default classmethod","text":"
def default(cls):\n

Get the default syntax theme.

Returns Type Description TextAreaTheme

The default TextAreaTheme (probably \"monokai\").

"},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.get_builtin_theme","title":"get_builtin_theme classmethod","text":"
def get_builtin_theme(cls, theme_name):\n

Get a TextAreaTheme by name.

Given a theme_name, return the corresponding TextAreaTheme object.

Parameters Name Type Description Default theme_name str

The name of the theme.

required Returns Type Description 'TextAreaTheme' | None

The TextAreaTheme corresponding to the name or None if the theme isn't

'TextAreaTheme' | None

found.

"},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.get_highlight","title":"get_highlight method","text":"
def get_highlight(self, name):\n

Return the Rich style corresponding to the name defined in the tree-sitter highlight query for the current theme.

Parameters Name Type Description Default name str

The name of the highlight.

required Returns Type Description Style | None

The Style to use for this highlight, or None if no style.

"},{"location":"widgets/text_area/#textual.widgets.text_area.ThemeDoesNotExist","title":"ThemeDoesNotExist class","text":"

Bases: Exception

Raised when the user tries to use a theme which does not exist. This means a theme which is not builtin, or has not been registered.

"},{"location":"widgets/toast/","title":"Toast","text":"

Added in version 0.30.0

A widget which displays a notification message.

  • Focusable
  • Container

Note that Toast isn't designed to be used directly in your applications, but it is instead used by notify to display a message when using Textual's built-in notification system.

"},{"location":"widgets/toast/#styling","title":"Styling","text":"

You can customize the style of Toasts by targeting the Toast CSS type. For example:

Toast {\npadding: 3;\n}\n

The three severity levels also have corresponding classes, allowing you to target the different styles of notification. They are:

  • -information
  • -warning
  • -error

If you wish to tailor the notifications for your application you can add rules to your CSS like this:

Toast.-information {\n/* Styling here. */\n}\nToast.-warning {\n/* Styling here. */\n}\nToast.-error {\n/* Styling here. */\n}\n

You can customize just the title wih the toast--title class. The following would make the title italic for an information toast:

Toast.-information .toast--title {\ntext-style: italic;\n}\n
"},{"location":"widgets/toast/#example","title":"Example","text":"Outputtoast.py

ToastApp \u258e\u258a \u258eIt's\u00a0an\u00a0older\u00a0code,\u00a0sir,\u00a0but\u00a0it\u00a0\u258a \u258echecks\u00a0out.\u258a \u258e\u258a \u258e\u258a \u258ePossible\u00a0trap\u00a0detected\u258a \u258eNow\u00a0witness\u00a0the\u00a0firepower\u00a0of\u00a0this\u258a \u258efully\u00a0ARMED\u00a0and\u00a0OPERATIONAL\u258a \u258ebattle\u00a0station!\u258a \u258e\u258a \u258e\u258a \u258eIt's\u00a0a\u00a0trap!\u258a \u258e\u258a \u258e\u258a \u258eIt's\u00a0against\u00a0my\u00a0programming\u00a0to\u00a0\u258a \u258eimpersonate\u00a0a\u00a0deity.\u258a \u258e\u258a

from textual.app import App\nclass ToastApp(App[None]):\ndef on_mount(self) -> None:\n# Show an information notification.\nself.notify(\"It's an older code, sir, but it checks out.\")\n# Show a warning. Note that Textual's notification system allows\n# for the use of Rich console markup.\nself.notify(\n\"Now witness the firepower of this fully \"\n\"[b]ARMED[/b] and [i][b]OPERATIONAL[/b][/i] battle station!\",\ntitle=\"Possible trap detected\",\nseverity=\"warning\",\n)\n# Show an error. Set a longer timeout so it's noticed.\nself.notify(\"It's a trap!\", severity=\"error\", timeout=10)\n# Show an information notification, but without any sort of title.\nself.notify(\"It's against my programming to impersonate a deity.\", title=\"\")\nif __name__ == \"__main__\":\nToastApp().run()\n
"},{"location":"widgets/toast/#reactive-attributes","title":"Reactive Attributes","text":"

This widget has no reactive attributes.

"},{"location":"widgets/toast/#messages","title":"Messages","text":"

This widget posts no messages.

"},{"location":"widgets/toast/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/toast/#component-classes","title":"Component Classes","text":"

The toast widget provides the following component classes:

Class Description toast--title Targets the title of the toast."},{"location":"widgets/toast/#textual.widgets._toast","title":"textual.widgets._toast","text":"

Widgets for showing notification messages in toasts.

"},{"location":"widgets/toast/#textual.widgets._toast.Toast","title":"Toast class","text":"
def __init__(self, notification):\n

Bases: Static

A widget for displaying short-lived notifications.

Parameters Name Type Description Default notification Notification

The notification to show in the toast.

required"},{"location":"widgets/toast/#textual.widgets._toast.Toast.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
COMPONENT_CLASSES: set[str] = {'toast--title'}\n
Class Description toast--title Targets the title of the toast."},{"location":"widgets/toast/#textual.widgets._toast.ToastHolder","title":"ToastHolder class","text":"

Bases: Container

Container that holds a single toast.

Used to control the alignment of each of the toasts in the main toast container.

"},{"location":"widgets/toast/#textual.widgets._toast.ToastRack","title":"ToastRack class","text":"

Bases: Container

A container for holding toasts.

"},{"location":"widgets/toast/#textual.widgets._toast.ToastRack.show","title":"show method","text":"
def show(self, notifications):\n

Show the notifications as toasts.

Parameters Name Type Description Default notifications Notifications

The notifications to show.

required"},{"location":"widgets/tree/","title":"Tree","text":"

Added in version 0.6.0

A tree control widget.

  • Focusable
  • Container
"},{"location":"widgets/tree/#example","title":"Example","text":"

The example below creates a simple tree.

Outputtree.py

TreeApp \u25bc\u00a0Dune \u2517\u2501\u2501\u00a0\u25bc\u00a0Characters \u2523\u2501\u2501\u00a0Paul \u2523\u2501\u2501\u00a0Jessica \u2517\u2501\u2501\u00a0Chani

from textual.app import App, ComposeResult\nfrom textual.widgets import Tree\nclass TreeApp(App):\ndef compose(self) -> ComposeResult:\ntree: Tree[dict] = Tree(\"Dune\")\ntree.root.expand()\ncharacters = tree.root.add(\"Characters\", expand=True)\ncharacters.add_leaf(\"Paul\")\ncharacters.add_leaf(\"Jessica\")\ncharacters.add_leaf(\"Chani\")\nyield tree\nif __name__ == \"__main__\":\napp = TreeApp()\napp.run()\n

Tree widgets have a \"root\" attribute which is an instance of a TreeNode. Call add() or add_leaf() to add new nodes underneath the root. Both these methods return a TreeNode for the child which you can use to add additional levels.

"},{"location":"widgets/tree/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description show_root bool True Show the root node. show_guides bool True Show guide lines between levels. guide_depth int 4 Amount of indentation between parent and child."},{"location":"widgets/tree/#messages","title":"Messages","text":"
  • Tree.NodeCollapsed
  • Tree.NodeExpanded
  • Tree.NodeHighlighted
  • Tree.NodeSelected
"},{"location":"widgets/tree/#bindings","title":"Bindings","text":"

The tree widget defines the following bindings:

Key(s) Description enter Select the current item. space Toggle the expand/collapsed space of the current item. up Move the cursor up. down Move the cursor down."},{"location":"widgets/tree/#component-classes","title":"Component Classes","text":"

The tree widget provides the following component classes:

Class Description tree--cursor Targets the cursor. tree--guides Targets the indentation guides. tree--guides-hover Targets the indentation guides under the cursor. tree--guides-selected Targets the indentation guides that are selected. tree--highlight Targets the highlighted items. tree--highlight-line Targets the lines under the cursor. tree--label Targets the (text) labels of the items."},{"location":"widgets/tree/#textual.widgets.Tree","title":"textual.widgets.Tree class","text":"
def __init__(\nself,\nlabel,\ndata=None,\n*,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: Generic[TreeDataType], ScrollView

A widget for displaying and navigating data in a tree.

Parameters Name Type Description Default label TextType

The label of the root node of the tree.

required data TreeDataType | None

The optional data to associate with the root node of the tree.

None name str | None

The name of the Tree.

None id str | None

The ID of the tree in the DOM.

None classes str | None

The CSS classes of the tree.

None disabled bool

Whether the tree is disabled or not.

False"},{"location":"widgets/tree/#textual.widgets._tree.Tree.BINDINGS","title":"BINDINGS class-attribute","text":"
BINDINGS: list[BindingType] = [\nBinding(\"enter\", \"select_cursor\", \"Select\", show=False),\nBinding(\"space\", \"toggle_node\", \"Toggle\", show=False),\nBinding(\"up\", \"cursor_up\", \"Cursor Up\", show=False),\nBinding(\n\"down\", \"cursor_down\", \"Cursor Down\", show=False\n),\n]\n
Key(s) Description enter Select the current item. space Toggle the expand/collapsed space of the current item. up Move the cursor up. down Move the cursor down."},{"location":"widgets/tree/#textual.widgets._tree.Tree.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
COMPONENT_CLASSES: set[str] = {\n\"tree--cursor\",\n\"tree--guides\",\n\"tree--guides-hover\",\n\"tree--guides-selected\",\n\"tree--highlight\",\n\"tree--highlight-line\",\n\"tree--label\",\n}\n
Class Description tree--cursor Targets the cursor. tree--guides Targets the indentation guides. tree--guides-hover Targets the indentation guides under the cursor. tree--guides-selected Targets the indentation guides that are selected. tree--highlight Targets the highlighted items. tree--highlight-line Targets the lines under the cursor. tree--label Targets the (text) labels of the items."},{"location":"widgets/tree/#textual.widgets._tree.Tree.auto_expand","title":"auto_expand class-attribute instance-attribute","text":"
auto_expand = var(True)\n

Auto expand tree nodes when clicked.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.cursor_line","title":"cursor_line class-attribute instance-attribute","text":"
cursor_line = var(-1, always_update=True)\n

The line with the cursor, or -1 if no cursor.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.cursor_node","title":"cursor_node property","text":"
cursor_node: TreeNode[TreeDataType] | None\n

The currently selected node, or None if no selection.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.guide_depth","title":"guide_depth class-attribute instance-attribute","text":"
guide_depth = reactive(4, init=False)\n

The indent depth of tree nodes.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.hover_line","title":"hover_line class-attribute instance-attribute","text":"
hover_line = var(-1)\n

The line number under the mouse pointer, or -1 if not under the mouse pointer.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.last_line","title":"last_line property","text":"
last_line: int\n

The index of the last line.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.root","title":"root instance-attribute","text":"
root = self._add_node(None, text_label, data)\n

The root node of the tree.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.show_guides","title":"show_guides class-attribute instance-attribute","text":"
show_guides = reactive(True)\n

Enable display of tree guide lines.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.show_root","title":"show_root class-attribute instance-attribute","text":"
show_root = reactive(True)\n

Show the root of the tree.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeCollapsed","title":"NodeCollapsed class","text":"
def __init__(self, node):\n

Bases: Generic[EventTreeDataType], Message

Event sent when a node is collapsed.

Can be handled using on_tree_node_collapsed in a subclass of Tree or in a parent node in the DOM.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeCollapsed.control","title":"control property","text":"
control: Tree[EventTreeDataType]\n

The tree that sent the message.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeCollapsed.node","title":"node instance-attribute","text":"
node: TreeNode[EventTreeDataType] = node\n

The node that was collapsed.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeExpanded","title":"NodeExpanded class","text":"
def __init__(self, node):\n

Bases: Generic[EventTreeDataType], Message

Event sent when a node is expanded.

Can be handled using on_tree_node_expanded in a subclass of Tree or in a parent node in the DOM.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeExpanded.control","title":"control property","text":"
control: Tree[EventTreeDataType]\n

The tree that sent the message.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeExpanded.node","title":"node instance-attribute","text":"
node: TreeNode[EventTreeDataType] = node\n

The node that was expanded.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeHighlighted","title":"NodeHighlighted class","text":"
def __init__(self, node):\n

Bases: Generic[EventTreeDataType], Message

Event sent when a node is highlighted.

Can be handled using on_tree_node_highlighted in a subclass of Tree or in a parent node in the DOM.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeHighlighted.control","title":"control property","text":"
control: Tree[EventTreeDataType]\n

The tree that sent the message.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeHighlighted.node","title":"node instance-attribute","text":"
node: TreeNode[EventTreeDataType] = node\n

The node that was highlighted.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeSelected","title":"NodeSelected class","text":"
def __init__(self, node):\n

Bases: Generic[EventTreeDataType], Message

Event sent when a node is selected.

Can be handled using on_tree_node_selected in a subclass of Tree or in a parent node in the DOM.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeSelected.control","title":"control property","text":"
control: Tree[EventTreeDataType]\n

The tree that sent the message.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeSelected.node","title":"node instance-attribute","text":"
node: TreeNode[EventTreeDataType] = node\n

The node that was selected.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.UnknownNodeID","title":"UnknownNodeID class","text":"

Bases: Exception

Exception raised when referring to an unknown TreeNode ID.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.action_cursor_down","title":"action_cursor_down method","text":"
def action_cursor_down(self):\n

Move the cursor down one node.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.action_cursor_up","title":"action_cursor_up method","text":"
def action_cursor_up(self):\n

Move the cursor up one node.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.action_page_down","title":"action_page_down method","text":"
def action_page_down(self):\n

Move the cursor down a page's-worth of nodes.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.action_page_up","title":"action_page_up method","text":"
def action_page_up(self):\n

Move the cursor up a page's-worth of nodes.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.action_scroll_end","title":"action_scroll_end method","text":"
def action_scroll_end(self):\n

Move the cursor to the bottom of the tree.

Note

Here bottom means vertically, not branch depth.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.action_scroll_home","title":"action_scroll_home method","text":"
def action_scroll_home(self):\n

Move the cursor to the top of the tree.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.action_select_cursor","title":"action_select_cursor method","text":"
def action_select_cursor(self):\n

Cause a select event for the target node.

Note

If auto_expand is True use of this action on a non-leaf node will cause both an expand/collapse event to occur, as well as a selected event.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.action_toggle_node","title":"action_toggle_node method","text":"
def action_toggle_node(self):\n

Toggle the expanded state of the target node.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.clear","title":"clear method","text":"
def clear(self):\n

Clear all nodes under root.

Returns Type Description Self

The Tree instance.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.get_label_width","title":"get_label_width method","text":"
def get_label_width(self, node):\n

Get the width of the nodes label.

The default behavior is to call render_node and return the cell length. This method may be overridden in a sub-class if it can be done more efficiently.

Parameters Name Type Description Default node TreeNode[TreeDataType]

A node.

required Returns Type Description int

Width in cells.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.get_node_at_line","title":"get_node_at_line method","text":"
def get_node_at_line(self, line_no):\n

Get the node for a given line.

Parameters Name Type Description Default line_no int

A line number.

required Returns Type Description TreeNode[TreeDataType] | None

A tree node, or None if there is no node at that line.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.get_node_by_id","title":"get_node_by_id method","text":"
def get_node_by_id(self, node_id):\n

Get a tree node by its ID.

Parameters Name Type Description Default node_id NodeID

The ID of the node to get.

required Returns Type Description TreeNode[TreeDataType]

The node associated with that ID.

Raises Type Description Tree.UnknownID

Raised if the TreeNode ID is unknown.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.process_label","title":"process_label method","text":"
def process_label(self, label):\n

Process a str or Text value into a label.

Maybe overridden in a subclass to change how labels are rendered.

Parameters Name Type Description Default label TextType

Label.

required Returns Type Description Text

A Rich Text object.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.refresh_line","title":"refresh_line method","text":"
def refresh_line(self, line):\n

Refresh (repaint) a given line in the tree.

Parameters Name Type Description Default line int

Line number.

required"},{"location":"widgets/tree/#textual.widgets._tree.Tree.render_label","title":"render_label method","text":"
def render_label(self, node, base_style, style):\n

Render a label for the given node. Override this to modify how labels are rendered.

Parameters Name Type Description Default node TreeNode[TreeDataType]

A tree node.

required base_style Style

The base style of the widget.

required style Style

The additional style for the label.

required Returns Type Description Text

A Rich Text object containing the label.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.reset","title":"reset method","text":"
def reset(self, label, data=None):\n

Clear the tree and reset the root node.

Parameters Name Type Description Default label TextType

The label for the root node.

required data TreeDataType | None

Optional data for the root node.

None Returns Type Description Self

The Tree instance.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.scroll_to_line","title":"scroll_to_line method","text":"
def scroll_to_line(self, line, animate=True):\n

Scroll to the given line.

Parameters Name Type Description Default line int

A line number.

required animate bool

Enable animation.

True"},{"location":"widgets/tree/#textual.widgets._tree.Tree.scroll_to_node","title":"scroll_to_node method","text":"
def scroll_to_node(self, node, animate=True):\n

Scroll to the given node.

Parameters Name Type Description Default node TreeNode[TreeDataType]

Node to scroll in to view.

required animate bool

Animate scrolling.

True"},{"location":"widgets/tree/#textual.widgets._tree.Tree.select_node","title":"select_node method","text":"
def select_node(self, node):\n

Move the cursor to the given node, or reset cursor.

Parameters Name Type Description Default node TreeNode[TreeDataType] | None

A tree node, or None to reset cursor.

required"},{"location":"widgets/tree/#textual.widgets._tree.Tree.validate_cursor_line","title":"validate_cursor_line method","text":"
def validate_cursor_line(self, value):\n

Prevent cursor line from going outside of range.

Parameters Name Type Description Default value int

The value to test.

required Return

A valid version of the given value.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.validate_guide_depth","title":"validate_guide_depth method","text":"
def validate_guide_depth(self, value):\n

Restrict guide depth to reasonable range.

Parameters Name Type Description Default value int

The value to test.

required Return

A valid version of the given value.

"},{"location":"widgets/tree/#textual.widgets.tree.TreeNode","title":"textual.widgets.tree.TreeNode class","text":"
def __init__(\nself,\ntree,\nparent,\nid,\nlabel,\ndata=None,\n*,\nexpanded=True,\nallow_expand=True\n):\n

Bases: Generic[TreeDataType]

An object that represents a \"node\" in a tree control.

Parameters Name Type Description Default tree Tree[TreeDataType]

The tree that the node is being attached to.

required parent TreeNode[TreeDataType] | None

The parent node that this node is being attached to.

required id NodeID

The ID of the node.

required label Text

The label for the node.

required data TreeDataType | None

Optional data to associate with the node.

None expanded bool

Should the node be attached in an expanded state?

True allow_expand bool

Should the node allow being expanded by the user?

True"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.allow_expand","title":"allow_expand property writable","text":"
allow_expand: bool\n

Is this node allowed to expand?

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.children","title":"children property","text":"
children: TreeNodes[TreeDataType]\n

The child nodes of a TreeNode.

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.data","title":"data instance-attribute","text":"
data = data\n

Optional data associated with the tree node.

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.id","title":"id property","text":"
id: NodeID\n

The ID of the node.

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.is_expanded","title":"is_expanded property","text":"
is_expanded: bool\n

Is the node expanded?

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.is_last","title":"is_last property","text":"
is_last: bool\n

Is this the last child node of its parent?

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.is_root","title":"is_root property","text":"
is_root: bool\n

Is this node the root of the tree?

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.label","title":"label property writable","text":"
label: TextType\n

The label for the node.

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.line","title":"line property","text":"
line: int\n

The line number for this node, or -1 if it is not displayed.

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.parent","title":"parent property","text":"
parent: TreeNode[TreeDataType] | None\n

The parent of the node.

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.tree","title":"tree property","text":"
tree: Tree[TreeDataType]\n

The tree that this node is attached to.

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.RemoveRootError","title":"RemoveRootError class","text":"

Bases: Exception

Exception raised when trying to remove a tree's root node.

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.add","title":"add method","text":"
def add(\nself,\nlabel,\ndata=None,\n*,\nexpand=False,\nallow_expand=True\n):\n

Add a node to the sub-tree.

Parameters Name Type Description Default label TextType

The new node's label.

required data TreeDataType | None

Data associated with the new node.

None expand bool

Node should be expanded.

False allow_expand bool

Allow use to expand the node via keyboard or mouse.

True Returns Type Description TreeNode[TreeDataType]

A new Tree node

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.add_leaf","title":"add_leaf method","text":"
def add_leaf(self, label, data=None):\n

Add a 'leaf' node (a node that can not expand).

Parameters Name Type Description Default label TextType

Label for the node.

required data TreeDataType | None

Optional data.

None Returns Type Description TreeNode[TreeDataType]

New node.

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.collapse","title":"collapse method","text":"
def collapse(self):\n

Collapse the node (hide its children).

Returns Type Description Self

The TreeNode instance.

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.collapse_all","title":"collapse_all method","text":"
def collapse_all(self):\n

Collapse the node (hide its children) and all those below it.

Returns Type Description Self

The TreeNode instance.

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.expand","title":"expand method","text":"
def expand(self):\n

Expand the node (show its children).

Returns Type Description Self

The TreeNode instance.

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.expand_all","title":"expand_all method","text":"
def expand_all(self):\n

Expand the node (show its children) and all those below it.

Returns Type Description Self

The TreeNode instance.

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.remove","title":"remove method","text":"
def remove(self):\n

Remove this node from the tree.

Raises Type Description TreeNode.RemoveRootError

If there is an attempt to remove the root.

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.remove_children","title":"remove_children method","text":"
def remove_children(self):\n

Remove any child nodes of this node.

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.set_label","title":"set_label method","text":"
def set_label(self, label):\n

Set a new label for the node.

Parameters Name Type Description Default label TextType

A str or Text object with the new label.

required"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.toggle","title":"toggle method","text":"
def toggle(self):\n

Toggle the node's expanded state.

Returns Type Description Self

The TreeNode instance.

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.toggle_all","title":"toggle_all method","text":"
def toggle_all(self):\n

Toggle the node's expanded state and make all those below it match.

Returns Type Description Self

The TreeNode instance.

"},{"location":"blog/archive/2023/","title":"2023","text":""},{"location":"blog/archive/2022/","title":"2022","text":""},{"location":"blog/category/release/","title":"Release","text":""},{"location":"blog/category/devlog/","title":"DevLog","text":""},{"location":"blog/category/news/","title":"News","text":""},{"location":"blog/page/2/","title":"Textual Blog","text":""},{"location":"blog/page/3/","title":"Textual Blog","text":""},{"location":"blog/page/4/","title":"Textual Blog","text":""},{"location":"blog/archive/2023/page/2/","title":"2023","text":""},{"location":"blog/archive/2023/page/3/","title":"2023","text":""},{"location":"blog/archive/2022/page/2/","title":"2022","text":""},{"location":"blog/category/release/page/2/","title":"Release","text":""},{"location":"blog/category/devlog/page/2/","title":"DevLog","text":""}]} \ No newline at end of file +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"],"fields":{"title":{"boost":1000.0},"text":{"boost":1.0},"tags":{"boost":1000000.0}}},"docs":[{"location":"","title":"Home","text":"

Tip

See the navigation links in the header or side-bar.

Click (top left) on mobile.

"},{"location":"#welcome","title":"Welcome","text":"

Welcome to the Textual framework documentation.

Get started or go straight to the Tutorial

"},{"location":"#what-is-textual","title":"What is Textual?","text":"

Textual is a Rapid Application Development framework for Python, built by Textualize.io.

Build sophisticated user interfaces with a simple Python API. Run your apps in the terminal or a web browser (with textual-web)!

  • Rapid development

    Uses your existing Python skills to build beautiful user interfaces.

  • Low requirements

    Run Textual on a single board computer if you want to.

  • Cross platform

    Textual runs just about everywhere.

  • Remote

    Textual apps can run over SSH.

  • CLI Integration

    Textual apps can be launched and run from the command prompt.

  • Open Source

    Textual is licensed under MIT.

CalculatorApp \u257a\u2501\u2513\u00a0\u00a0\u2513\u00a0\u257b\u00a0\u257b\u250f\u2501\u2578\u250f\u2501\u2513\u257a\u2501\u2513 \u00a0\u2501\u252b\u00a0\u00a0\u2503\u00a0\u2517\u2501\u252b\u2517\u2501\u2513\u2517\u2501\u252b\u250f\u2501\u251b \u257a\u2501\u251b.\u257a\u253b\u2578\u00a0\u00a0\u2579\u257a\u2501\u251b\u257a\u2501\u251b\u2517\u2501\u2578 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0C\u00a0\u00a0+/-\u00a0\u00a0%\u00a0\u00a0\u00f7\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a07\u00a0\u00a08\u00a0\u00a09\u00a0\u00a0\u00d7\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a04\u00a0\u00a05\u00a0\u00a06\u00a0\u00a0-\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a01\u00a0\u00a02\u00a0\u00a03\u00a0\u00a0+\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a00\u00a0\u00a0.\u00a0\u00a0=\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

PrideApp

StopwatchApp \u2b58StopwatchApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a000:00:00.00\u00a0Reset\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a000:00:00.00\u00a0Reset\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a000:00:00.00\u00a0Reset\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0\u00a0A\u00a0\u00a0Add\u00a0\u00a0R\u00a0\u00a0Remove\u00a0

CombiningLayoutsExample \u2b58CombiningLayoutsExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502HorizontallyPositionedChildrenHere\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a00\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a01\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a02\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2586\u2586\u2502\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502Vertical\u00a0layout,\u00a0child\u00a03\u2502\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502Thispanelis\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a04\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a05\u2502\u2502usinggrid\u00a0layout!\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a06\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

WelcomeApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Welcome!\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b Textual\u00a0is\u00a0a\u00a0TUI,\u00a0or\u00a0Text\u00a0User\u00a0Interface,\u00a0framework\u00a0for\u00a0Python\u00a0inspired\u00a0by\u00a0\u00a0 modern\u00a0web\u00a0development.\u00a0We\u00a0hope\u00a0you\u00a0enjoy\u00a0using\u00a0Textual! Dune\u00a0quote \u258c\u00a0\"I\u00a0must\u00a0not\u00a0fear.Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0little-death\u00a0that \u258c\u00a0brings\u00a0total\u00a0obliteration.I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass \u258c\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner \u258c\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only \u258c\u00a0I\u00a0will\u00a0remain.\" \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0OK\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

"},{"location":"FAQ/","title":"FAQ","text":""},{"location":"FAQ/#frequently-asked-questions","title":"Frequently Asked Questions","text":"

Welcome to the Textual FAQ. Here we try and answer any question that comes up frequently. If you can't find what you are looking for here, see our other help channels.

"},{"location":"FAQ/#does-textual-support-images","title":"Does Textual support images?","text":"

Textual doesn't have built-in support for images yet, but it is on the Roadmap.

See also the rich-pixels project for a Rich renderable for images that works with Textual.

"},{"location":"FAQ/#how-can-i-fix-importerror-cannot-import-name-composeresult-from-textualapp","title":"How can I fix ImportError cannot import name ComposeResult from textual.app ?","text":"

You likely have an older version of Textual. You can install the latest version by adding the -U switch which will force pip to upgrade.

The following should do it:

pip install textual-dev -U\n

"},{"location":"FAQ/#how-can-i-select-and-copy-text-in-a-textual-app","title":"How can I select and copy text in a Textual app?","text":"

Running a Textual app puts your terminal in to application mode which disables clicking and dragging to select text. Most terminal emulators offer a modifier key which you can hold while you click and drag to restore the behavior you may expect from the command line. The exact modifier key depends on the terminal and platform you are running on.

  • iTerm Hold the OPTION key.
  • Gnome Terminal Hold the SHIFT key.
  • Windows Terminal Hold the SHIFT key.

Refer to the documentation for your terminal emulator, if it is not listed above.

"},{"location":"FAQ/#how-can-i-set-a-translucent-app-background","title":"How can I set a translucent app background?","text":"

Some terminal emulators have a translucent background feature which allows the desktop underneath to be partially visible.

This feature is unlikely to work with Textual, as the translucency effect requires the use of ANSI background colors, which Textual doesn't use. Textual uses 16.7 million colors where available which enables consistent colors across all platforms and additional effects which aren't possible with ANSI colors.

For more information on ANSI colors in Textual, see Why no Ansi Themes?.

"},{"location":"FAQ/#how-do-i-center-a-widget-in-a-screen","title":"How do I center a widget in a screen?","text":"

Tip

See How To Center Things in the Textual documentation for a more comprensive answer to this question.

To center a widget within a container use align. But remember that align works on the children of a container, it isn't something you use on the child you want centered.

For example, here's an app that shows a Button in the middle of a Screen:

from textual.app import App, ComposeResult\nfrom textual.widgets import Button\nclass ButtonApp(App):\nCSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Button(\"PUSH ME!\")\nif __name__ == \"__main__\":\nButtonApp().run()\n

If you use the above on multiple widgets, you'll find they appear to \"left-align\" in the center of the screen, like this:

+-----+\n|     |\n+-----+\n\n+---------+\n|         |\n+---------+\n\n+---------------+\n|               |\n+---------------+\n

If you want them more like this:

     +-----+\n     |     |\n     +-----+\n\n   +---------+\n   |         |\n   +---------+\n\n+---------------+\n|               |\n+---------------+\n

the best approach is to wrap each widget in a Center container that individually centers it. For example:

from textual.app import App, ComposeResult\nfrom textual.containers import Center\nfrom textual.widgets import Button\nclass ButtonApp(App):\nCSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Center(Button(\"PUSH ME!\"))\nyield Center(Button(\"AND ME!\"))\nyield Center(Button(\"ALSO PLEASE PUSH ME!\"))\nyield Center(Button(\"HEY ME ALSO!!\"))\nif __name__ == \"__main__\":\nButtonApp().run()\n

"},{"location":"FAQ/#how-do-i-fix-workerdeclarationerror","title":"How do I fix WorkerDeclarationError?","text":"

Textual version 0.31.0 requires that you set thread=True on the @work decorator if you want to run a threaded worker.

If you want a threaded worker, you would declare it in the following way:

@work(thread=True)\ndef run_in_background():\n...\n

If you don't want a threaded worker, you should make your work function async:

@work()\nasync def run_in_background():\n...\n

This change was made because it was too easy to accidentally create a threaded worker, which may produce unexpected results.

"},{"location":"FAQ/#how-do-i-pass-arguments-to-an-app","title":"How do I pass arguments to an app?","text":"

When creating your App class, override __init__ as you would when inheriting normally. For example:

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass Greetings(App[None]):\ndef __init__(self, greeting: str=\"Hello\", to_greet: str=\"World\") -> None:\nself.greeting = greeting\nself.to_greet = to_greet\nsuper().__init__()\ndef compose(self) -> ComposeResult:\nyield Static(f\"{self.greeting}, {self.to_greet}\")\n

Then the app can be run, passing in various arguments; for example:

# Running with default arguments.\nGreetings().run()\n# Running with a keyword argument.\nGreetings(to_greet=\"davep\").run()\n# Running with both positional arguments.\nGreetings(\"Well hello\", \"there\").run()\n

"},{"location":"FAQ/#no-widget-called-textlog","title":"No widget called TextLog","text":"

The TextLog widget was renamed to RichLog in Textual 0.32.0. You will need to replace all references to TextLog in your code, with RichLog. Most IDEs will have a search and replace function which will help you do this.

Here's how you should import RichLog:

from textual.widgets import RichLog\n

"},{"location":"FAQ/#why-do-some-key-combinations-never-make-it-to-my-app","title":"Why do some key combinations never make it to my app?","text":"

Textual can only ever support key combinations that are passed on by your terminal application. Which keys get passed on can differ from terminal to terminal, and from operating system to operating system.

Because of this it's best to stick to key combinations that are known to be universally-supported; these include the likes of:

  • Letters
  • Numbers
  • Numbered function keys (especially F1 through F10)
  • Space
  • Return
  • Arrow, home, end and page keys
  • Control
  • Shift

When creating bindings for your application we recommend picking keys and key combinations from the above.

Keys that aren't normally passed through by terminals include Cmd and Option on macOS, and the Windows key on Windows.

If you need to test what key combinations work in different environments you can try them out with textual keys.

"},{"location":"FAQ/#why-doesnt-textual-look-good-on-macos","title":"Why doesn't Textual look good on macOS?","text":"

You may find that the default macOS Terminal.app doesn't render Textual apps (and likely other TUIs) very well, particuarily when it comes to box characters. For instance, you may find it displays misaligned blocks and lines like this:

You can (mostly) fix this by opening settings -> profiles > Text tab, and changing the font settings. We have found that Menlo Regular font, with a character spacing of 1 and line spacing of 0.805 produces reasonable results. If you want to use another font, you may have to tweak the line spacing until you get good results.

With these changes, Textual apps render more as intended:

Even with this fix, Terminal.app has a few limitations. It is limited to 256 colors, and can be a little slow compared to more modern alternatives. Fortunately there are a number of free terminal emulators for macOS which produces high quality results.

We recommend any of the following terminals:

  • iTerm2
  • Kitty
  • WezTerm
"},{"location":"FAQ/#terminalapp-colors","title":"Terminal.app colors","text":""},{"location":"FAQ/#iterm2-colors","title":"iTerm2 colors","text":""},{"location":"FAQ/#why-doesnt-textual-support-ansi-themes","title":"Why doesn't Textual support ANSI themes?","text":"

Textual will not generate escape sequences for the 16 themeable ANSI colors.

This is an intentional design decision we took for for the following reasons:

  • Not everyone has a carefully chosen ANSI color theme. Color combinations which may look fine on your system, may be unreadable on another machine. There is very little an app author or Textual can do to resolve this. Asking users to simply pick a better theme is not a good solution, since not all users will know how.
  • ANSI colors can't be manipulated in the way Textual can do with other colors. Textual can blend colors and produce light and dark shades from an original color, which is used to create more readable text and user interfaces. Color blending will also be used to power future accessibility features.

Textual has a design system which guarantees apps will be readable on all platforms and terminals, and produces better results than ANSI colors.

There is currently a light and dark version of the design system, but more are planned. It will also be possible for users to customize the source colors on a per-app or per-system basis. This means that in the future you will be able to modify the core colors to blend in with your chosen terminal theme.

"},{"location":"FAQ/#why-doesnt-the-datatable-scroll-programmatically","title":"Why doesn't the DataTable scroll programmatically?","text":"

If scrolling in your DataTable is apparently broken, it may be because your DataTable is using the default value of height: auto. This means that the table will be sized to fit its rows without scrolling, which may cause the container (typically the screen) to scroll. If you would like the table itself to scroll, set the height to something other than auto, like 100%.

NOTE: As of Textual v0.31.0 the max-height of a DataTable is set to 100%, this will mean that the above is no longer the default experience.

Generated by FAQtory

"},{"location":"getting_started/","title":"Getting started","text":"

All you need to get started building Textual apps.

"},{"location":"getting_started/#requirements","title":"Requirements","text":"

Textual requires Python 3.7 or later (if you have a choice, pick the most recent Python). Textual runs on Linux, macOS, Windows and probably any OS where Python also runs.

Your platform

"},{"location":"getting_started/#linux-all-distros","title":"Linux (all distros)","text":"

All Linux distros come with a terminal emulator that can run Textual apps.

"},{"location":"getting_started/#macos","title":"macOS","text":"

The default terminal app is limited to 256 colors. We recommend installing a newer terminal such as iterm2, Kitty, or WezTerm.

"},{"location":"getting_started/#windows","title":"Windows","text":"

The new Windows Terminal runs Textual apps beautifully.

"},{"location":"getting_started/#installation","title":"Installation","text":"

Here's how to install Textual.

"},{"location":"getting_started/#from-pypi","title":"From PyPI","text":"

You can install Textual via PyPI, with the following command:

pip install textual\n

If you plan on developing Textual apps, you should also install textual developer tools:

pip install textual-dev\n
"},{"location":"getting_started/#from-conda-forge","title":"From conda-forge","text":"

Textual is also available on conda-forge. The preferred package manager for conda-forge is currently micromamba:

micromamba install -c conda-forge textual\n

And for the textual developer tools:

micromamba install -c conda-forge textual-dev\n
"},{"location":"getting_started/#textual-cli","title":"Textual CLI","text":"

If you installed the developer tools you should have access to the textual command. There are a number of sub-commands available which will aid you in building Textual apps. Run the following for a list of the available commands:

textual --help\n

See devtools for more about the textual command.

"},{"location":"getting_started/#demo","title":"Demo","text":"

Once you have Textual installed, run the following to get an impression of what it can do:

python -m textual\n

If Textual is installed you should see the following:

Textual\u00a0Demo \u2b58Textual\u00a0Demo TOP\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Widgets\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 Textual\u00a0widgets\u00a0are\u00a0powerful\u00a0interactive\u00a0components.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 Widgets Build\u00a0your\u00a0own\u00a0or\u00a0use\u00a0the\u00a0builtin\u00a0widgets.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u2022\u00a0Input\u00a0Text\u00a0/\u00a0Password\u00a0input.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 Rich\u00a0content\u00a0\u2022\u00a0Button\u00a0Clickable\u00a0button\u00a0with\u00a0a\u00a0number\u00a0of\u00a0styles.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u2022\u00a0Switch\u00a0A\u00a0switch\u00a0to\u00a0toggle\u00a0between\u00a0states.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2583\u2583 \u00a0\u2022\u00a0DataTable\u00a0A\u00a0spreadsheet-like\u00a0widget\u00a0for\u00a0navigating\u00a0data.\u00a0Cells\u00a0may\u00a0contain\u00a0text\u00a0or\u00a0Rich\u00a0 renderables.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 CSS\u00a0\u2022\u00a0Tree\u00a0An\u00a0generic\u00a0tree\u00a0with\u00a0expandable\u00a0nodes.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u2022\u00a0DirectoryTree\u00a0A\u00a0tree\u00a0of\u00a0file\u00a0and\u00a0folders.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u2022\u00a0...\u00a0many\u00a0more\u00a0planned\u00a0... \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a \u258eUsername\u258awill\u258e\u258a \u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a \u258e\u258a\u2585\u2585 \u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a \u258ePassword\u258aPassword\u258e\u258a \u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a \u258e\u258a \u258e\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258a \u258e\u00a0Login\u00a0\u258a \u258e\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Foo\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Bar\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Baz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Foo\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(0,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(0,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(0,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(0,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(1,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(1,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(1,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(1,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(2,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(2,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(2,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(2,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(3,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(3,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(3,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(3,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(4,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(4,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(4,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(4,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(5,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(5,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(5,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(5,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(6,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(6,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(6,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(6,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(7,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(7,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(7,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(7,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(8,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(8,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(8,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(8,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(9,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(9,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(9,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(9,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2582\u2582 \u00a0Cell\u00a0(10,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(10,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(10,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(10,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(11,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(11,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(11,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(11,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(12,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(12,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(12,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(12,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0Cell\u00a0(13,\u00a00)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(13,\u00a01)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(13,\u00a02)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Cell\u00a0(13,\u00a03)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u258f \u00a0CTRL+B\u00a0\u00a0Sidebar\u00a0\u00a0CTRL+T\u00a0\u00a0Toggle\u00a0Dark\u00a0mode\u00a0\u00a0CTRL+S\u00a0\u00a0Screenshot\u00a0\u00a0F1\u00a0\u00a0Notes\u00a0\u00a0CTRL+Q\u00a0\u00a0Quit\u00a0

"},{"location":"getting_started/#examples","title":"Examples","text":"

The Textual repository comes with a number of example apps. To try out the examples, first clone the Textual repository:

HTTPSSSHGitHub CLI
git clone https://github.com/Textualize/textual.git\n
git clone git@github.com:Textualize/textual.git\n
gh repo clone Textualize/textual\n

With the repository cloned, navigate to the /examples/ directory where you will find a number of Python files you can run from the command line:

cd textual/examples/\npython code_browser.py ../\n
"},{"location":"getting_started/#need-help","title":"Need help?","text":"

See the help page for how to get help with Textual, or to report bugs.

"},{"location":"help/","title":"Help","text":"

If you need help with any aspect of Textual, let us know! We would be happy to hear from you.

"},{"location":"help/#bugs-and-feature-requests","title":"Bugs and feature requests","text":"

Report bugs via GitHub on the Textual issues page. You can also post feature requests via GitHub issues, but see the roadmap first.

"},{"location":"help/#help-with-using-textual","title":"Help with using Textual","text":"

You can seek help with using Textual in the discussion area on GitHub.

"},{"location":"help/#discord-server","title":"Discord Server","text":"

For more realtime feedback or chat, join our discord server to connect with the Textual community.

"},{"location":"roadmap/","title":"Roadmap","text":"

We (textualize.io) are actively building and maintaining Textual.

We have many new features in the pipeline. This page will keep track of that work.

"},{"location":"roadmap/#features","title":"Features","text":"

High-level features we plan on implementing.

  • Accessibility
    • Integration with screen readers
    • Monochrome mode
    • High contrast theme
    • Color-blind themes
  • Command palette
    • Fuzzy search
  • Configuration (.toml based extensible configuration format)
  • Console
  • Devtools
    • Integrated log
    • DOM tree view
    • REPL
  • Reactive state abstraction
  • Themes
    • Customize via config
    • Builtin theme editor
"},{"location":"roadmap/#widgets","title":"Widgets","text":"

Widgets are key to making user-friendly interfaces. The builtin widgets should cover many common (and some uncommon) use-cases. The following is a list of the widgets we have built or are planning to build.

  • Buttons
    • Error / warning variants
  • Color picker
  • Checkbox
  • Content switcher
  • DataTable
    • Cell select
    • Row / Column select
    • API to update cells / rows
    • Lazy loading API
  • Date picker
  • Drop-down menus
  • Form Widget
    • Serialization / Deserialization
    • Export to attrs objects
    • Export to PyDantic objects
  • Image support
    • Half block
    • Braille
    • Sixels, and other image extensions
  • Input
    • Validation
    • Error / warning states
    • Template types: IP address, physical units (weight, volume), currency, credit card etc
  • Select control (pull-down)
  • Markdown viewer
    • Collapsible sections
    • Custom widgets
  • Plots
    • bar chart
    • line chart
    • Candlestick chars
  • Progress bars
    • Style variants (solid, thin etc)
  • Radio boxes
  • Spark-lines
  • Switch
  • Tabs
  • TextArea (multi-line input)
    • Basic controls
    • Indentation guides
    • Smart features for various languages
    • Syntax highlighting
"},{"location":"tutorial/","title":"Tutorial","text":"

Welcome to the Textual Tutorial!

By the end of this page you should have a solid understanding of app development with Textual.

Quote

If you want people to build things, make it fun.

\u2014 Will McGugan (creator of Rich and Textual)

"},{"location":"tutorial/#video-series","title":"Video series","text":"

This tutorial has an accompanying video series which covers the same content.

"},{"location":"tutorial/#stopwatch-application","title":"Stopwatch Application","text":"

We're going to build a stopwatch application. This application should show a list of stopwatches with buttons to start, stop, and reset the stopwatches. We also want the user to be able to add and remove stopwatches as required.

This will be a simple yet fully featured app \u2014 you could distribute this app if you wanted to!

Here's what the finished app will look like:

stopwatch.py \u2b58StopwatchApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Stop\u00a000:00:02.01 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Stop\u00a000:00:10.09 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Stop\u00a000:00:06.07 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0\u00a0A\u00a0\u00a0Add\u00a0\u00a0R\u00a0\u00a0Remove\u00a0

"},{"location":"tutorial/#get-the-code","title":"Get the code","text":"

If you want to try the finished Stopwatch app and follow along with the code, first make sure you have Textual installed then check out the Textual repository:

HTTPSSSHGitHub CLI
git clone https://github.com/Textualize/textual.git\n
git clone git@github.com:Textualize/textual.git\n
gh repo clone Textualize/textual\n

With the repository cloned, navigate to docs/examples/tutorial and run stopwatch.py.

cd textual/docs/examples/tutorial\npython stopwatch.py\n
"},{"location":"tutorial/#type-hints-in-brief","title":"Type hints (in brief)","text":"

Tip

Type hints are entirely optional in Textual. We've included them in the example code but it's up to you whether you add them to your own projects.

We're a big fan of Python type hints at Textualize. If you haven't encountered type hinting, it's a way to express the types of your data, parameters, and return values. Type hinting allows tools like mypy to catch bugs before your code runs.

The following function contains type hints:

def repeat(text: str, count: int) -> str:\n\"\"\"Repeat a string a given number of times.\"\"\"\nreturn text * count\n

Parameter types follow a colon. So text: str indicates that text requires a string and count: int means that count requires an integer.

Return types follow ->. So -> str: indicates this method returns a string.

"},{"location":"tutorial/#the-app-class","title":"The App class","text":"

The first step in building a Textual app is to import and extend the App class. Here's a basic app class we will use as a starting point for the stopwatch app.

stopwatch01.py
from textual.app import App, ComposeResult\nfrom textual.widgets import Header, Footer\nclass StopwatchApp(App):\n\"\"\"A Textual app to manage stopwatches.\"\"\"\nBINDINGS = [(\"d\", \"toggle_dark\", \"Toggle dark mode\")]\ndef compose(self) -> ComposeResult:\n\"\"\"Create child widgets for the app.\"\"\"\nyield Header()\nyield Footer()\ndef action_toggle_dark(self) -> None:\n\"\"\"An action to toggle dark mode.\"\"\"\nself.dark = not self.dark\nif __name__ == \"__main__\":\napp = StopwatchApp()\napp.run()\n

If you run this code, you should see something like the following:

stopwatch01.py \u2b58StopwatchApp \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0

Hit the D key to toggle between light and dark mode.

stopwatch01.py \u2b58StopwatchApp \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0

Hit Ctrl+C to exit the app and return to the command prompt.

"},{"location":"tutorial/#a-closer-look-at-the-app-class","title":"A closer look at the App class","text":"

Let's examine stopwatch01.py in more detail.

stopwatch01.py
from textual.app import App, ComposeResult\nfrom textual.widgets import Header, Footer\nclass StopwatchApp(App):\n\"\"\"A Textual app to manage stopwatches.\"\"\"\nBINDINGS = [(\"d\", \"toggle_dark\", \"Toggle dark mode\")]\ndef compose(self) -> ComposeResult:\n\"\"\"Create child widgets for the app.\"\"\"\nyield Header()\nyield Footer()\ndef action_toggle_dark(self) -> None:\n\"\"\"An action to toggle dark mode.\"\"\"\nself.dark = not self.dark\nif __name__ == \"__main__\":\napp = StopwatchApp()\napp.run()\n

The first line imports the Textual App class, which we will use as the base class for our App. The second line imports two builtin widgets: Footer which shows a bar at the bottom of the screen with bound keys, and Header which shows a title at the top of the screen. Widgets are re-usable components responsible for managing a part of the screen. We will cover how to build widgets in this tutorial.

The following lines define the app itself:

stopwatch01.py
from textual.app import App, ComposeResult\nfrom textual.widgets import Header, Footer\nclass StopwatchApp(App):\n\"\"\"A Textual app to manage stopwatches.\"\"\"\nBINDINGS = [(\"d\", \"toggle_dark\", \"Toggle dark mode\")]\ndef compose(self) -> ComposeResult:\n\"\"\"Create child widgets for the app.\"\"\"\nyield Header()\nyield Footer()\ndef action_toggle_dark(self) -> None:\n\"\"\"An action to toggle dark mode.\"\"\"\nself.dark = not self.dark\nif __name__ == \"__main__\":\napp = StopwatchApp()\napp.run()\n

The App class is where most of the logic of Textual apps is written. It is responsible for loading configuration, setting up widgets, handling keys, and more.

Here's what the above app defines:

  • BINDINGS is a list of tuples that maps (or binds) keys to actions in your app. The first value in the tuple is the key; the second value is the name of the action; the final value is a short description. We have a single binding which maps the D key on to the \"toggle_dark\" action. See key bindings in the guide for details.

  • compose() is where we construct a user interface with widgets. The compose() method may return a list of widgets, but it is generally easier to yield them (making this method a generator). In the example code we yield an instance of each of the widget classes we imported, i.e. Header() and Footer().

  • action_toggle_dark() defines an action method. Actions are methods beginning with action_ followed by the name of the action. The BINDINGS list above tells Textual to run this action when the user hits the D key. See actions in the guide for details.

stopwatch01.py
from textual.app import App, ComposeResult\nfrom textual.widgets import Header, Footer\nclass StopwatchApp(App):\n\"\"\"A Textual app to manage stopwatches.\"\"\"\nBINDINGS = [(\"d\", \"toggle_dark\", \"Toggle dark mode\")]\ndef compose(self) -> ComposeResult:\n\"\"\"Create child widgets for the app.\"\"\"\nyield Header()\nyield Footer()\ndef action_toggle_dark(self) -> None:\n\"\"\"An action to toggle dark mode.\"\"\"\nself.dark = not self.dark\nif __name__ == \"__main__\":\napp = StopwatchApp()\napp.run()\n

The final three lines create an instance of the app and calls the run() method which puts your terminal in to application mode and runs the app until you exit with Ctrl+C. This happens within a __name__ == \"__main__\" block so we could run the app with python stopwatch01.py or import it as part of a larger project.

"},{"location":"tutorial/#designing-a-ui-with-widgets","title":"Designing a UI with widgets","text":"

Textual comes with a number of builtin widgets, like Header and Footer, which are versatile and re-usable. We will need to build some custom widgets for the stopwatch. Before we dive in to that, let's first sketch a design for the app \u2014 so we know what we're aiming for.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nNVcXGlP40pcdTAwMTb93r9cdTAwMDIxX2akxq/2paXRqFx1MDAwM4R9XHUwMDBiW8PMXHUwMDEzMrFDXGbxguNA4Kn/+5SddOwktuM4S/tFLVx1MDAxYVxcjuu66tx77lJVf33Z2NhcZj48c/PbxqbZb+pcdTAwMWTL8PX3za/h9TfT71quo5pQ9HfX7fnN6M52XHUwMDEweN1vf/xh6/6LXHUwMDE5eFx1MDAxZL1pam9Wt6d3ukHPsFxcrenaf1iBaXf/XHUwMDEz/jzVbfPfnmtcdTAwMWKBr8WdbJmGXHUwMDE1uP6gL7Nj2qZcdTAwMTN01dP/q/7e2Pgr+pmQzjebge48dczoXHUwMDBiUVMsIFx1MDAwNmzy6qnrRMJcbi5cdTAwMDWFXGaR0VxyVndHdVx1MDAxN5iGam0pkc24Jby0+aizxo/G/VPr6uX83Hu/ueqSPo97bVmdzmXw0Ymk6rrqZeK2buC7L+atZVx1MDAwNG3VXG4nrmd9y3d7T23H7HbHvuN6etNcbj7Ca1x1MDAwMIyuXHUwMDBlhuDbRnylXHUwMDFmTlx1MDAxMCdcdTAwMWFkXHUwMDAwUoboqCH8KpJcXCOEYpi4Plx1MDAxMGbb7ajBV8L8XHUwMDAzRJ9YnEe9+fKkZHKM+Fx1MDAxZVx1MDAwMpqAJlx1MDAwNuF9+IqqQ1xyy4lcdTAwMGXapvXUXHUwMDBlXHUwMDA2gmtcdTAwMWNcbp7o24yGXHUwMDFkXCLEOUFcYqNRS9ijd2BEXGL4c3Lg2rrvXHJcdTAwMDdos1x1MDAxYv6RkDZcdTAwMTR0d1x1MDAxMj5JXGIlpvag2/C2m5+HRzdcdTAwMTf1t7uL8114vnc4etZcdTAwMTjedN933zdHLT+Hv8Wi9TxDXHUwMDFmgFxiMlx1MDAwNiRcdTAwMTaSXHUwMDEwgmNcdTAwMWN2LOdFNTq9Tie+5jZfYtxFV39+LYF3gmBcdTAwMTbeJVx1MDAxNYJQSorj3XrEnty37P7JsSlcdTAwMWT97NV7/VGvON5cdTAwMDXRXHUwMDA0wXxcdTAwMWPsXHUwMDE4YlxyYyqT18uAvaVTRNE02JWGTWOcsSlwc8FcdTAwMDRUUCDrXHUwMDAy9y/MXHUwMDA0Zj9cdTAwMThH82CG67KPb+3+/bFcclx1MDAwZnZunFx1MDAwN3v/sL01XHUwMDE3tlx1MDAxOVx1MDAwNVxigWVhe0zOYmZcdTAwMWMqXHUwMDExXHUwMDE4R1x1MDAxY4nCuE5/63RcXLf1Zrvnm1VAtkxDNlZ4X1x1MDAxY9mBrztdT/dcdTAwMTWaUtBNU9CN8LTpJlAqky9WgO5lXHUwMDAyMJ5n11x0Lq3PcKhcdTAwMTNcdTAwMGZcYq/WddvqfIxNVYRMJell4CZcdTAwMDXVu6bqMcIhXHUwMDFmu/d7x3pcbpG72VTvYPpjoFx1MDAwZSzl6oxusC3DSFx1MDAxYfOmXHUwMDEyQFfP9Fx1MDAwZopcdTAwMThh17eeLEfvXFwl5SvPXHUwMDFmjMlM/lAug0SCo8J65lx1MDAxY2/3t06R6Fx1MDAxY8BG42brXHUwMDAzXFw1/I9q81x1MDAwN2NEXHUwMDAzWIJJd4lgpPRcZoGF/aWmaVx1MDAxMEMvSiFiSskgR8q1XHUwMDEyZG1cdTAwMTQymMpeXHUwMDFmn+zsideL9zuj9tK6sE9/bDfS/aNIU2Jcbvma/thZzJTeYXFmXHUwMDEyWPmXWMaTtVwiZqJcdOM4yUycXHUwMDAzyVxiT8zsLI3JXHUwMDFm5ooyk7JcdTAwMTnpOkOp8rpcdTAwMTbXmaWQXHUwMDEzXHUwMDA3XGIoXHUwMDFkS7jhqyenXHUwMDEyXHUwMDE4XFyMnFx1MDAxYWbXXGbWyk4zTPwkO1xyXHUwMDA0LE9PMGFcdTAwMTEntI1wXGKQXHUwMDE0rHh4c/zs9smja183ru5Ojp73jWtcdTAwMTI0q01PhCtVo2Lc34v8QMQ1oMhhPKYuXHUwMDEz4kSfND3DXHUwMDFhnExcdTAwMTiMXHUwMDE0XHUwMDBlU02OO6dDveMq5FRcdTAwMTaArkDtyvHKx0n/ljx8Xlx1MDAwN96xW/NZv+18st1cbkY8kmRcdTAwMDFcdTAwMWQyjpVZ4ax4wJP+0lx1MDAxNadcdTAwMTVcdTAwMTVIZGBd+WJ0knCWzitcdTAwMDRPwzyFV6hcdTAwMDQq5GGr8MeqXHUwMDEz9Fx1MDAwMPAt/Mc1XHUwMDA018ouMyz0JLskxSzPMVhmptCgQFxicVxuOSusende//LkZGf39uiqVoeg3Ttv+8e/k2RwkZQxXHUwMDA1krOpnDGBQmOccrGqIIgypnHO8Vx1MDAxOJOMJY1cdTAwMDVcdTAwMWRPZY/CXCJcdTAwMDRcdTAwMTX/XHUwMDEwsVYtXHUwMDE0XHUwMDEwYonm0MLyoKRcdTAwMTRngpJcdTAwMTPJkVx1MDAwNKQ4XHUwMDFmyGOhO/unXHKf1M97bbttXl1cdTAwMWZUvZAhqFx1MDAwNlx1MDAwNaWEXHQ+XHUwMDE5mlx1MDAxM00uI7ubVcoomt1ccrP8gvLEq6wlNP++3Xpr2O32XHUwMDE2Mz6fejY4erg74uOuz3JD8/RcdTAwMGWr50IpK5KlM0x5XHUwMDBmXHUwMDAwwDlcXKj8Ua6oXHUwMDBipaxCltJQppElKM1yYnMsXHUwMDAwXHUwMDEwfFx1MDAxNTW/6vhQl4Hurzc2n2HlpzPHoYBcdTAwMGL4TSiborCQiOE5XHUwMDEyYW7t5NW8QPzItnuNeqv/dnyB33+vuvFcIsE5XHUwMDAxqZkwXCKVT1xuVqxspVx1MDAwMnRKwuhFrrve/iDfL8+O/Ju9k7bVMPZcdTAwMGZcdTAwMDS+P2uvkrTSO6xcdTAwMWVpIZpZgFGBP8CcUFA8xZU/zFx1MDAxNVUjXHUwMDE192eoXHUwMDExpVx1MDAxYV9xOrlY2C9cdFx1MDAxMpJSuM5s8lx1MDAxMIFwXHUwMDBlXHUwMDA0LsZYw3BcdTAwMWFo41x1MDAwM7py3pph+zOi/kjM8uyFWKazKFx1MDAxOEZcdTAwMDBcdTAwMTFQ3Fl8RkivfaKzXHUwMDFh3vGI34JXd82b/d9cdTAwMTlfkdlBP9Mog1x1MDAxNOGpJVx1MDAwNsroaVx1MDAwYkf8Lf1cdTAwMTFcdTAwMDCaXHUwMDE28XNccipdXHUwMDFh0/WR4il9R1igsPeoXHUwMDBmOKmHXGJcdTAwMTBlXHUwMDEwMV1fOXRcdTAwMTbP6PpcdTAwMTXbu7o/OFx1MDAwMSe39nZw+eRcdTAwMWScXs63WkxIXHRji7NcIp6BIDvBLJByJOZYXHUwMDE3mf7O6XBv+m63u9XWg2b794NewEzQh1x1MDAwZVx1MDAxM4aMrDTJTCmfXHUwMDA2fUqAxCRcdTAwMDJUKcl6k8xzXHUwMDAzcTG62Td1I0lcdTAwMWJrYJpcdTAwMTl2epJphlx1MDAxMpYnXHUwMDE5XG4zSVx1MDAwNlwiyKOca2Gtc+y3k8tcdTAwMTf48Irk+71OwFx1MDAxYjx9LpVZRkvSNzpb34CGXGKcjIRcIq9cdTAwMTAqfWNkXCJEKUE0XGKJR5OlXHUwMDExXHKZVjWcsohcciNcdTAwMTLSUWVcYuU2QPrdzsOWj/pta5/dP+3hXHUwMDA3ez5CkYrd4/dZVeDCM0vzkGEogUCgePyf/ta/nVJcbkCcZkJcXPlRy4D4XGZGSYF5SvxcdTAwMTKuhyFcdTAwMDCtmVDmXHUwMDA14mKEUnfdYM2EMsMmT1x1MDAxMspQwkVcYiUn56ZAiOZSujP60NxvdY1cdTAwMWYt1Kyf39qn/aB5U/FaJdOYXHUwMDEwJKVYSTHXplLf1ShWXCLMJcOYraBamc4xw1x1MDAxMsZcdTAwMWU7O3uxrf7lNjk6291BXHUwMDFl+Vx1MDAxMOk5t1x1MDAxMntcXFx1MDAxMGF8PbVQxrM3XHUwMDAzIE64ilx1MDAwN+fINNvPp/a2d3x0dF4zOvt35lPt8sWpei2Ua5hcdTAwMTE0UYn/XHUwMDE42H9ccirVXzhgX7RcdTAwMTgqiFRTQcSai6E7XHI9eDyy2s9cdTAwMTdcdTAwMTf711x1MDAwZl3y2q5jP1x1MDAxZOPF8soreuwsry+9w3lcdTAwMTRS4DCHumqvj+akq7lcdTAwMDBcdTAwMTBcdFx1MDAwNouHNPnDXFzZXCIrytRGTjW2XHUwMDA0bVxcSpVcdTAwMTVcYkokoGtdXHUwMDAxXVx1MDAwMoaL+Xzrr7LO4I9lV1lcdMkmP4wogpTC4jt0asfXzkO7Z+yC163vV/XWNdjln5WvXHUwMDBmQY1yXHRTNjVTJrRVr1x1MDAwYi1XZoWIXHUwMDAxiClfRVxuL4+4LpqH6PFwz3Vfnlxm9/Ni79Opi93F+XDpj53Fh+lcdTAwMWRWj1x1MDAwZrHIXFw8ijBcdTAwMDVcXMGmeFx1MDAxOSl/lKuqnSxTOznRhFgxXHUwMDE5XHUwMDE2q98qXHUwMDE2RGqq1rxVdc1cXPi76rczSKV0/TYz84hpls5cdTAwMTGBJZtrb3jt7HnLe39/dPmLrd95dYs9YiND59aSVZ/tgEpGNSogSFnmR2G4LUjmXHUwMDFm84FcdNZJ2W2rXHUwMDAwpejbXHUwMDE0/SGiXGaCSM5D1TNcdTAwMWW59Cdr37dcdTAwMGbPXHUwMDFmxPfDmjh8faa017r/sbRMXG6lXHUwMDAwr6/sXHUwMDE2blx1MDAxOH9cdTAwMWbPnSdthFx1MDAxOPvCyFx1MDAwNHTMVpBjIcY2yY+bh7FcdTAwMTdJ270+XHUwMDEwJtdcdTAwMTJcZlx1MDAwNjWNfrOPP1FcdTAwMDAkmPB5zvvJn+Zq2lx1MDAwMlxuNFxmXHUwMDA1Y1x1MDAwMmMsMJw0XHUwMDA3QoOYhVx1MDAwYiZouFtyNTaBizBcdTAwMDWFZbhcdTAwMWSR8OSBXHUwMDAx8cJcdTAwMGWoQYSEomGGpVx1MDAwMvykvVAuNCfhSq/57UUkZNn6hGCiVH2iXHUwMDFiRnU1yzEs50k1xibj1/lVXHUwMDA3XHUwMDA1yCXS1WYvlHJcdTAwMGJoTKrRXHUwMDEzMFxc8IeT5yGFY6F7USSiodCLYThcdTAwMWNGQeTwhpHp2jRcdTAwMWQjlmn8NfRusO3atlx1MDAxNahcdTAwMDE4dy0nmLwjeqPvoZ61TX1Kb9WTk22TXG7phU9cdTAwMWO3zvFvXHUwMDFiMWSjP0a///k19e6tTDxFrZNQilx1MDAxZvcl+f/cplx1MDAwNKHszJZEUir9wsUzW/lMVFFTwjV1V9pcdTAwMGVMisP1zFxmQ45BaFNXYkaksmScyF9cdTAwMWaeslSGKFx0XHUwMDE5XHUwMDAyXHUwMDEw8TCyXHUwMDAyaOq4XGbVICVgskTOa1x1MDAxMTtCldqW8vOXbUeApiw9UapcdTAwMDEg41xckZ9I3DQwI0IjXHUwMDAyRJ5jvv3IkiW/aDgmXHUwMDBilzzccqIgxaREdFpcdTAwMTaENUSHO1x1MDAwZqek+VvZrEzwhp8p2M5pszKTXHUwMDBmXHS+nNzwhChnhOHiqcGjz7u911Zw8456jkktv9WA9duKWywsNJZusZQnpFx0wfLPR1gsXHUwMDEwimWO3ZxYvtFSPlx1MDAwZVx1MDAxMFwiZM1lsVx1MDAwNVx1MDAwMpbcQGhVXHUwMDAx1mqPY1x1MDAxNJSBeUrVS1xusP7n/DOyUKbxr9RYK5G6mi9cdTAwMWazcLSVlKyct4SzXHUwMDE3okjCwpNw5ti1kj/7XHUwMDE1NT1UxV2/llx1MDAxMidXqlx1MDAwZk6skJpiWIakcth5cqf2Mk1cdTAwMTBcdTAwMTOaXHUwMDE4nLhcdTAwMWFcdTAwMTGPiD2QODOjUVxuOFx1MDAwZuXkTFwiknagXHUwMDA1XHUwMDA2UGBaJlVT5dArn87G3Fx1MDAxNFxiiVQ2XHUwMDFhcCYoJ3FcYj1yU5SXMtixkO8x/X0jrkwkhZ9pXGbN6b7k1jd5zuJcdTAwMWWoaF1IJIpnc9+E3fQ+3G6999yRXHUwMDA3dv2cg+1SlmSdp6lRXHJcdTAwMDCkQszpbC5ccjclrGg5W9FcdTAwMDNcYjFV1mE1S0fzPILT21OvfaVcdTAwMDb70rm+wVcnwdbxoZ3uXHUwMDExzFPIXFz6Y2dcdTAwMTUy0zss7r4oJlxy/cd5dlx1MDAwMeYqY1YskdyJMJX+4FhKguY4ai1/mCu6sEe56pmayFXYXFyBo1x1MDAxM1x1MDAxMMRcZofnXHUwMDExrfPgm1x1MDAxMlx1MDAxMFxczINe/7GGM3gj71jDL0Pl3dQ97zJQ4zZyStTUWMbw5eOx2nyzzPda9kl8X4a6XHUwMDFiKolcdTAwMTlOzF8/v/z8P1xiXCJcdTAwMWT6In0= StopReset00:00:07.21Start00:00:00.00HeaderFooterStart00:00:00.00StopwatchStopwatch(started)Reset"},{"location":"tutorial/#custom-widgets","title":"Custom widgets","text":"

We need a Stopwatch widget composed of the following child widgets:

  • A \"Start\" button
  • A \"Stop\" button
  • A \"Reset\" button
  • A time display

Textual has a builtin Button widget which takes care of the first three components. All we need to build is the time display widget which will show the elapsed time and the stopwatch widget itself.

Let's add those to the app. Just a skeleton for now, we will add the rest of the features as we go.

stopwatch02.py
from textual.app import App, ComposeResult\nfrom textual.containers import ScrollableContainer\nfrom textual.widgets import Button, Footer, Header, Static\nclass TimeDisplay(Static):\n\"\"\"A widget to display elapsed time.\"\"\"\nclass Stopwatch(Static):\n\"\"\"A stopwatch widget.\"\"\"\ndef compose(self) -> ComposeResult:\n\"\"\"Create child widgets of a stopwatch.\"\"\"\nyield Button(\"Start\", id=\"start\", variant=\"success\")\nyield Button(\"Stop\", id=\"stop\", variant=\"error\")\nyield Button(\"Reset\", id=\"reset\")\nyield TimeDisplay(\"00:00:00.00\")\nclass StopwatchApp(App):\n\"\"\"A Textual app to manage stopwatches.\"\"\"\nBINDINGS = [(\"d\", \"toggle_dark\", \"Toggle dark mode\")]\ndef compose(self) -> ComposeResult:\n\"\"\"Create child widgets for the app.\"\"\"\nyield Header()\nyield Footer()\nyield ScrollableContainer(Stopwatch(), Stopwatch(), Stopwatch())\ndef action_toggle_dark(self) -> None:\n\"\"\"An action to toggle dark mode.\"\"\"\nself.dark = not self.dark\nif __name__ == \"__main__\":\napp = StopwatchApp()\napp.run()\n

We've imported two new widgets in this code: Button, which creates a clickable button, and Static which is a base class for a simple control. We've also imported ScrollableContainer from textual.containers which (as the name suggests) is a Widget which contains other widgets.

We've defined an empty TimeDisplay widget by extending Static. We will flesh this out later.

The Stopwatch widget class also extends Static. This class has a compose() method which yields child widgets, consisting of three Button objects and a single TimeDisplay object. These widgets will form the stopwatch in our sketch.

"},{"location":"tutorial/#the-buttons","title":"The buttons","text":"

The Button constructor takes a label to be displayed in the button (\"Start\", \"Stop\", or \"Reset\"). Additionally, some of the buttons set the following parameters:

  • id is an identifier we can use to tell the buttons apart in code and apply styles. More on that later.
  • variant is a string which selects a default style. The \"success\" variant makes the button green, and the \"error\" variant makes it red.
"},{"location":"tutorial/#composing-the-widgets","title":"Composing the widgets","text":"

To add widgets to our application we first need to yield them from the app's compose() method:

The new line in StopwatchApp.compose() yields a single ScrollableContainer object which will create a scrolling list of stopwatches. When classes contain other widgets (like ScrollableContainer) they will typically accept their child widgets as positional arguments. We want to start the app with three stopwatches, so we construct three Stopwatch instances and pass them to the container's constructor.

"},{"location":"tutorial/#the-unstyled-app","title":"The unstyled app","text":"

Let's see what happens when we run stopwatch02.py.

stopwatch02.py \u2b58StopwatchApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Stop\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Reset\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 00:00:00.00 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Stop\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2586\u2586 \u00a0Reset\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 00:00:00.00 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a0 \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0

The elements of the stopwatch application are there. The buttons are clickable and you can scroll the container but it doesn't look like the sketch. This is because we have yet to apply any styles to our new widgets.

"},{"location":"tutorial/#writing-textual-css","title":"Writing Textual CSS","text":"

Every widget has a styles object with a number of attributes that impact how the widget will appear. Here's how you might set white text and a blue background for a widget:

self.styles.background = \"blue\"\nself.styles.color = \"white\"\n

While it's possible to set all styles for an app this way, it is rarely necessary. Textual has support for CSS (Cascading Style Sheets), a technology used by web browsers. CSS files are data files loaded by your app which contain information about styles to apply to your widgets.

Info

The dialect of CSS used in Textual is greatly simplified over web based CSS and much easier to learn.

CSS makes it easy to iterate on the design of your app and enables live-editing \u2014 you can edit CSS and see the changes without restarting the app!

Let's add a CSS file to our application.

stopwatch03.py
from textual.app import App, ComposeResult\nfrom textual.containers import ScrollableContainer\nfrom textual.widgets import Button, Footer, Header, Static\nclass TimeDisplay(Static):\n\"\"\"A widget to display elapsed time.\"\"\"\nclass Stopwatch(Static):\n\"\"\"A stopwatch widget.\"\"\"\ndef compose(self) -> ComposeResult:\n\"\"\"Create child widgets of a stopwatch.\"\"\"\nyield Button(\"Start\", id=\"start\", variant=\"success\")\nyield Button(\"Stop\", id=\"stop\", variant=\"error\")\nyield Button(\"Reset\", id=\"reset\")\nyield TimeDisplay(\"00:00:00.00\")\nclass StopwatchApp(App):\n\"\"\"A Textual app to manage stopwatches.\"\"\"\nCSS_PATH = \"stopwatch03.tcss\"\nBINDINGS = [(\"d\", \"toggle_dark\", \"Toggle dark mode\")]\ndef compose(self) -> ComposeResult:\n\"\"\"Create child widgets for the app.\"\"\"\nyield Header()\nyield Footer()\nyield ScrollableContainer(Stopwatch(), Stopwatch(), Stopwatch())\ndef action_toggle_dark(self) -> None:\n\"\"\"An action to toggle dark mode.\"\"\"\nself.dark = not self.dark\nif __name__ == \"__main__\":\napp = StopwatchApp()\napp.run()\n

Adding the CSS_PATH class variable tells Textual to load the following file when the app starts:

stopwatch03.tcss
Stopwatch {\nlayout: horizontal;\nbackground: $boost;\nheight: 5;\nmargin: 1;\nmin-width: 50;\npadding: 1;\n}\nTimeDisplay {\ncontent-align: center middle;\ntext-opacity: 60%;\nheight: 3;\n}\nButton {\nwidth: 16;\n}\n#start {\ndock: left;\n}\n#stop {\ndock: left;\ndisplay: none;\n}\n#reset {\ndock: right;\n}\n

If we run the app now, it will look very different.

stopwatch03.py \u2b58StopwatchApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a000:00:00.00\u00a0Reset\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a000:00:00.00\u00a0Reset\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a000:00:00.00\u00a0Reset\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0

This app looks much more like our sketch. Let's look at how Textual uses stopwatch03.tcss to apply styles.

"},{"location":"tutorial/#css-basics","title":"CSS basics","text":"

CSS files contain a number of declaration blocks. Here's the first such block from stopwatch03.tcss again:

Stopwatch {\nlayout: horizontal;\nbackground: $boost;\nheight: 5;\nmargin: 1;\nmin-width: 50;\npadding: 1;\n}\n

The first line tells Textual that the styles should apply to the Stopwatch widget. The lines between the curly brackets contain the styles themselves.

Here's how this CSS code changes how the Stopwatch widget is displayed.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXGlT20pcdTAwMTb9nl/hYuZj3Ol9SdXUXHUwMDE0mLCEsIUlycx7lVx1MDAxMpKwXHUwMDE1ZMuxZFx1MDAxNr/Kf58rQazd2MZcdTAwMDYz9VRcdTAwMTRcdTAwMTi1lqu+5/Q993bLf71pNNaiu7679r6x5t7alu85XHUwMDAz62btbbz/2lx1MDAxZIRe0IMmmvxcdTAwMWZcdTAwMDbDgZ1cdTAwMWPZiaJ++P7du641uHKjvm/ZLrr2wqHlh9HQ8Vx1MDAwMmRcdTAwMDfdd17kdsN/x79cdTAwMGasrvuvftB1olx1MDAwMUpv0nRcdTAwMWQvXG5cdTAwMDb393J9t+v2olx1MDAxMK7+X/i/0fgr+Z2xbuDakdVr+25yQtKUXHUwMDFhKDUt7j1cYnqJsUxcdTAwMWLKJNVkfIBcdTAwMTduwu1cIteB1ksw2U1b4l1rp+fnV527b8cnrfbt94D7XHUwMDFi3uczkt710vP9k+jOT6xcblx1MDAwM3iYtC2MXHUwMDA2wZX7xXOiXHUwMDBltJLC/rqzXHUwMDA2wbDd6blhmDsn6Fu2XHUwMDE33cWPgMc773vgfSPdc1x1MDAxYvvHMIQ1M5ozJcYt8amUcyR1Zue9Ja3Ah55cdTAwMDdL/oGTLbXlwrKv2mBQz0mPsV2HO1Z6zM3D81x0KZFSiuXu2nG9dieKn1x1MDAwNGOkXHUwMDA1XHUwMDExkmbu7ia9ToRmUnKJ06eN79nfdVx1MDAxMlx1MDAwMPyZ7Zme89AzvaHvp2bGXHJcdTAwMWaKoMlcdTAwMDIn41Duj1pHN44z3Fx1MDAxMteYbOtmv+tvjVx1MDAxZieHMmswXGJu1sYtv1x1MDAxZT6lXHUwMDE2XHL7jnVcdTAwMGZcdTAwMWRcIiU2WmAhXHUwMDE11eN23+tdXHUwMDE1jfVcdTAwMDP7KkVbsvfX2zlQrjNAKKCcXHUwMDFiXGZcdTAwMDBQxkyN8rsvx1x1MDAxZlpmt7l7sVx1MDAxZH36XHUwMDFlXHUwMDA1m+vfv929JMpcdTAwMDEvj8CcYYaUMErmMJXAXFxiZLhghD5cculcdTAwMWPbWKgy0onEZYBLWcJ1XGZcYqYwXHUwMDExz4LrT+vOZcft2ups+1x1MDAwN93fOT883j2+rcZ15N5GL1xi6+T2XHUwMDE1iFaE1CFagT9h5FJ8akRP7o48ojuW3Vx1MDAxOVx1MDAwZdxcdTAwMTXAtOCIVmNaU8SfjuloYPXCvjVcdTAwMDBAVYzgplxma8pKsNaEY8IlN1x1MDAwYoN1NfI0JopzXHUwMDA2XHUwMDAxxcyAvNTDQS868UZxJ1Oc27tldT3/LuekXHUwMDA0ktA/J5E1iLI9XHUwMDE4unDL+Fx1MDAxYUTlXHUwMDBlXve9dozZNVx1MDAxYlx1MDAxZcJcdTAwMWTk4Fx1MDAxY3mgasZcdTAwMDd0PcfJjuA2WGDBNVx1MDAwN7vTjLzBwGt7Pcs/zVx1MDAxOTh/1JC4NmpcdTAwMDA8qVx1MDAxNJRjNTXJLs9bR82D/Vx1MDAxZnZcdTAwMTieXvMvrattdui9LMnUY1x1MDAxY+NGIaNAbpiSPFJcdTAwMDJRSvPkWzTJiGCIXHUwMDE0mTymXHUwMDFiXHUwMDEzqKDbXHUwMDFlWCdcdTAwMDWEXHUwMDEyLtXiSDcplvxs/Tw7bNODjf6BXGZcdTAwMGZYf32fd9ZfVSxcdTAwMTGM1eFcXCjCmOQzxJLJ3bGaMFx1MDAxN1x1MDAwNNfBXFxcdTAwMTOkXGIvwGzRMKeqjO6KYIKxXHUwMDExhDC6OO3/SDDheFx1MDAwNug9LZhg/D75QfmOXFx6SHlkVC6GlKyZ81x1MDAwN1x1MDAxNsNqxZvkXFxRo9X06VxiiUbHwaXeXHUwMDEyx9d476K5c9lum9FqpyNcdTAwMTJYVZlzS4qUNE/PRury7upsRJeYJsBcYmJohoPLjCBk3TlmXHUwMDAz22/rI4b90Z41+mzvT1x1MDAxNUHeTrrsK0re61wik8a6jijGKEO1Znp6okzs5lx1MDAxNc1yXHUwMDE0q6GKXHUwMDA2/bVcdTAwMDCqTFxmTFVsqYhLRIPWXHUwMDEykv8/xqXPbug+b5LzyHhejEj3XHUwMDA2zsUurmrDkCaKMqxcdJ6aXba5acqbj6PNnVx1MDAxZGNv9Jtfty82/VVnl2FcdTAwMDblKXTPLYPMMlx1MDAwNZ+syGbKtV5cbj4gYpFBaHVoJVx1MDAxYTGLwmpi6TmJXHUwMDE1XHUwMDA1/TpW5Vx1MDAxZaRIod/GTCTRfYisYpFcdTAwMTF1LDKGXG4lKZ9ezIU6tH7Y/fObs30pdLBcdTAwMWZcdTAwMWVccjtm5VlEXGZSxVpAQiRjXHUwMDEwgU4gT5xHmUgmjLjGPDdVMyZcdTAwMTU3SFx1MDAwYpFv/M0uzDCnRlxuNVx1MDAwM71SlfVcdTAwMWJcdTAwMTf0Yc+vWVk3n4rK9KI1iDa8nuP12sVT3J5T0+JbYdRcbrpdL1x1MDAwMjOOXHUwMDAyr1x1MDAxN1x1MDAxNY9IrrtcdTAwMWVjveNaJebAlbNtRVL04yvm5W76qZGiJvln/PnPt5VHN8uOTXZnfJpe4k3278xcdTAwMTRcdTAwMTZEXHUwMDE2945cdTAwMGJ9XHUwMDFjc1x1MDAwZVx1MDAxNJ9+XHUwMDEyNNjb/EG2etf+9o9j94BcdTAwMGbZXHUwMDA3vi9Xn8JcdTAwMTKp4pzj/VxmqkHw/MulMEFwXHUwMDAzrFx1MDAxNIa7S1xuv1N3pFTGSFx1MDAxMlx1MDAwM4iQ4H2IjCVcdTAwMTXKXHUwMDE4xEqhZyq1/83n5+NzvZPjrejeXHUwMDE5qZ1Ih1xuZoO2q2W2XHUwMDExJp5Mz1x1MDAxND9cdTAwMWaj9lx1MDAwNiNcdTAwMWJH+ESwy+1u6+Y/ncNR3/+48tRcdTAwMDZcdTAwMDZcdTAwMGLMXGYuRWcuXHUwMDExlpLjZZbwKYabK11ZwIemglnjSifXXHUwMDE0XHUwMDAw8vJcXJ5rNm0luJxrWyiRyy5NTvvtzFx1MDAwNTGXitpJXHSipJEwWkwvq+Uu3zsyh7fru4e7QeeL+DB0jtXqXHUwMDEzVyFVZEhcdTAwMTKTXHUwMDA1RphcdTAwMTZWXHUwMDBmLZy4ulxcX0qZW2IsXHUwMDAxLa20kS+upv9mbPnoXG5fXHUwMDE2zpuKqlx1MDAxM+c0uObFveNcdTAwMDUpkFx1MDAwNzKq9fRz5XpcdTAwMDN3j/btT9s3XHUwMDE3n4eR/rBtt1uXXHUwMDBi5qtjhVx1MDAxZHehhKVKIa6q8mAqXHUwMDEwK+Soi6/VUiSK0TxdUshcZsJJ0sRNsqW+eGCwwdhcdTAwMTBN8PMswFx1MDAxYfV1Z7RPvu58O1xuNvpdtqPWeVrkzKGuODfxdtJ1N7+FV2fOQXvnzmtcdTAwMWXuaOc6PO21XHUwMDE2Oucxy/gykU11VVmKa4lcdTAwMDTeoVxcSZmpOD3GpD1ztH973lx1MDAxYlx1MDAxZFx1MDAwN1tcdTAwMTY+3W3tnUaOs/pMMlx1MDAxYVx1MDAxOVMxXHUwMDFmXHUwMDBmXGJFy5asRFat7SqLVCmwoYw/05LFJzDmcWRcdTAwMTOuNGczIDtcdTAwMDXQPNXfTjDwRnF91m/41l0wrJlgqalcdTAwMDP77mWeOIupXHUwMDAyl42aSN/aYlx1MDAxMnRnXHUwMDFkf6VcdTAwMTJcdTAwMWNcdTAwMGJNplx1MDAwZoSTvb6iwpVcdTAwMWKGaHHRTHKqXHUwMDExyFx1MDAxMEKZXHUwMDAwclPOl8hhXCKRllx1MDAxNFBccsZcdTAwMDDCVVxu7kz6yVxihERcdTAwMDKySGtwSibbTFclXHUwMDBiaJJslpnNRata+EPFXFwzM1x1MDAwNVWbMezhpZHdKcRWwmZ7XHUwMDE4W9kkiEOPXHUwMDE4JrHmMZS1zlx1MDAxY9W2+snIjZhhXFxcdTAwMTCBwftClFx1MDAxZT0np59sXHUwMDEyQUxjqZigQihcdTAwMDXAqrZIYKyEhFxcimNOy+54VWW0WmTHW7NcZupcdTAwMTlFfq0sMfVcdTAwMGLOMSdEsVx1MDAxOfLxw+DO4k54ff1ldHeuw7NbbM72Vn1Yo0IjTngpXHUwMDFk58QgoYtLUVx1MDAxNz6gVa1cdTAwMTEsi1x1MDAxMiaJxmyRazFej9x+mighXHLb9f1G11x1MDAxYYAsWFx1MDAwNUGSN2g+MVwiMolegbWUKqownaH8PdnbK8paRlx1MDAxOeKCK+CEXHUwMDAxksr0ce+5S5ApTlx1MDAxZi/8XVx1MDAxMYyIVFxuXHUwMDEzXHUwMDA16TdE81xuLcI5wsVcdTAwMWH9mNLUyDjZmGVcdTAwMWTIypbW6lwi/uSIkFx1MDAxNSFcdTAwMThRKiBcdTAwMDVUgsVcdTAwMDE9M3c7XHUwMDBl+Vxu3aeOS1x1MDAxNlx1MDAxZnGsNYZwJjVcdTAwMDX8UJa+XHIyNoUhSVx1MDAxNNFcdTAwMTW2vCbRUYvgeGtmwLsgscFVffGfU1x1MDAxMtdcdTAwMTPN9Fx1MDAwYj+DT/3t06/d1sneh83RlzY3gm0uupq4+IWfXFwhSD1UbuI9XHUwMDE5skDQXHUwMDEyTJb84k06bTpcdTAwMWWjMmsgxiVcdTAwMTCl43l38zxqY1lcdTAwMGKa01x1MDAwNXDpczyT2uhbTjxcdTAwMWH90bOSLmpcXFxmoyjoVa+Ly1x1MDAxNGieZ13cIzbOp0iMqZ+Rh5QyjjczvFQ3XHUwMDE5XHUwMDEyK0ptjTWIXHUwMDBlI0g8y22yq3DvJYlGy13WTeM8XHUwMDFlayZULHziZVx1MDAxMGWySzBcdTAwMTGyXGKBQVx1MDAxOUllZEX5XHUwMDEz0ktqlHjR2lxi5UYuYr1NnVx1MDAxNphcdTAwMWM68rJcdTAwMDRzoaRkQlx1MDAwYkY1XHUwMDE1qVvHYoBgXHUwMDA0fVx1MDAxZft2PmUy+fs18taA3OVKa2ygg1x1MDAxNElfXHUwMDBiXHUwMDFlXHUwMDFiXHUwMDAzQZvGqVwieJFRzMTrLos060GdNJfwvCClQln9bI2iWlxiQXG6mu2x4cy/utz+aX8ynYNd7Xe3xGZw5lxmVn0441qh4mqEZCBjXHUwMDE0SUrwUpfSXHUwMDEzOZVS0VhcdTAwMTlccu5/nlx1MDAxN7q+hV/7srP58/bC73V+3uy3XHUwMDA33Y81Xy8xh1Dh0NnPJ1RShzTs2CN/9Lyw8c+LIFxiq2dtnl2lTDJwPolCSP1SXHUwMDA2oWFw4Wz696EnY2FFKS2kQFLG31x1MDAwYsMwXHUwMDAxzFx1MDAxNTNcdTAwMTCOlvviv0RExyNcdTAwMDcxXHUwMDFhXCKpIFx1MDAxNStcdTAwMWGMRIWv/lx1MDAxOL+8aSBlXHUwMDAyXHUwMDEx+ULC5IkknVKYTI5cdTAwMTRcdTAwMDVhXCJcdTAwMTTTWihKuMBgXHUwMDFlz1x1MDAxY/a7YFx1MDAwMsExKWBPXHUwMDE2Jq928XEtoOKtOcZSnSh483DhNavfP4nA1WPPXHUwMDAwtjznYVBNn27t2nNvNipeTL5MtnhcXEp6OKa/m+Dv15tf/1x1MDAwM1x1MDAxY9s3yyJ9 Start00:00:00.00Reset5 lineshorizontal layout1 cell margin1 cell paddingaround buttonsbackground coloris $boost
  • layout: horizontal aligns child widgets horizontally from left to right.
  • background: $boost sets the background color to $boost. The $ prefix picks a pre-defined color from the builtin theme. There are other ways to specify colors such as \"blue\" or rgb(20,46,210).
  • height: 5 sets the height of our widget to 5 lines of text.
  • margin: 1 sets a margin of 1 cell around the Stopwatch widget to create a little space between widgets in the list.
  • min-width: 50 sets the minimum width of our widget to 50 cells.
  • padding: 1 sets a padding of 1 cell around the child widgets.

Here's the rest of stopwatch03.tcss which contains further declaration blocks:

TimeDisplay {\ncontent-align: center middle;\nopacity: 60%;\nheight: 3;\n}\nButton {\nwidth: 16;\n}\n#start {\ndock: left;\n}\n#stop {\ndock: left;\ndisplay: none;\n}\n#reset {\ndock: right;\n}\n

The TimeDisplay block aligns text to the center (content-align), fades it slightly (opacity), and sets its height (height) to 3 lines.

The Button block sets the width (width) of buttons to 16 cells (character widths).

The last 3 blocks have a slightly different format. When the declaration begins with a # then the styles will be applied to widgets with a matching \"id\" attribute. We've set an ID on the Button widgets we yielded in compose. For instance the first button has id=\"start\" which matches #start in the CSS.

The buttons have a dock style which aligns the widget to a given edge. The start and stop buttons are docked to the left edge, while the reset button is docked to the right edge.

You may have noticed that the stop button (#stop in the CSS) has display: none;. This tells Textual to not show the button. We do this because we don't want to display the stop button when the timer is not running. Similarly, we don't want to show the start button when the timer is running. We will cover how to manage such dynamic user interfaces in the next section.

"},{"location":"tutorial/#dynamic-css","title":"Dynamic CSS","text":"

We want our Stopwatch widget to have two states: a default state with a Start and Reset button; and a started state with a Stop button. When a stopwatch is started it should also have a green background and bold text.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nNVaa1PiSFx1MDAxNP3ur7CYr5rp98OqrS1fOO4oXCLq6ri1ZYWkIZFAMFx0XHUwMDAzOOV/305wJYFEXHUwMDEwXHUwMDExXHUwMDE5Synp7vS96b6nz+nb/Wtjc7NcdTAwMTRccruqtLNZUlx1MDAwM8v0XFw7MPulrbj8p1xuQtfv6CqUfFx1MDAwZv1eYCUtnSjqhjtfv7bNoKWirmdayvjphj3TXHUwMDBio57t+oblt7+6kWqHf8afXHUwMDE1s63+6PptO1xujLGRbWW7kVx1MDAxZoxsKU+1VSdcbnXv/+jvm5u/ks+Ud4GyXCKz0/RU8kBSNXZcdTAwMTBTNlla8TuJs1x1MDAxMENcdTAwMDExxlx1MDAwMr20cMNcdTAwMDNtL1K2rm5on9W4Ji4qoW23o/reoSjzk6FPh+JQXHUwMDBl62OzXHLX8y6ioZe4XHUwMDE1+vptxnVhXHUwMDE0+C117dqRXHUwMDEz254oL3oq8HtNp6PCMPOM3zUtN1x1MDAxYcZlXHUwMDAwvJSOxmBnc1xcMohniFx1MDAwMlx1MDAwM0GJ9GtcbojoS138NELSoIhiXHRYqmbk0b7v6SnQXHUwMDFlfVx1MDAwMcnP2Ke6abWa2rGOPW5DgFx1MDAwNShcdTAwMWa36T+/J2XIwJJQnDbtKLfpRCPvXHJcdTAwMGVcdTAwMDVP2VbJ2ENcdTAwMDRcdTAwMDRHXHUwMDEwkfHUxFx1MDAxNrvHdlx1MDAxMlx1MDAwN/9Ojp5jXHUwMDA23edRKoXxl5S3saOHk0GUXHUwMDBlpNT87pFcdTAwMTCE/UFj96T5sCtxXHUwMDEz/+Wdf3/pK1x1MDAxM3VmXHUwMDEw+P3SS83T839j13pd21x1MDAxY0VcdTAwMTJkXGZxyKCk+u+l3nM7LV3Z6XneuMy3WuPgS0qftlx1MDAxNoh6wkFh1EtcdTAwMGUklZSLuaM+qNTR7vZcdL4/sitn1UrtR3hb21vzqGfAkFx1MDAwMFx0jFx1MDAwMJ6KekJcclx1MDAwMlx1MDAxOYD0vVHfMDV60HTU686ng52xqSiHUlxuXHUwMDAyQGqyVlx1MDAxMuWHu4d30VEzVJWWffvQOFx1MDAwNPBuu5dcdTAwMWblkVx1MDAxYUSpIN/K7zbTemteg5+HnYyfXHUwMDE5siBFsEFcYnKs10syN2peXHUwMDFm5SxqXHUwMDFj03J6gVpcdTAwMDfc0GLcMGbIZeAmXG7MTtg1XHUwMDAzXHUwMDFkqznYoTnYQXhcbjtSYqqdoWL52FlmXHUwMDFjjufb70RcdTAwMTfuY1x1MDAxMksgU1o22643zExZXHUwMDEyoNrTi8hPO2qGSltMVnGeabvruc04gEuWflx1MDAwN1x1MDAxNWRiO3K1rnpp0HZtO81cdTAwMTmWdsDUfVx1MDAwNsfzLPV+4Dbdjuldpv1bnKZQ6jUmaUpALUpcdTAwMDCZX5v1XHUwMDBlr1x1MDAwM+fS/SZJvyo6dXl601x1MDAxMt/Wm6VcYqVcdTAwMDaEnHHNxlNoo9JcdTAwMDBcdTAwMTJyyTL6aFx1MDAxMZZKfvKQhlxySIq0XHUwMDE51lx1MDAwYoEgOEebMY6JXlxiXHRbPvJeY63WXfPQbLdcdTAwMWQk78hjXHUwMDE5n4fSuyrQZsthrXyD68dakMhCXHUwMDE0SUxcdTAwMDSjLKUwZsHo9WFeU9pcIlruXHUwMDE1XHUwMDAySVNcdTAwMWFcdTAwMTV4MpiXTVtcdTAwMDRPY2iatqhcdTAwMDZcdTAwMWRcdTAwMTP4I7CzPqxcdTAwMDXATvJrZFx1MDAwN/TDyWtcdTAwMDZcdTAwMDNMklfazcU5jMvirVx1MDAxNlx1MDAxMZzogYfzi0bYP6uehFF9t7ZrXHUwMDFmPJRZ11xu67X1JjGGpcFFNoVcdTAwMTA/uc1cclx1MDAxZOlUvlx1MDAxM3VfLGVcdTAwMTPbnHeTJSZcdTAwMTHHXHUwMDA0xVxcolXvsS49//6x45fvavVqZ7/Svq5W9uvvYavfq9tZ3Jpv8Fxy3Eo5kzKlXHUwMDBlP4hbXHUwMDE5K1aoVCAuxVx1MDAxYsD9+iivKbUyQnLhXHKJISAj781cdTAwMWMuZy9IuVx1MDAxNrJcdTAwMWN/XHUwMDAwxpdcdTAwMTmB72PVmlxuVbRSPp1BRpN8OnJwcSalQFx1MDAxNGGNx0hcdTAwMTNcZr9cdTAwMDFrXHUwMDAz+O3+7j4kofqhRexpMPjR3f5MXCLFcyXqqeSMTjEpgsYyXHUwMDA0bCGVUsZcZs71ti5tIJOlXHUwMDE3dFwi6/N/nlx1MDAxZSMtcEBqXHUwMDExXFyJno1XXHUwMDFl+Fx1MDAwNuQtXHUwMDFllIyjoqDEXHUwMDE4MoTwXHUwMDFiUlx1MDAxNOft87NK1ynT/burRlx1MDAxN3lcdTAwMTXsW0frre6SRDqmlGTSXHUwMDEwSVTG26qJNOEyj47mTaJTXHUwMDBl9NKPwYrTXHUwMDExe41WebD7/Vie3J62L09cdTAwMDfgxqyU36/EfpduZ1x0vHyD6yfwaHHKX6+4klxuXCLnz528PsprKvCSlH8uwqGuWEJcdTAwMDJyXHUwMDE5XHUwMDEyXHUwMDBmaX2nXaFopYmTVUu8i8hcZlYr8WYw0nS+P3ZwcTYlpJBNIcNcdTAwMTJLXHUwMDFkb/NrvL9r29fSqdVOq4KU/dtcdTAwMDPn4LzZ/FxcuPG5Uv5cdTAwMTTg6VxylTCYpO9l01x1MDAxOVhbLOFPmdB2Kf6Ao7bXOOvCeri+8/f/dlx1MDAwNvXHq+F5w2pcdTAwMGWCo/dT4e/S7SyGzTe4flxmi3nhXHIsxiiIz3Hnh/zro7ymkI9cdTAwMGYnciGvXHRcdTAwMTZ+KLnOdyghpN5eM/BcdTAwMTH4Xlx1MDAxZm79rEOJXHUwMDE5JLXwoURcdTAwMTHcXGIovsNCMJGI0vlcdTAwMDXtzZk68Fx1MDAxYex+31x1MDAxOYKa1+Ku6Fx1MDAwNnTdXHUwMDA1LYlcdTAwMTMpOVlcdTAwMTQujUmVu3TAXHUwMDAxlFx1MDAwM7hpRiWAUckkXm3aXHUwMDA0QcRTm+pVXFxe6ZuR5eTjTeTjzVON6Fx1MDAxNbRlLsRkoZZ5kbybKiNnXHUwMDE2Qlx1MDAxNS1GXHUwMDE1RHqXKKVEb4BVs/5Ar1x1MDAxYSdcdTAwMDdu9fjspkrCI+ZcdTAwMDRrXHUwMDBmK6hcdTAwMTUqZtn7usmjnFx1MDAxOIxcIvyx5+tcdTAwMTLOhSwmXHUwMDEwkECI1e1cdTAwMTOJ5jLGqcSrRJbehil7czGEzWS0xTE26dZcYmtcdTAwMWLPKrRkdru6TVx1MDAxNDs3Qp6eXHUwMDFj135+/XHXpZ+u6u9cdTAwMTXfZNp4xm9cZlx1MDAxNFx1MDAxNU/Nr6eNp/9cdTAwMDDD6SGzIn0= Stop00:00:00.00ResetStart00:00:00.00StopwatchStarted Stopwatch

We can accomplish this with a CSS class. Not to be confused with a Python class, a CSS class is like a tag you can add to a widget to modify its styles.

Here's the new CSS:

stopwatch04.tcss
Stopwatch {\nlayout: horizontal;\nbackground: $boost;\nheight: 5;\nmargin: 1;\nmin-width: 50;\npadding: 1;\n}\nTimeDisplay {\ncontent-align: center middle;\ntext-opacity: 60%;\nheight: 3;\n}\nButton {\nwidth: 16;\n}\n#start {\ndock: left;\n}\n#stop {\ndock: left;\ndisplay: none;\n}\n#reset {\ndock: right;\n}\n.started {\ntext-style: bold;\nbackground: $success;\ncolor: $text;\n}\n.started TimeDisplay {\ntext-opacity: 100%;\n}\n.started #start {\ndisplay: none\n}\n.started #stop {\ndisplay: block\n}\n.started #reset {\nvisibility: hidden\n}\n

These new rules are prefixed with .started. The . indicates that .started refers to a CSS class called \"started\". The new styles will be applied only to widgets that have this CSS class.

Some of the new styles have more than one selector separated by a space. The space indicates that the rule should match the second selector if it is a child of the first. Let's look at one of these styles:

.started #start {\ndisplay: none\n}\n

The .started selector matches any widget with a \"started\" CSS class. While #start matches a child widget with an ID of \"start\". So it matches the Start button only for Stopwatches in a started state.

The rule is \"display: none\" which tells Textual to hide the button.

"},{"location":"tutorial/#manipulating-classes","title":"Manipulating classes","text":"

Modifying a widget's CSS classes is a convenient way to update visuals without introducing a lot of messy display related code.

You can add and remove CSS classes with the add_class() and remove_class() methods. We will use these methods to connect the started state to the Start / Stop buttons.

The following code will start or stop the stopwatches in response to clicking a button.

stopwatch04.py
from textual.app import App, ComposeResult\nfrom textual.containers import ScrollableContainer\nfrom textual.widgets import Button, Footer, Header, Static\nclass TimeDisplay(Static):\n\"\"\"A widget to display elapsed time.\"\"\"\nclass Stopwatch(Static):\n\"\"\"A stopwatch widget.\"\"\"\ndef on_button_pressed(self, event: Button.Pressed) -> None:\n\"\"\"Event handler called when a button is pressed.\"\"\"\nif event.button.id == \"start\":\nself.add_class(\"started\")\nelif event.button.id == \"stop\":\nself.remove_class(\"started\")\ndef compose(self) -> ComposeResult:\n\"\"\"Create child widgets of a stopwatch.\"\"\"\nyield Button(\"Start\", id=\"start\", variant=\"success\")\nyield Button(\"Stop\", id=\"stop\", variant=\"error\")\nyield Button(\"Reset\", id=\"reset\")\nyield TimeDisplay(\"00:00:00.00\")\nclass StopwatchApp(App):\n\"\"\"A Textual app to manage stopwatches.\"\"\"\nCSS_PATH = \"stopwatch04.tcss\"\nBINDINGS = [(\"d\", \"toggle_dark\", \"Toggle dark mode\")]\ndef compose(self) -> ComposeResult:\n\"\"\"Create child widgets for the app.\"\"\"\nyield Header()\nyield Footer()\nyield ScrollableContainer(Stopwatch(), Stopwatch(), Stopwatch())\ndef action_toggle_dark(self) -> None:\n\"\"\"An action to toggle dark mode.\"\"\"\nself.dark = not self.dark\nif __name__ == \"__main__\":\napp = StopwatchApp()\napp.run()\n

The on_button_pressed method is an event handler. Event handlers are methods called by Textual in response to an event such as a key press, mouse click, etc. Event handlers begin with on_ followed by the name of the event they will handle. Hence on_button_pressed will handle the button pressed event.

If you run stopwatch04.py now you will be able to toggle between the two states by clicking the first button:

stopwatch04.py \u2b58StopwatchApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a000:00:00.00\u00a0Reset\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a000:00:00.00\u00a0Reset\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a000:00:00.00\u00a0Reset\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0

"},{"location":"tutorial/#reactive-attributes","title":"Reactive attributes","text":"

A recurring theme in Textual is that you rarely need to explicitly update a widget. It is possible: you can call refresh() to display new data. However, Textual prefers to do this automatically via reactive attributes.

You can declare a reactive attribute with reactive. Let's use this feature to create a timer that displays elapsed time and keeps it updated.

stopwatch05.py
from time import monotonic\nfrom textual.app import App, ComposeResult\nfrom textual.containers import ScrollableContainer\nfrom textual.reactive import reactive\nfrom textual.widgets import Button, Footer, Header, Static\nclass TimeDisplay(Static):\n\"\"\"A widget to display elapsed time.\"\"\"\nstart_time = reactive(monotonic)\ntime = reactive(0.0)\ndef on_mount(self) -> None:\n\"\"\"Event handler called when widget is added to the app.\"\"\"\nself.set_interval(1 / 60, self.update_time)\ndef update_time(self) -> None:\n\"\"\"Method to update the time to the current time.\"\"\"\nself.time = monotonic() - self.start_time\ndef watch_time(self, time: float) -> None:\n\"\"\"Called when the time attribute changes.\"\"\"\nminutes, seconds = divmod(time, 60)\nhours, minutes = divmod(minutes, 60)\nself.update(f\"{hours:02,.0f}:{minutes:02.0f}:{seconds:05.2f}\")\nclass Stopwatch(Static):\n\"\"\"A stopwatch widget.\"\"\"\ndef on_button_pressed(self, event: Button.Pressed) -> None:\n\"\"\"Event handler called when a button is pressed.\"\"\"\nif event.button.id == \"start\":\nself.add_class(\"started\")\nelif event.button.id == \"stop\":\nself.remove_class(\"started\")\ndef compose(self) -> ComposeResult:\n\"\"\"Create child widgets of a stopwatch.\"\"\"\nyield Button(\"Start\", id=\"start\", variant=\"success\")\nyield Button(\"Stop\", id=\"stop\", variant=\"error\")\nyield Button(\"Reset\", id=\"reset\")\nyield TimeDisplay()\nclass StopwatchApp(App):\n\"\"\"A Textual app to manage stopwatches.\"\"\"\nCSS_PATH = \"stopwatch04.tcss\"\nBINDINGS = [(\"d\", \"toggle_dark\", \"Toggle dark mode\")]\ndef compose(self) -> ComposeResult:\n\"\"\"Create child widgets for the app.\"\"\"\nyield Header()\nyield Footer()\nyield ScrollableContainer(Stopwatch(), Stopwatch(), Stopwatch())\ndef action_toggle_dark(self) -> None:\n\"\"\"An action to toggle dark mode.\"\"\"\nself.dark = not self.dark\nif __name__ == \"__main__\":\napp = StopwatchApp()\napp.run()\n

We have added two reactive attributes to the TimeDisplay widget: start_time will contain the time (in seconds) the stopwatch was started, and time will contain the time to be displayed on the Stopwatch.

Both attributes will be available on self as if you had assigned them in __init__. If you write to either of these attributes the widget will update automatically.

Info

The monotonic function in this example is imported from the standard library time module. It is similar to time.time but won't go backwards if the system clock is changed.

The first argument to reactive may be a default value for the attribute or a callable that returns a default value. We set the default for start_time to the monotonic function which will be called to initialize the attribute with the current time when the TimeDisplay is added to the app. The time attribute has a simple float as the default, so self.time will be initialized to 0.

The on_mount method is an event handler called when the widget is first added to the application (or mounted in Textual terminology). In this method we call set_interval() to create a timer which calls self.update_time sixty times a second. This update_time method calculates the time elapsed since the widget started and assigns it to self.time \u2014 which brings us to one of Reactive's super-powers.

If you implement a method that begins with watch_ followed by the name of a reactive attribute, then the method will be called when the attribute is modified. Such methods are known as watch methods.

Because watch_time watches the time attribute, when we update self.time 60 times a second we also implicitly call watch_time which converts the elapsed time to a string and updates the widget with a call to self.update. Because this happens automatically, we don't need to pass in an initial argument to TimeDisplay.

The end result is that the Stopwatch widgets show the time elapsed since the widget was created:

stopwatch05.py \u2b58StopwatchApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a000:00:01.03\u00a0Reset\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a000:00:01.03\u00a0Reset\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a000:00:01.03\u00a0Reset\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0

We've seen how we can update widgets with a timer, but we still need to wire up the buttons so we can operate stopwatches independently.

"},{"location":"tutorial/#wiring-buttons","title":"Wiring buttons","text":"

We need to be able to start, stop, and reset each stopwatch independently. We can do this by adding a few more methods to the TimeDisplay class.

stopwatch06.py
from time import monotonic\nfrom textual.app import App, ComposeResult\nfrom textual.containers import ScrollableContainer\nfrom textual.reactive import reactive\nfrom textual.widgets import Button, Footer, Header, Static\nclass TimeDisplay(Static):\n\"\"\"A widget to display elapsed time.\"\"\"\nstart_time = reactive(monotonic)\ntime = reactive(0.0)\ntotal = reactive(0.0)\ndef on_mount(self) -> None:\n\"\"\"Event handler called when widget is added to the app.\"\"\"\nself.update_timer = self.set_interval(1 / 60, self.update_time, pause=True)\ndef update_time(self) -> None:\n\"\"\"Method to update time to current.\"\"\"\nself.time = self.total + (monotonic() - self.start_time)\ndef watch_time(self, time: float) -> None:\n\"\"\"Called when the time attribute changes.\"\"\"\nminutes, seconds = divmod(time, 60)\nhours, minutes = divmod(minutes, 60)\nself.update(f\"{hours:02,.0f}:{minutes:02.0f}:{seconds:05.2f}\")\ndef start(self) -> None:\n\"\"\"Method to start (or resume) time updating.\"\"\"\nself.start_time = monotonic()\nself.update_timer.resume()\ndef stop(self) -> None:\n\"\"\"Method to stop the time display updating.\"\"\"\nself.update_timer.pause()\nself.total += monotonic() - self.start_time\nself.time = self.total\ndef reset(self) -> None:\n\"\"\"Method to reset the time display to zero.\"\"\"\nself.total = 0\nself.time = 0\nclass Stopwatch(Static):\n\"\"\"A stopwatch widget.\"\"\"\ndef on_button_pressed(self, event: Button.Pressed) -> None:\n\"\"\"Event handler called when a button is pressed.\"\"\"\nbutton_id = event.button.id\ntime_display = self.query_one(TimeDisplay)\nif button_id == \"start\":\ntime_display.start()\nself.add_class(\"started\")\nelif button_id == \"stop\":\ntime_display.stop()\nself.remove_class(\"started\")\nelif button_id == \"reset\":\ntime_display.reset()\ndef compose(self) -> ComposeResult:\n\"\"\"Create child widgets of a stopwatch.\"\"\"\nyield Button(\"Start\", id=\"start\", variant=\"success\")\nyield Button(\"Stop\", id=\"stop\", variant=\"error\")\nyield Button(\"Reset\", id=\"reset\")\nyield TimeDisplay()\nclass StopwatchApp(App):\n\"\"\"A Textual app to manage stopwatches.\"\"\"\nCSS_PATH = \"stopwatch04.tcss\"\nBINDINGS = [(\"d\", \"toggle_dark\", \"Toggle dark mode\")]\ndef compose(self) -> ComposeResult:\n\"\"\"Called to add widgets to the app.\"\"\"\nyield Header()\nyield Footer()\nyield ScrollableContainer(Stopwatch(), Stopwatch(), Stopwatch())\ndef action_toggle_dark(self) -> None:\n\"\"\"An action to toggle dark mode.\"\"\"\nself.dark = not self.dark\nif __name__ == \"__main__\":\napp = StopwatchApp()\napp.run()\n

Here's a summary of the changes made to TimeDisplay.

  • We've added a total reactive attribute to store the total time elapsed between clicking the start and stop buttons.
  • The call to set_interval has grown a pause=True argument which starts the timer in pause mode (when a timer is paused it won't run until resume() is called). This is because we don't want the time to update until the user hits the start button.
  • The update_time method now adds total to the current time to account for the time between any previous clicks of the start and stop buttons.
  • We've stored the result of set_interval which returns a Timer object. We will use this later to resume the timer when we start the Stopwatch.
  • We've added start(), stop(), and reset() methods.

In addition, the on_button_pressed method on Stopwatch has grown some code to manage the time display when the user clicks a button. Let's look at that in detail:

    def on_button_pressed(self, event: Button.Pressed) -> None:\n\"\"\"Event handler called when a button is pressed.\"\"\"\nbutton_id = event.button.id\ntime_display = self.query_one(TimeDisplay)\nif button_id == \"start\":\ntime_display.start()\nself.add_class(\"started\")\nelif button_id == \"stop\":\ntime_display.stop()\nself.remove_class(\"started\")\nelif button_id == \"reset\":\ntime_display.reset()\n

This code supplies missing features and makes our app useful. We've made the following changes.

  • The first line retrieves id attribute of the button that was pressed. We can use this to decide what to do in response.
  • The second line calls query_one to get a reference to the TimeDisplay widget.
  • We call the method on TimeDisplay that matches the pressed button.
  • We add the \"started\" class when the Stopwatch is started (self.add_class(\"started\")), and remove it (self.remove_class(\"started\")) when it is stopped. This will update the Stopwatch visuals via CSS.

If you run stopwatch06.py you will be able to use the stopwatches independently.

stopwatch06.py \u2b58StopwatchApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a000:00:00.00\u00a0Reset\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Stop\u00a000:00:04.03 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a000:00:00.00\u00a0Reset\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0

The only remaining feature of the Stopwatch app left to implement is the ability to add and remove stopwatches.

"},{"location":"tutorial/#dynamic-widgets","title":"Dynamic widgets","text":"

The Stopwatch app creates widgets when it starts via the compose method. We will also need to create new widgets while the app is running, and remove widgets we no longer need. We can do this by calling mount() to add a widget, and remove() to remove a widget.

Let's use these methods to implement adding and removing stopwatches to our app.

stopwatch.py
from time import monotonic\nfrom textual.app import App, ComposeResult\nfrom textual.containers import ScrollableContainer\nfrom textual.reactive import reactive\nfrom textual.widgets import Button, Footer, Header, Static\nclass TimeDisplay(Static):\n\"\"\"A widget to display elapsed time.\"\"\"\nstart_time = reactive(monotonic)\ntime = reactive(0.0)\ntotal = reactive(0.0)\ndef on_mount(self) -> None:\n\"\"\"Event handler called when widget is added to the app.\"\"\"\nself.update_timer = self.set_interval(1 / 60, self.update_time, pause=True)\ndef update_time(self) -> None:\n\"\"\"Method to update time to current.\"\"\"\nself.time = self.total + (monotonic() - self.start_time)\ndef watch_time(self, time: float) -> None:\n\"\"\"Called when the time attribute changes.\"\"\"\nminutes, seconds = divmod(time, 60)\nhours, minutes = divmod(minutes, 60)\nself.update(f\"{hours:02,.0f}:{minutes:02.0f}:{seconds:05.2f}\")\ndef start(self) -> None:\n\"\"\"Method to start (or resume) time updating.\"\"\"\nself.start_time = monotonic()\nself.update_timer.resume()\ndef stop(self):\n\"\"\"Method to stop the time display updating.\"\"\"\nself.update_timer.pause()\nself.total += monotonic() - self.start_time\nself.time = self.total\ndef reset(self):\n\"\"\"Method to reset the time display to zero.\"\"\"\nself.total = 0\nself.time = 0\nclass Stopwatch(Static):\n\"\"\"A stopwatch widget.\"\"\"\ndef on_button_pressed(self, event: Button.Pressed) -> None:\n\"\"\"Event handler called when a button is pressed.\"\"\"\nbutton_id = event.button.id\ntime_display = self.query_one(TimeDisplay)\nif button_id == \"start\":\ntime_display.start()\nself.add_class(\"started\")\nelif button_id == \"stop\":\ntime_display.stop()\nself.remove_class(\"started\")\nelif button_id == \"reset\":\ntime_display.reset()\ndef compose(self) -> ComposeResult:\n\"\"\"Create child widgets of a stopwatch.\"\"\"\nyield Button(\"Start\", id=\"start\", variant=\"success\")\nyield Button(\"Stop\", id=\"stop\", variant=\"error\")\nyield Button(\"Reset\", id=\"reset\")\nyield TimeDisplay()\nclass StopwatchApp(App):\n\"\"\"A Textual app to manage stopwatches.\"\"\"\nCSS_PATH = \"stopwatch.tcss\"\nBINDINGS = [\n(\"d\", \"toggle_dark\", \"Toggle dark mode\"),\n(\"a\", \"add_stopwatch\", \"Add\"),\n(\"r\", \"remove_stopwatch\", \"Remove\"),\n]\ndef compose(self) -> ComposeResult:\n\"\"\"Called to add widgets to the app.\"\"\"\nyield Header()\nyield Footer()\nyield ScrollableContainer(Stopwatch(), Stopwatch(), Stopwatch(), id=\"timers\")\ndef action_add_stopwatch(self) -> None:\n\"\"\"An action to add a timer.\"\"\"\nnew_stopwatch = Stopwatch()\nself.query_one(\"#timers\").mount(new_stopwatch)\nnew_stopwatch.scroll_visible()\ndef action_remove_stopwatch(self) -> None:\n\"\"\"Called to remove a timer.\"\"\"\ntimers = self.query(\"Stopwatch\")\nif timers:\ntimers.last().remove()\ndef action_toggle_dark(self) -> None:\n\"\"\"An action to toggle dark mode.\"\"\"\nself.dark = not self.dark\nif __name__ == \"__main__\":\napp = StopwatchApp()\napp.run()\n

Here's a summary of the changes:

  • The ScrollableContainer object in StopWatchApp grew a \"timers\" ID.
  • Added action_add_stopwatch to add a new stopwatch.
  • Added action_remove_stopwatch to remove a stopwatch.
  • Added keybindings for the actions.

The action_add_stopwatch method creates and mounts a new stopwatch. Note the call to query_one() with a CSS selector of \"#timers\" which gets the timer's container via its ID. Once mounted, the new Stopwatch will appear in the terminal. That last line in action_add_stopwatch calls scroll_visible() which will scroll the container to make the new Stopwatch visible (if required).

The action_remove_stopwatch function calls query() with a CSS selector of \"Stopwatch\" which gets all the Stopwatch widgets. If there are stopwatches then the action calls last() to get the last stopwatch, and remove() to remove it.

If you run stopwatch.py now you can add a new stopwatch with the A key and remove a stopwatch with R.

stopwatch.py \u2b58StopwatchApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a000:00:00.00\u00a0Reset\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a000:00:00.00\u00a0Reset\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a000:00:00.00\u00a0Reset\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Start\u00a000:00:00.00\u00a0Reset\u00a0 \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0\u00a0A\u00a0\u00a0Add\u00a0\u00a0R\u00a0\u00a0Remove\u00a0

"},{"location":"tutorial/#what-next","title":"What next?","text":"

Congratulations on building your first Textual application! This tutorial has covered a lot of ground. If you are the type that prefers to learn a framework by coding, feel free. You could tweak stopwatch.py or look through the examples.

Read the guide for the full details on how to build sophisticated TUI applications with Textual.

"},{"location":"widget_gallery/","title":"Widgets","text":"

Welcome to the Textual widget gallery.

We have many more widgets planned, or you can build your own.

Info

Textual is a TUI framework. Everything below runs in the terminal.

"},{"location":"widget_gallery/#button","title":"Button","text":"

A simple button with a variety of semantic styles.

Button reference

ButtonsApp Standard\u00a0ButtonsDisabled\u00a0Buttons \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Default\u00a0\u00a0Default\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Primary!\u00a0\u00a0Primary!\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Success!\u00a0\u00a0Success!\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Warning!\u00a0\u00a0Warning!\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Error!\u00a0\u00a0Error!\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

"},{"location":"widget_gallery/#checkbox","title":"Checkbox","text":"

A classic checkbox control.

Checkbox reference

CheckboxApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258c\u00a0Arrakis\u00a0\ud83d\ude13\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258c\u00a0Caladan\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258c\u00a0Chusuk\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258cGiedi\u00a0Prime\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258cGinaz\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258c\u00a0Grumman\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2583\u2583 \u258a\u2590X\u258cKaitain\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e

"},{"location":"widget_gallery/#collapsible","title":"Collapsible","text":"

Content that may be toggled on and off by clicking a title.

Collapsible reference

CollapsibleApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25bc\u00a0Leto #\u00a0Duke\u00a0Leto\u00a0I\u00a0Atreides Head\u00a0of\u00a0House\u00a0Atreides. \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25bc\u00a0Jessica \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eLady\u00a0Jessica\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Bene\u00a0Gesserit\u00a0and\u00a0concubine\u00a0of\u00a0Leto,\u00a0and\u00a0mother\u00a0of\u00a0Paul\u00a0and\u00a0Alia. \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25b6\u00a0Paul \u00a0C\u00a0\u00a0Collapse\u00a0All\u00a0\u00a0E\u00a0\u00a0Expand\u00a0All\u00a0

"},{"location":"widget_gallery/#contentswitcher","title":"ContentSwitcher","text":"

A widget for containing and switching display between multiple child widgets.

ContentSwitcher reference

"},{"location":"widget_gallery/#datatable","title":"DataTable","text":"

A powerful data table, with configurable cursors.

DataTable reference

TableApp \u00a0lane\u00a0\u00a0swimmer\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0country\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0time\u00a0\u00a0 \u00a04\u00a0\u00a0\u00a0\u00a0\u00a0Joseph\u00a0Schooling\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Singapore\u00a0\u00a0\u00a0\u00a0\u00a0\u00a050.39\u00a0 \u00a02\u00a0\u00a0\u00a0\u00a0\u00a0Michael\u00a0Phelps\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.14\u00a0 \u00a05\u00a0\u00a0\u00a0\u00a0\u00a0Chad\u00a0le\u00a0Clos\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0South\u00a0Africa\u00a0\u00a0\u00a051.14\u00a0 \u00a06\u00a0\u00a0\u00a0\u00a0\u00a0L\u00e1szl\u00f3\u00a0Cseh\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Hungary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.14\u00a0 \u00a03\u00a0\u00a0\u00a0\u00a0\u00a0Li\u00a0Zhuhao\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0China\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.26\u00a0 \u00a08\u00a0\u00a0\u00a0\u00a0\u00a0Mehdy\u00a0Metella\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0France\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.58\u00a0 \u00a07\u00a0\u00a0\u00a0\u00a0\u00a0Tom\u00a0Shields\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.73\u00a0 \u00a01\u00a0\u00a0\u00a0\u00a0\u00a0Aleksandr\u00a0Sadovnikov\u00a0\u00a0Russia\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0 \u00a010\u00a0\u00a0\u00a0\u00a0Darren\u00a0Burns\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Scotland\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0

"},{"location":"widget_gallery/#digits","title":"Digits","text":"

Display numbers in tall characters.

Digits reference

DigitApp \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557 \u2551\u257a\u2501\u2513\u00a0\u00a0\u2513\u00a0\u257b\u00a0\u257b\u00a0\u2513\u00a0\u00a0\u250f\u2501\u2578\u250f\u2501\u2513\u257a\u2501\u2513\u00a0\u250f\u2501\u2578\u250f\u2501\u2578\u257a\u2501\u2513\u00a0\u250f\u2501\u2578\u250f\u2501\u2513\u250f\u2501\u2513\u257a\u2501\u2513\u2551 \u2551\u00a0\u2501\u252b\u00a0\u00a0\u2503\u00a0\u2517\u2501\u252b\u00a0\u2503\u00a0\u00a0\u2517\u2501\u2513\u2517\u2501\u252b\u250f\u2501\u251b\u00a0\u2523\u2501\u2513\u2517\u2501\u2513\u00a0\u2501\u252b\u00a0\u2517\u2501\u2513\u2523\u2501\u252b\u2517\u2501\u252b\u00a0\u00a0\u2503\u2551 \u2551\u257a\u2501\u251b.\u257a\u253b\u2578\u00a0\u00a0\u2579\u257a\u253b\u2578,\u257a\u2501\u251b\u257a\u2501\u251b\u2517\u2501\u2578,\u2517\u2501\u251b\u257a\u2501\u251b\u257a\u2501\u251b,\u257a\u2501\u251b\u2517\u2501\u251b\u257a\u2501\u251b\u00a0\u00a0\u2579\u2551 \u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d

"},{"location":"widget_gallery/#directorytree","title":"DirectoryTree","text":"

A tree view of files and folders.

DirectoryTree reference

DirectoryTreeApp \ud83d\udcc2\u00a0. \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0.cache \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0.faq \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0.git \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0.github \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0.ipynb_checkpoints \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0.mypy_cache \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0.pytest_cache \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0.screenshot_cache \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0.vscode \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0__pycache__ \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0dist\u2581\u2581 \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0docs \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0examples \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0imgs \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0notes \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0questions \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0reference \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0sandbox \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0site \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0src \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0tests \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0tools \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0.coverage

"},{"location":"widget_gallery/#footer","title":"Footer","text":"

A footer to display and interact with key bindings.

Footer reference

FooterApp \u00a0Q\u00a0\u00a0Quit\u00a0the\u00a0app\u00a0\u00a0?\u00a0\u00a0Show\u00a0help\u00a0screen\u00a0\u00a0DELETE\u00a0\u00a0Delete\u00a0the\u00a0thing\u00a0

"},{"location":"widget_gallery/#header","title":"Header","text":"

A header to display the app's title and subtitle.

Header reference

HeaderApp \u2b58HeaderApp

"},{"location":"widget_gallery/#input","title":"Input","text":"

A control to enter text.

Input reference

InputApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aDarren\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aLast\u00a0Name\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

"},{"location":"widget_gallery/#label","title":"Label","text":"

A simple text label.

Label reference

"},{"location":"widget_gallery/#listview","title":"ListView","text":"

Display a list of items (items may be other widgets).

ListView reference

ListViewExample One Two Three

"},{"location":"widget_gallery/#loadingindicator","title":"LoadingIndicator","text":"

Display an animation while data is loading.

LoadingIndicator reference

LoadingApp \u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf

"},{"location":"widget_gallery/#log","title":"Log","text":"

Display and update lines of text (such as from a file).

Log reference

LogApp And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain. I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain. I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\u2584\u2584 I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.

"},{"location":"widget_gallery/#markdownviewer","title":"MarkdownViewer","text":"

Display and interact with a Markdown document (adds a table of contents and browser-like navigation to Markdown).

MarkdownViewer reference

MarkdownExampleApp \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u25bc\u00a0\u2160\u00a0Markdown\u00a0Viewer\u258a\u258e\u258a \u251c\u2500\u2500\u00a0\u2161\u00a0Features\u258a\u258eMarkdown\u00a0Viewer\u258a \u251c\u2500\u2500\u00a0\u2161\u00a0Tables\u258a\u258e\u258a \u2514\u2500\u2500\u00a0\u2161\u00a0Code\u00a0Blocks\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258aThis\u00a0is\u00a0an\u00a0example\u00a0of\u00a0Textual's\u00a0MarkdownViewer\u00a0widget. \u258a \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258a\u258e\u258a \u258a\u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Features\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258e\u258a \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258aMarkdown\u00a0syntax\u00a0and\u00a0extensions\u00a0are\u00a0supported. \u258a \u258a\u25cf\u00a0Typography\u00a0emphasis,\u00a0strong,\u00a0inline\u00a0code\u00a0etc. \u258a\u25cf\u00a0Headers \u258a\u25cf\u00a0Lists\u00a0(bullet\u00a0and\u00a0ordered) \u258a\u25cf\u00a0Syntax\u00a0highlighted\u00a0code\u00a0blocks \u258a\u25cf\u00a0Tables! \u258a \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258a\u258e\u258a \u258a\u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Tables\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258e\u258a \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258aTables\u00a0are\u00a0displayed\u00a0in\u00a0a\u00a0DataTable\u00a0widget. \u258a \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258a\u258e\u258a \u258a\u258eName\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0TypeDefaultDescription\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258e\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0\u258a \u258a\u258eshow_headerboolTrueShow\u00a0the\u00a0table\u00a0header\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258efixed_rowsint0Number\u00a0of\u00a0fixed\u00a0rows\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258efixed_columnsint0Number\u00a0of\u00a0fixed\u00a0columns\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258ezebra_stripesboolFalseDisplay\u00a0alternating\u00a0colors\u00a0on\u00a0rows\u258a \u258a\u258eheader_heightint1Height\u00a0of\u00a0header\u00a0row\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258eshow_cursorboolTrueShow\u00a0a\u00a0cell\u00a0cursor\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258e\u258a \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258a \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258a\u258e\u258a

"},{"location":"widget_gallery/#markdown","title":"Markdown","text":"

Display a markdown document.

Markdown reference

MarkdownExampleApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eMarkdown\u00a0Document\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 This\u00a0is\u00a0an\u00a0example\u00a0of\u00a0Textual's\u00a0Markdown\u00a0widget. \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Features\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Markdown\u00a0syntax\u00a0and\u00a0extensions\u00a0are\u00a0supported. \u25cf\u00a0Typography\u00a0emphasis,\u00a0strong,\u00a0inline\u00a0code\u00a0etc. \u25cf\u00a0Headers \u25cf\u00a0Lists\u00a0(bullet\u00a0and\u00a0ordered) \u25cf\u00a0Syntax\u00a0highlighted\u00a0code\u00a0blocks \u25cf\u00a0Tables!

"},{"location":"widget_gallery/#optionlist","title":"OptionList","text":"

Display a vertical list of options (options may be Rich renderables).

OptionList reference

OptionListApp \u2b58OptionListApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aAerilon\u258e \u258aAquaria\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aCanceron\u258e \u258aCaprica\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aGemenon\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aLeonis\u258e \u258aLibran\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aPicon\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aSagittaron\u2584\u2584\u258e \u258aScorpia\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

"},{"location":"widget_gallery/#placeholder","title":"Placeholder","text":"

Display placeholder content while you are designing a UI.

Placeholder reference

PlaceholderApp Placeholder\u00a0p2\u00a0here! This\u00a0is\u00a0a\u00a0custom\u00a0label\u00a0for\u00a0p1. #p4 #p3#p5Placeholde r Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0 26\u00a0x\u00a06amet,\u00a0consectetur\u00a027\u00a0x\u00a06 adipiscing\u00a0elit.\u00a0Etiam\u00a0 feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0 Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0 consectetur\u00a0adipiscing\u00a0elit.\u00a0Etiam\u00a040\u00a0x\u00a06 feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0accumsan.\u00a0 Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis\u00a0 gravida.\u00a0Phasellus\u00a0id\u00a0eleifend\u00a0ligula. Nullam\u00a0imperdiet\u00a0sem\u00a0tellus,\u00a0sed\u00a0 vehicula\u00a0nisl\u00a0faucibus\u00a0sit\u00a0amet.\u00a0Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0 Praesent\u00a0iaculis\u00a0tempor\u00a0ultricies.\u00a0Sedconsectetur\u00a0adipiscing\u00a0elit.\u00a0Etiam\u00a0 lacinia,\u00a0tellus\u00a0id\u00a0rutrum\u00a0lacinia,\u00a0feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0accumsan.\u00a0 sapien\u00a0sapien\u00a0congue\u00a0mauris,\u00a0sit\u00a0amet\u00a0Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis\u00a0

"},{"location":"widget_gallery/#pretty","title":"Pretty","text":"

Display a pretty-formatted Rich renderable.

Pretty reference

PrettyExample { 'title':\u00a0'Back\u00a0to\u00a0the\u00a0Future', 'releaseYear':\u00a01985, 'director':\u00a0'Robert\u00a0Zemeckis', 'genre':\u00a0'Adventure,\u00a0Comedy,\u00a0Sci-Fi', 'cast':\u00a0[ {'actor':\u00a0'Michael\u00a0J.\u00a0Fox',\u00a0'character':\u00a0'Marty\u00a0McFly'}, {'actor':\u00a0'Christopher\u00a0Lloyd',\u00a0'character':\u00a0'Dr.\u00a0Emmett\u00a0Brown'} ] }

"},{"location":"widget_gallery/#progressbar","title":"ProgressBar","text":"

A configurable progress bar with ETA and percentage complete.

ProgressBar reference

Funding\u00a0tracking \u2b58Funding\u00a0tracking Funding:\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u250150% \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258a$$$\u258e\u00a0Donate\u00a0 \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 Donation\u00a0for\u00a0$50\u00a0received!

"},{"location":"widget_gallery/#radiobutton","title":"RadioButton","text":"

A simple radio button.

RadioButton reference

RadioChoicesApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590\u25cf\u258cBattlestar\u00a0Galactica\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a01984\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a02021\u258e \u258a\u2590\u25cf\u258c\u00a0Serenity\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Trek:\u00a0The\u00a0Motion\u00a0Picture\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Wars:\u00a0A\u00a0New\u00a0Hope\u258e \u258a\u2590\u25cf\u258c\u00a0The\u00a0Last\u00a0Starfighter\u258e \u258a\u2590\u25cf\u258c\u00a0Total\u00a0Recall\u00a0\ud83d\udc49\u00a0\ud83d\udd34\u258e \u258a\u2590\u25cf\u258c\u00a0Wing\u00a0Commander\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

"},{"location":"widget_gallery/#radioset","title":"RadioSet","text":"

A collection of radio buttons, that enforces uniqueness.

RadioSet reference

RadioChoicesApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590\u25cf\u258cBattlestar\u00a0Galactica\u258e\u258a\u2590\u25cf\u258c\u00a0Amanda\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a01984\u258e\u258a\u2590\u25cf\u258c\u00a0Connor\u00a0MacLeod\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a02021\u258e\u258a\u2590\u25cf\u258c\u00a0Duncan\u00a0MacLeod\u258e \u258a\u2590\u25cf\u258c\u00a0Serenity\u258e\u258a\u2590\u25cf\u258c\u00a0Heather\u00a0MacLeod\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Trek:\u00a0The\u00a0Motion\u00a0Picture\u258e\u258a\u2590\u25cf\u258c\u00a0Joe\u00a0Dawson\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Wars:\u00a0A\u00a0New\u00a0Hope\u258e\u258a\u2590\u25cf\u258c\u00a0Kurgan,\u00a0The\u258e \u258a\u2590\u25cf\u258c\u00a0The\u00a0Last\u00a0Starfighter\u258e\u258a\u2590\u25cf\u258c\u00a0Methos\u258e \u258a\u2590\u25cf\u258c\u00a0Total\u00a0Recall\u00a0\ud83d\udc49\u00a0\ud83d\udd34\u258e\u258a\u2590\u25cf\u258c\u00a0Rachel\u00a0Ellenstein\u258e \u258a\u2590\u25cf\u258c\u00a0Wing\u00a0Commander\u258e\u258a\u2590\u25cf\u258c\u00a0Ram\u00edrez\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

"},{"location":"widget_gallery/#richlog","title":"RichLog","text":"

Display and update text in a scrolling panel.

RichLog reference

RichLogApp \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0previous_value=next(iter_values) \u2502\u00a0\u00a0\u00a0exceptStopIteration: \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0return \u2502\u00a0\u00a0\u00a0first=True\u2585\u2585 \u2502\u00a0\u00a0\u00a0forvalueiniter_values: \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0yieldfirst,False,previous_value \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0first=False \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0previous_value=value \u2502\u00a0\u00a0\u00a0yieldfirst,True,previous_value \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503lane\u2503swimmer\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503country\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503time\u00a0\u2503 \u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529 \u25024\u00a0\u00a0\u00a0\u2502Joseph\u00a0Schooling\u00a0\u00a0\u00a0\u00a0\u2502Singapore\u00a0\u00a0\u00a0\u00a0\u250250.39\u2502 \u25022\u00a0\u00a0\u00a0\u2502Michael\u00a0Phelps\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502United\u00a0States\u250251.14\u2502 \u25025\u00a0\u00a0\u00a0\u2502Chad\u00a0le\u00a0Clos\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502South\u00a0Africa\u00a0\u250251.14\u2502 \u25026\u00a0\u00a0\u00a0\u2502L\u00e1szl\u00f3\u00a0Cseh\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502Hungary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250251.14\u2502 \u25023\u00a0\u00a0\u00a0\u2502Li\u00a0Zhuhao\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502China\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250251.26\u2502 \u25028\u00a0\u00a0\u00a0\u2502Mehdy\u00a0Metella\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502France\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250251.58\u2502 \u25027\u00a0\u00a0\u00a0\u2502Tom\u00a0Shields\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502United\u00a0States\u250251.73\u2502 \u25021\u00a0\u00a0\u00a0\u2502Aleksandr\u00a0Sadovnikov\u2502Russia\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250251.84\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 Write\u00a0text\u00a0or\u00a0any\u00a0Rich\u00a0renderable! Key(key='H',\u00a0character='H',\u00a0name='upper_h',\u00a0is_printable=True) Key(key='i',\u00a0character='i',\u00a0name='i',\u00a0is_printable=True)

"},{"location":"widget_gallery/#rule","title":"Rule","text":"

A rule widget to separate content, similar to a <hr> HTML tag.

Rule reference

HorizontalRulesApp \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0solid\u00a0(default)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0heavy\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0thick\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0dashed\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0double\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0ascii\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 ----------------------------------------------------------------

"},{"location":"widget_gallery/#select","title":"Select","text":"

Select from a number of possible options.

Select reference

SelectApp \u2b58SelectApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aSelect\u25b2\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aSelect\u258e \u258aI\u00a0must\u00a0not\u00a0fear.\u258e \u258aFear\u00a0is\u00a0the\u00a0mind-killer.\u258e \u258aFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0\u258e \u258aobliteration.\u258e \u258aI\u00a0will\u00a0face\u00a0my\u00a0fear.\u258e \u258aI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

"},{"location":"widget_gallery/#selectionlist","title":"SelectionList","text":"

Select multiple values from a list of options.

SelectionList reference

SelectionListApp \u2b58SelectionListApp \u250c\u2500\u00a0Shall\u00a0we\u00a0play\u00a0some\u00a0games?\u00a0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502 \u2502\u2590X\u258cFalken's\u00a0Maze\u2502 \u2502\u2590X\u258cBlack\u00a0Jack\u2502 \u2502\u2590X\u258cGin\u00a0Rummy\u2502 \u2502\u2590X\u258cHearts\u2502 \u2502\u2590X\u258cBridge\u2502 \u2502\u2590X\u258cCheckers\u2502 \u2502\u2590X\u258cChess\u2502 \u2502\u2590X\u258cPoker\u2502 \u2502\u2590X\u258cFighter\u00a0Combat\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

"},{"location":"widget_gallery/#sparkline","title":"Sparkline","text":"

Display numerical data.

Sparkline reference

SparklineSummaryFunctionApp \u2582\u2584\u2582\u2584\u2583\u2583\u2586\u2585\u2583\u2582\u2583\u2582\u2583\u2582\u2584\u2587\u2583\u2583\u2587\u2585\u2584\u2583\u2584\u2584\u2583\u2582\u2583\u2582\u2583\u2584\u2584\u2588\u2586\u2582\u2583\u2583\u2585\u2583\u2583\u2584\u2583\u2587\u2583\u2583\u2583\u2584\u2584\u2586\u2583\u2583\u2585\u2582\u2585\u2583\u2584\u2583\u2583\u2584\u2583\u2585\u2586\u2582\u2582\u2583\u2586\u2582\u2583\u2584\u2585\u2584\u2583\u2584\u2584\u2581\u2583\u2582 \u2581\u2582\u2581\u2582\u2581\u2581\u2582\u2582\u2581\u2581\u2581\u2581\u2582\u2581\u2581\u2582\u2581\u2581\u2582\u2582\u2582\u2582\u2582\u2582\u2581\u2581\u2581\u2581\u2581\u2582\u2581\u2582\u2582\u2581\u2581\u2581\u2582\u2581\u2581\u2582\u2581\u2582\u2581\u2581\u2581\u2581\u2582\u2582\u2581\u2581\u2581\u2581\u2582\u2581\u2581\u2582\u2581\u2582\u2581\u2581\u2582\u2581\u2581\u2581\u2581\u2581\u2581\u2582\u2582\u2582\u2581\u2582\u2581\u2581\u2581\u2581 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

"},{"location":"widget_gallery/#static","title":"Static","text":"

Displays simple static content. Typically used as a base class.

Static reference

"},{"location":"widget_gallery/#switch","title":"Switch","text":"

An on / off control, inspired by toggle buttons.

Switch reference

SwitchApp Example\u00a0switches \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e off:\u00a0\u00a0\u00a0\u00a0\u00a0\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e on:\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e focused:\u00a0\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e custom:\u00a0\u00a0\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

"},{"location":"widget_gallery/#tabs","title":"Tabs","text":"

A row of tabs you can select with the mouse or navigate with keys.

Tabs reference

TabsApp \u00a0AtreidiesDuke\u00a0Leto\u00a0AtreidesLady\u00a0JessicaGurney\u00a0HalleckBaron\u00a0Vladimir \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258aLady\u00a0Jessica\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u00a0A\u00a0\u00a0Add\u00a0tab\u00a0\u00a0R\u00a0\u00a0Remove\u00a0active\u00a0tab\u00a0\u00a0C\u00a0\u00a0Clear\u00a0tabs\u00a0

"},{"location":"widget_gallery/#tabbedcontent","title":"TabbedContent","text":"

A Combination of Tabs and ContentSwitcher to navigate static content.

TabbedContent reference

TabbedApp LetoJessicaPaul \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eLady\u00a0Jessica\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Bene\u00a0Gesserit\u00a0and\u00a0concubine\u00a0of\u00a0Leto,\u00a0and\u00a0mother\u00a0of\u00a0Paul\u00a0and\u00a0Alia. PaulAlia \u2501\u2578\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 First\u00a0child \u00a0L\u00a0\u00a0Leto\u00a0\u00a0J\u00a0\u00a0Jessica\u00a0\u00a0P\u00a0\u00a0Paul\u00a0

"},{"location":"widget_gallery/#textarea","title":"TextArea","text":"

A multi-line text area which supports syntax highlighting various languages.

TextArea reference

TextAreaExample 1\u00a0\u00a0defhello(name):\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 2\u00a0\u00a0print(\"hello\"+\u00a0name)\u00a0\u00a0\u00a0 3\u00a0\u00a0 4\u00a0\u00a0defgoodbye(name):\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 5\u00a0\u00a0print(\"goodbye\"+\u00a0name)\u00a0 6\u00a0\u00a0

"},{"location":"widget_gallery/#tree","title":"Tree","text":"

A tree control with expandable nodes.

Tree reference

TreeApp \u25bc\u00a0Dune \u2517\u2501\u2501\u00a0\u25bc\u00a0Characters \u2523\u2501\u2501\u00a0Paul \u2523\u2501\u2501\u00a0Jessica \u2517\u2501\u2501\u00a0Chani

"},{"location":"api/","title":"API","text":"

This is a API-level reference to the Textual API. Click the links to your left (or in the burger menu) to open a reference for each module.

If you are new to Textual, you may want to read the tutorial or guide first.

"},{"location":"api/app/","title":"App","text":"

Here you will find the App class, which is the base class for Textual apps.

See app basics for how to build Textual apps.

"},{"location":"api/app/#textual.app.AutopilotCallbackType","title":"AutopilotCallbackType module-attribute","text":"
AutopilotCallbackType: TypeAlias = (\n\"Callable[[Pilot[object]], Coroutine[Any, Any, None]]\"\n)\n

Signature for valid callbacks that can be used to control apps.

"},{"location":"api/app/#textual.app.CSSPathType","title":"CSSPathType module-attribute","text":"
CSSPathType = Union[\nstr, PurePath, List[Union[str, PurePath]]\n]\n

Valid ways of specifying paths to CSS files.

"},{"location":"api/app/#textual.app.ActionError","title":"ActionError class","text":"

Bases: Exception

Base class for exceptions relating to actions.

"},{"location":"api/app/#textual.app.ActiveModeError","title":"ActiveModeError class","text":"

Bases: ModeError

Raised when attempting to remove the currently active mode.

"},{"location":"api/app/#textual.app.App","title":"App class","text":"
def __init__(\nself, driver_class=None, css_path=None, watch_css=False\n):\n

Bases: Generic[ReturnType], DOMNode

The base class for Textual Applications.

Parameters Name Type Description Default driver_class Type[Driver] | None

Driver class or None to auto-detect. This will be used by some Textual tools.

None css_path CSSPathType | None

Path to CSS or None to use the CSS_PATH class variable. To load multiple CSS files, pass a list of strings or paths which will be loaded in order.

None watch_css bool

Reload CSS if the files changed. This is set automatically if you are using textual run with the dev switch.

False Raises Type Description CssPathError

When the supplied CSS path(s) are an unexpected type.

"},{"location":"api/app/#textual.app.App.AUTO_FOCUS","title":"AUTO_FOCUS class-attribute","text":"
AUTO_FOCUS: str | None = '*'\n

A selector to determine what to focus automatically when a screen is activated.

The widget focused is the first that matches the given CSS selector. Setting to None or \"\" disables auto focus.

"},{"location":"api/app/#textual.app.App.COMMANDS","title":"COMMANDS class-attribute","text":"
COMMANDS: set[type[Provider]] = {SystemCommands}\n

Command providers used by the command palette.

Should be a set of command.Provider classes.

"},{"location":"api/app/#textual.app.App.CSS","title":"CSS class-attribute","text":"
CSS: str = ''\n

Inline CSS, useful for quick scripts. This is loaded after CSS_PATH, and therefore takes priority in the event of a specificity clash.

"},{"location":"api/app/#textual.app.App.CSS_PATH","title":"CSS_PATH class-attribute","text":"
CSS_PATH: CSSPathType | None = None\n

File paths to load CSS from.

"},{"location":"api/app/#textual.app.App.ENABLE_COMMAND_PALETTE","title":"ENABLE_COMMAND_PALETTE class-attribute","text":"
ENABLE_COMMAND_PALETTE: bool = True\n

Should the command palette be enabled for the application?

"},{"location":"api/app/#textual.app.App.MODES","title":"MODES class-attribute","text":"
MODES: dict[str, str | Screen | Callable[[], Screen]] = {}\n

Modes associated with the app and their base screens.

The base screen is the screen at the bottom of the mode stack. You can think of it as the default screen for that stack. The base screens can be names of screens listed in SCREENS, Screen instances, or callables that return screens.

Example
class HelpScreen(Screen[None]):\n...\nclass MainAppScreen(Screen[None]):\n...\nclass MyApp(App[None]):\nMODES = {\n\"default\": \"main\",\n\"help\": HelpScreen,\n}\nSCREENS = {\n\"main\": MainAppScreen,\n}\n...\n
"},{"location":"api/app/#textual.app.App.SCREENS","title":"SCREENS class-attribute","text":"
SCREENS: dict[str, Screen | Callable[[], Screen]] = {}\n

Screens associated with the app for the lifetime of the app.

"},{"location":"api/app/#textual.app.App.SUB_TITLE","title":"SUB_TITLE instance-attribute class-attribute","text":"
SUB_TITLE: str | None = None\n

A class variable to set the default sub-title for the application.

To update the sub-title while the app is running, you can set the sub_title attribute. See also the Screen.SUB_TITLE attribute.

"},{"location":"api/app/#textual.app.App.TITLE","title":"TITLE instance-attribute class-attribute","text":"
TITLE: str | None = None\n

A class variable to set the default title for the application.

To update the title while the app is running, you can set the title attribute. See also the Screen.TITLE attribute.

"},{"location":"api/app/#textual.app.App.children","title":"children property","text":"
children: Sequence['Widget']\n

A view onto the app's immediate children.

This attribute exists on all widgets. In the case of the App, it will only ever contain a single child, which will be the currently active screen.

Returns Type Description Sequence['Widget']

A sequence of widgets.

"},{"location":"api/app/#textual.app.App.current_mode","title":"current_mode property","text":"
current_mode: str\n

The name of the currently active mode.

"},{"location":"api/app/#textual.app.App.dark","title":"dark instance-attribute class-attribute","text":"
dark: Reactive[bool] = Reactive(True, compute=False)\n

Use a dark theme if True, otherwise use a light theme.

Modify this attribute to switch between light and dark themes.

Example
self.app.dark = not self.app.dark  # Toggle dark mode\n
"},{"location":"api/app/#textual.app.App.debug","title":"debug property","text":"
debug: bool\n

Is debug mode enabled?

"},{"location":"api/app/#textual.app.App.focused","title":"focused property","text":"
focused: Widget | None\n

The widget that is focused on the currently active screen, or None.

Focused widgets receive keyboard input.

Returns Type Description Widget | None

The currently focused widget, or None if nothing is focused.

"},{"location":"api/app/#textual.app.App.is_headless","title":"is_headless property","text":"
is_headless: bool\n

Is the driver running in 'headless' mode?

Headless mode is used when running tests with run_test.

"},{"location":"api/app/#textual.app.App.log","title":"log property","text":"
log: Logger\n

The textual logger.

Example
self.log(\"Hello, World!\")\nself.log(self.tree)\n
Returns Type Description Logger

A Textual logger.

"},{"location":"api/app/#textual.app.App.namespace_bindings","title":"namespace_bindings property","text":"
namespace_bindings: dict[str, tuple[DOMNode, Binding]]\n

Get currently active bindings.

If no widget is focused, then app-level bindings are returned. If a widget is focused, then any bindings present in the active screen and app are merged and returned.

This property may be used to inspect current bindings.

Returns Type Description dict[str, tuple[DOMNode, Binding]]

A mapping of keys onto pairs of nodes and bindings.

"},{"location":"api/app/#textual.app.App.return_code","title":"return_code property","text":"
return_code: int | None\n

The return code with which the app exited.

Non-zero codes indicate errors. A value of 1 means the app exited with a fatal error. If the app wasn't exited yet, this will be None.

Example

The return code can be used to exit the process via sys.exit.

my_app.run()\nsys.exit(my_app.return_code)\n

"},{"location":"api/app/#textual.app.App.return_value","title":"return_value property","text":"
return_value: ReturnType | None\n

The return value of the app, or None if it has not yet been set.

The return value is set when calling exit.

"},{"location":"api/app/#textual.app.App.screen","title":"screen property","text":"
screen: Screen[object]\n

The current active screen.

Returns Type Description Screen[object]

The currently active (visible) screen.

Raises Type Description ScreenStackError

If there are no screens on the stack.

"},{"location":"api/app/#textual.app.App.screen_stack","title":"screen_stack property","text":"
screen_stack: Sequence[Screen]\n

A snapshot of the current screen stack.

Returns Type Description Sequence[Screen]

A snapshot of the current state of the screen stack.

"},{"location":"api/app/#textual.app.App.scroll_sensitivity_x","title":"scroll_sensitivity_x instance-attribute","text":"
scroll_sensitivity_x: float = 4.0\n

Number of columns to scroll in the X direction with wheel or trackpad.

"},{"location":"api/app/#textual.app.App.scroll_sensitivity_y","title":"scroll_sensitivity_y instance-attribute","text":"
scroll_sensitivity_y: float = 2.0\n

Number of lines to scroll in the Y direction with wheel or trackpad.

"},{"location":"api/app/#textual.app.App.size","title":"size property","text":"
size: Size\n

The size of the terminal.

Returns Type Description Size

Size of the terminal.

"},{"location":"api/app/#textual.app.App.sub_title","title":"sub_title instance-attribute class-attribute","text":"
sub_title: Reactive[str] = (\nself.SUB_TITLE if self.SUB_TITLE is not None else \"\"\n)\n

The sub-title for the application.

The initial value for sub_title will be set to the SUB_TITLE class variable if it exists, or an empty string if it doesn't.

Sub-titles are typically used to show the high-level state of the app, such as the current mode, or path to the file being worked on.

Assign a new value to this attribute to change the sub-title. The new value is always converted to string.

"},{"location":"api/app/#textual.app.App.title","title":"title instance-attribute class-attribute","text":"
title: Reactive[str] = (\nself.TITLE\nif self.TITLE is not None\nelse f\"{self.__class__.__name__}\"\n)\n

The title for the application.

The initial value for title will be set to the TITLE class variable if it exists, or the name of the app if it doesn't.

Assign a new value to this attribute to change the title. The new value is always converted to string.

"},{"location":"api/app/#textual.app.App.use_command_palette","title":"use_command_palette instance-attribute","text":"
use_command_palette: bool = self.ENABLE_COMMAND_PALETTE\n

A flag to say if the application should use the command palette.

If set to False any call to action_command_palette will be ignored.

"},{"location":"api/app/#textual.app.App.workers","title":"workers property","text":"
workers: WorkerManager\n

The worker manager.

Returns Type Description WorkerManager

An object to manage workers.

"},{"location":"api/app/#textual.app.App.action_add_class","title":"action_add_class async","text":"
def action_add_class(self, selector, class_name):\n

An action to add a CSS class to the selected widget.

Parameters Name Type Description Default selector str

Selects the widget to add the class to.

required class_name str

The class to add to the selected widget.

required"},{"location":"api/app/#textual.app.App.action_back","title":"action_back async","text":"
def action_back(self):\n

An action to go back to the previous screen (pop the current screen).

Note

If there is no screen to go back to, this is a non-operation (in other words it's safe to call even if there are no other screens on the stack.)

"},{"location":"api/app/#textual.app.App.action_bell","title":"action_bell async","text":"
def action_bell(self):\n

An action to play the terminal 'bell'.

"},{"location":"api/app/#textual.app.App.action_check_bindings","title":"action_check_bindings async","text":"
def action_check_bindings(self, key):\n

An action to handle a key press using the binding system.

Parameters Name Type Description Default key str

The key to process.

required"},{"location":"api/app/#textual.app.App.action_command_palette","title":"action_command_palette method","text":"
def action_command_palette(self):\n

Show the Textual command palette.

"},{"location":"api/app/#textual.app.App.action_focus","title":"action_focus async","text":"
def action_focus(self, widget_id):\n

An action to focus the given widget.

Parameters Name Type Description Default widget_id str

ID of widget to focus.

required"},{"location":"api/app/#textual.app.App.action_focus_next","title":"action_focus_next method","text":"
def action_focus_next(self):\n

An action to focus the next widget.

"},{"location":"api/app/#textual.app.App.action_focus_previous","title":"action_focus_previous method","text":"
def action_focus_previous(self):\n

An action to focus the previous widget.

"},{"location":"api/app/#textual.app.App.action_pop_screen","title":"action_pop_screen async","text":"
def action_pop_screen(self):\n

An action to remove the topmost screen and makes the new topmost screen active.

"},{"location":"api/app/#textual.app.App.action_push_screen","title":"action_push_screen async","text":"
def action_push_screen(self, screen):\n

An action to push a new screen on to the stack and make it active.

Parameters Name Type Description Default screen str

Name of the screen.

required"},{"location":"api/app/#textual.app.App.action_quit","title":"action_quit async","text":"
def action_quit(self):\n

An action to quit the app as soon as possible.

"},{"location":"api/app/#textual.app.App.action_remove_class","title":"action_remove_class async","text":"
def action_remove_class(self, selector, class_name):\n

An action to remove a CSS class from the selected widget.

Parameters Name Type Description Default selector str

Selects the widget to remove the class from.

required class_name str

The class to remove from the selected widget.

required"},{"location":"api/app/#textual.app.App.action_screenshot","title":"action_screenshot method","text":"
def action_screenshot(self, filename=None, path='./'):\n

This action will save an SVG file containing the current contents of the screen.

Parameters Name Type Description Default filename str | None

Filename of screenshot, or None to auto-generate.

None path str

Path to directory. Defaults to current working directory.

'./'"},{"location":"api/app/#textual.app.App.action_switch_mode","title":"action_switch_mode async","text":"
def action_switch_mode(self, mode):\n

An action that switches to the given mode..

"},{"location":"api/app/#textual.app.App.action_switch_screen","title":"action_switch_screen async","text":"
def action_switch_screen(self, screen):\n

An action to switch screens.

Parameters Name Type Description Default screen str

Name of the screen.

required"},{"location":"api/app/#textual.app.App.action_toggle_class","title":"action_toggle_class async","text":"
def action_toggle_class(self, selector, class_name):\n

An action to toggle a CSS class on the selected widget.

Parameters Name Type Description Default selector str

Selects the widget to toggle the class on.

required class_name str

The class to toggle on the selected widget.

required"},{"location":"api/app/#textual.app.App.action_toggle_dark","title":"action_toggle_dark method","text":"
def action_toggle_dark(self):\n

An action to toggle dark mode.

"},{"location":"api/app/#textual.app.App.add_mode","title":"add_mode method","text":"
def add_mode(self, mode, base_screen):\n

Adds a mode and its corresponding base screen to the app.

Parameters Name Type Description Default mode str

The new mode.

required base_screen str | Screen | Callable[[], Screen]

The base screen associated with the given mode.

required Raises Type Description InvalidModeError

If the name of the mode is not valid/duplicated.

"},{"location":"api/app/#textual.app.App.animate","title":"animate method","text":"
def animate(\nself,\nattribute,\nvalue,\n*,\nfinal_value=Ellipsis,\nduration=None,\nspeed=None,\ndelay=0.0,\neasing=DEFAULT_EASING,\non_complete=None\n):\n

Animate an attribute.

See the guide for how to use the animation system.

Parameters Name Type Description Default attribute str

Name of the attribute to animate.

required value float | Animatable

The value to animate to.

required final_value object

The final value of the animation.

Ellipsis duration float | None

The duration of the animate.

None speed float | None

The speed of the animation.

None delay float

A delay (in seconds) before the animation starts.

0.0 easing EasingFunction | str

An easing method.

DEFAULT_EASING on_complete CallbackType | None

A callable to invoke when the animation is finished.

None"},{"location":"api/app/#textual.app.App.batch_update","title":"batch_update method","text":"
def batch_update(self):\n

A context manager to suspend all repaints until the end of the batch.

"},{"location":"api/app/#textual.app.App.begin_capture_print","title":"begin_capture_print method","text":"
def begin_capture_print(self, target, stdout=True, stderr=True):\n

Capture content that is printed (or written to stdout / stderr).

If printing is captured, the target will be send an events.Print message.

Parameters Name Type Description Default target MessageTarget

The widget where print content will be sent.

required stdout bool

Capture stdout.

True stderr bool

Capture stderr.

True"},{"location":"api/app/#textual.app.App.bell","title":"bell method","text":"
def bell(self):\n

Play the console 'bell'.

For terminals that support a bell, this typically makes a notification or error sound. Some terminals may make no sound or display a visual bell indicator, depending on configuration.

"},{"location":"api/app/#textual.app.App.bind","title":"bind method","text":"
def bind(\nself,\nkeys,\naction,\n*,\ndescription=\"\",\nshow=True,\nkey_display=None\n):\n

Bind a key to an action.

Parameters Name Type Description Default keys str

A comma separated list of keys, i.e.

required action str

Action to bind to.

required description str

Short description of action.

'' show bool

Show key in UI.

True key_display str | None

Replacement text for key, or None to use default.

None"},{"location":"api/app/#textual.app.App.call_from_thread","title":"call_from_thread method","text":"
def call_from_thread(self, callback, *args, **kwargs):\n

Run a callable from another thread, and return the result.

Like asyncio apps in general, Textual apps are not thread-safe. If you call methods or set attributes on Textual objects from a thread, you may get unpredictable results.

This method will ensure that your code runs within the correct context.

Tip

Consider using post_message which is also thread-safe.

Parameters Name Type Description Default callback Callable[..., CallThreadReturnType | Awaitable[CallThreadReturnType]]

A callable to run.

required *args object

Arguments to the callback.

() **kwargs object

Keyword arguments for the callback.

{} Raises Type Description RuntimeError

If the app isn't running or if this method is called from the same thread where the app is running.

Returns Type Description CallThreadReturnType

The result of the callback.

"},{"location":"api/app/#textual.app.App.capture_mouse","title":"capture_mouse method","text":"
def capture_mouse(self, widget):\n

Send all mouse events to the given widget or disable mouse capture.

Parameters Name Type Description Default widget Widget | None

If a widget, capture mouse event, or None to end mouse capture.

required"},{"location":"api/app/#textual.app.App.check_bindings","title":"check_bindings async","text":"
def check_bindings(self, key, priority=False):\n

Handle a key press.

This method is used internally by the bindings system, but may be called directly if you wish to simulate a key being pressed.

Parameters Name Type Description Default key str

A key.

required priority bool

If True check from App down, otherwise from focused up.

False Returns Type Description bool

True if the key was handled by a binding, otherwise False

"},{"location":"api/app/#textual.app.App.clear_notifications","title":"clear_notifications method","text":"
def clear_notifications(self):\n

Clear all the current notifications.

"},{"location":"api/app/#textual.app.App.end_capture_print","title":"end_capture_print method","text":"
def end_capture_print(self, target):\n

End capturing of prints.

Parameters Name Type Description Default target MessageTarget

The widget that was capturing prints.

required"},{"location":"api/app/#textual.app.App.exit","title":"exit method","text":"
def exit(self, result=None, return_code=0, message=None):\n

Exit the app, and return the supplied result.

Parameters Name Type Description Default result ReturnType | None

Return value.

None return_code int

The return code. Use non-zero values for error codes.

0 message RenderableType | None

Optional message to display on exit.

None"},{"location":"api/app/#textual.app.App.export_screenshot","title":"export_screenshot method","text":"
def export_screenshot(self, *, title=None):\n

Export an SVG screenshot of the current screen.

See also save_screenshot which writes the screenshot to a file.

Parameters Name Type Description Default title str | None

The title of the exported screenshot or None to use app title.

None"},{"location":"api/app/#textual.app.App.get_child_by_id","title":"get_child_by_id method","text":"
def get_child_by_id(self, id, expect_type=None):\n

Get the first child (immediate descendent) of this DOMNode with the given ID.

Parameters Name Type Description Default id str

The ID of the node to search for.

required expect_type type[ExpectType] | None

Require the object be of the supplied type, or use None to apply no type restriction.

None Returns Type Description ExpectType | Widget

The first child of this node with the specified ID.

Raises Type Description NoMatches

If no children could be found for this ID.

WrongType

If the wrong type was found.

"},{"location":"api/app/#textual.app.App.get_child_by_type","title":"get_child_by_type method","text":"
def get_child_by_type(self, expect_type):\n

Get a child of a give type.

Parameters Name Type Description Default expect_type type[ExpectType]

The type of the expected child.

required Raises Type Description NoMatches

If no valid child is found.

Returns Type Description ExpectType

A widget.

"},{"location":"api/app/#textual.app.App.get_css_variables","title":"get_css_variables method","text":"
def get_css_variables(self):\n

Get a mapping of variables used to pre-populate CSS.

May be implemented in a subclass to add new CSS variables.

Returns Type Description dict[str, str]

A mapping of variable name to value.

"},{"location":"api/app/#textual.app.App.get_driver_class","title":"get_driver_class method","text":"
def get_driver_class(self):\n

Get a driver class for this platform.

This method is called by the constructor, and unlikely to be required when building a Textual app.

Returns Type Description Type[Driver]

A Driver class which manages input and display.

"},{"location":"api/app/#textual.app.App.get_key_display","title":"get_key_display method","text":"
def get_key_display(self, key):\n

For a given key, return how it should be displayed in an app (e.g. in the Footer widget). By key, we refer to the string used in the \"key\" argument for a Binding instance. By overriding this method, you can ensure that keys are displayed consistently throughout your app, without needing to add a key_display to every binding.

Parameters Name Type Description Default key str

The binding key string.

required Returns Type Description str

The display string for the input key.

"},{"location":"api/app/#textual.app.App.get_screen","title":"get_screen method","text":"
def get_screen(self, screen):\n

Get an installed screen.

Parameters Name Type Description Default screen Screen | str

Either a Screen object or screen name (the name argument when installed).

required Raises Type Description KeyError

If the named screen doesn't exist.

Returns Type Description Screen

A screen instance.

"},{"location":"api/app/#textual.app.App.get_widget_at","title":"get_widget_at method","text":"
def get_widget_at(self, x, y):\n

Get the widget under the given coordinates.

Parameters Name Type Description Default x int

X coordinate.

required y int

Y coordinate.

required Returns Type Description tuple[Widget, Region]

The widget and the widget's screen region.

"},{"location":"api/app/#textual.app.App.get_widget_by_id","title":"get_widget_by_id method","text":"
def get_widget_by_id(self, id, expect_type=None):\n

Get the first descendant widget with the given ID.

Performs a breadth-first search rooted at the current screen. It will not return the Screen if that matches the ID. To get the screen, use self.screen.

Parameters Name Type Description Default id str

The ID to search for in the subtree

required expect_type type[ExpectType] | None

Require the object be of the supplied type, or None for any type. Defaults to None.

None Returns Type Description ExpectType | Widget

The first descendant encountered with this ID.

Raises Type Description NoMatches

if no children could be found for this ID

WrongType

if the wrong type was found.

"},{"location":"api/app/#textual.app.App.install_screen","title":"install_screen method","text":"
def install_screen(self, screen, name):\n

Install a screen.

Installing a screen prevents Textual from destroying it when it is no longer on the screen stack. Note that you don't need to install a screen to use it. See push_screen or switch_screen to make a new screen current.

Parameters Name Type Description Default screen Screen

Screen to install.

required name str

Unique name to identify the screen.

required Raises Type Description ScreenError

If the screen can't be installed.

Returns Type Description None

An awaitable that awaits the mounting of the screen and its children.

"},{"location":"api/app/#textual.app.App.is_mounted","title":"is_mounted method","text":"
def is_mounted(self, widget):\n

Check if a widget is mounted.

Parameters Name Type Description Default widget Widget

A widget.

required Returns Type Description bool

True of the widget is mounted.

"},{"location":"api/app/#textual.app.App.is_screen_installed","title":"is_screen_installed method","text":"
def is_screen_installed(self, screen):\n

Check if a given screen has been installed.

Parameters Name Type Description Default screen Screen | str

Either a Screen object or screen name (the name argument when installed).

required Returns Type Description bool

True if the screen is currently installed,

"},{"location":"api/app/#textual.app.App.mount","title":"mount method","text":"
def mount(self, *widgets, before=None, after=None):\n

Mount the given widgets relative to the app's screen.

Parameters Name Type Description Default *widgets Widget

The widget(s) to mount.

() before int | str | Widget | None

Optional location to mount before. An int is the index of the child to mount before, a str is a query_one query to find the widget to mount before.

None after int | str | Widget | None

Optional location to mount after. An int is the index of the child to mount after, a str is a query_one query to find the widget to mount after.

None Returns Type Description AwaitMount

An awaitable object that waits for widgets to be mounted.

Raises Type Description MountError

If there is a problem with the mount request.

Note

Only one of before or after can be provided. If both are provided a MountError will be raised.

"},{"location":"api/app/#textual.app.App.mount_all","title":"mount_all method","text":"
def mount_all(self, widgets, *, before=None, after=None):\n

Mount widgets from an iterable.

Parameters Name Type Description Default widgets Iterable[Widget]

An iterable of widgets.

required before int | str | Widget | None

Optional location to mount before. An int is the index of the child to mount before, a str is a query_one query to find the widget to mount before.

None after int | str | Widget | None

Optional location to mount after. An int is the index of the child to mount after, a str is a query_one query to find the widget to mount after.

None Returns Type Description AwaitMount

An awaitable object that waits for widgets to be mounted.

Raises Type Description MountError

If there is a problem with the mount request.

Note

Only one of before or after can be provided. If both are provided a MountError will be raised.

"},{"location":"api/app/#textual.app.App.notify","title":"notify method","text":"
def notify(\nself,\nmessage,\n*,\ntitle=\"\",\nseverity=\"information\",\ntimeout=Notification.timeout\n):\n

Create a notification.

Tip

This method is thread-safe.

Parameters Name Type Description Default message str

The message for the notification.

required title str

The title for the notification.

'' severity SeverityLevel

The severity of the notification.

'information' timeout float

The timeout for the notification.

Notification.timeout

The notify method is used to create an application-wide notification, shown in a Toast, normally originating in the bottom right corner of the display.

Notifications can have the following severity levels:

  • information
  • warning
  • error

The default is information.

Example
# Show an information notification.\nself.notify(\"It's an older code, sir, but it checks out.\")\n# Show a warning. Note that Textual's notification system allows\n# for the use of Rich console markup.\nself.notify(\n\"Now witness the firepower of this fully \"\n\"[b]ARMED[/b] and [i][b]OPERATIONAL[/b][/i] battle station!\",\ntitle=\"Possible trap detected\",\nseverity=\"warning\",\n)\n# Show an error. Set a longer timeout so it's noticed.\nself.notify(\"It's a trap!\", severity=\"error\", timeout=10)\n# Show an information notification, but without any sort of title.\nself.notify(\"It's against my programming to impersonate a deity.\", title=\"\")\n
"},{"location":"api/app/#textual.app.App.panic","title":"panic method","text":"
def panic(self, *renderables):\n

Exits the app and display error message(s).

Used in response to unexpected errors. For a more graceful exit, see the exit method.

Parameters Name Type Description Default *renderables RenderableType

Text or Rich renderable(s) to display on exit.

()"},{"location":"api/app/#textual.app.App.pop_screen","title":"pop_screen method","text":"
def pop_screen(self):\n

Pop the current screen from the stack, and switch to the previous screen.

Returns Type Description Screen[object]

The screen that was replaced.

"},{"location":"api/app/#textual.app.App.post_display_hook","title":"post_display_hook method","text":"
def post_display_hook(self):\n

Called immediately after a display is done. Used in tests.

"},{"location":"api/app/#textual.app.App.push_screen","title":"push_screen method","text":"
def push_screen(self, screen, callback=None):\n

Push a new screen on the screen stack, making it the current screen.

Parameters Name Type Description Default screen Screen[ScreenResultType] | str

A Screen instance or the name of an installed screen.

required callback ScreenResultCallbackType[ScreenResultType] | None

An optional callback function that will be called if the screen is dismissed with a result.

None Returns Type Description AwaitMount

An optional awaitable that awaits the mounting of the screen and its children.

"},{"location":"api/app/#textual.app.App.refresh_css","title":"refresh_css method","text":"
def refresh_css(self, animate=True):\n

Refresh CSS.

Parameters Name Type Description Default animate bool

Also execute CSS animations.

True"},{"location":"api/app/#textual.app.App.remove_mode","title":"remove_mode method","text":"
def remove_mode(self, mode):\n

Removes a mode from the app.

Screens that are running in the stack of that mode are scheduled for pruning.

Parameters Name Type Description Default mode str

The mode to remove. It can't be the active mode.

required Raises Type Description ActiveModeError

If trying to remove the active mode.

UnknownModeError

If trying to remove an unknown mode.

"},{"location":"api/app/#textual.app.App.run","title":"run method","text":"
def run(self, *, headless=False, size=None, auto_pilot=None):\n

Run the app.

Parameters Name Type Description Default headless bool

Run in headless mode (no output).

False size tuple[int, int] | None

Force terminal size to (WIDTH, HEIGHT), or None to auto-detect.

None auto_pilot AutopilotCallbackType | None

An auto pilot coroutine.

None Returns Type Description ReturnType | None

App return value.

"},{"location":"api/app/#textual.app.App.run_action","title":"run_action async","text":"
def run_action(self, action, default_namespace=None):\n

Perform an action.

Actions are typically associated with key bindings, where you wouldn't need to call this method manually.

Parameters Name Type Description Default action str | ActionParseResult

Action encoded in a string.

required default_namespace object | None

Namespace to use if not provided in the action, or None to use app.

None Returns Type Description bool

True if the event has been handled.

"},{"location":"api/app/#textual.app.App.run_async","title":"run_async async","text":"
def run_async(\nself, *, headless=False, size=None, auto_pilot=None\n):\n

Run the app asynchronously.

Parameters Name Type Description Default headless bool

Run in headless mode (no output).

False size tuple[int, int] | None

Force terminal size to (WIDTH, HEIGHT), or None to auto-detect.

None auto_pilot AutopilotCallbackType | None

An auto pilot coroutine.

None Returns Type Description ReturnType | None

App return value.

"},{"location":"api/app/#textual.app.App.run_test","title":"run_test async","text":"
def run_test(\nself,\n*,\nheadless=True,\nsize=(80, 24),\ntooltips=False,\nnotifications=False,\nmessage_hook=None\n):\n

An asynchronous context manager for testing apps.

Tip

See the guide for testing Textual apps.

Use this to run your app in \"headless\" mode (no output) and drive the app via a Pilot object.

Example
async with app.run_test() as pilot:\nawait pilot.click(\"#Button.ok\")\nassert ...\n
Parameters Name Type Description Default headless bool

Run in headless mode (no output or input).

True size tuple[int, int] | None

Force terminal size to (WIDTH, HEIGHT), or None to auto-detect.

(80, 24) tooltips bool

Enable tooltips when testing.

False notifications bool

Enable notifications when testing.

False message_hook Callable[[Message], None] | None

An optional callback that will called with every message going through the app.

None"},{"location":"api/app/#textual.app.App.save_screenshot","title":"save_screenshot method","text":"
def save_screenshot(\nself, filename=None, path=\"./\", time_format=None\n):\n

Save an SVG screenshot of the current screen.

Parameters Name Type Description Default filename str | None

Filename of SVG screenshot, or None to auto-generate a filename with the date and time.

None path str

Path to directory for output. Defaults to current working directory.

'./' time_format str | None

Date and time format to use if filename is None. Defaults to a format like ISO 8601 with some reserved characters replaced with underscores.

None Returns Type Description str

Filename of screenshot.

"},{"location":"api/app/#textual.app.App.set_focus","title":"set_focus method","text":"
def set_focus(self, widget, scroll_visible=True):\n

Focus (or unfocus) a widget. A focused widget will receive key events first.

Parameters Name Type Description Default widget Widget | None

Widget to focus.

required scroll_visible bool

Scroll widget in to view.

True"},{"location":"api/app/#textual.app.App.stop_animation","title":"stop_animation async","text":"
def stop_animation(self, attribute, complete=True):\n

Stop an animation on an attribute.

Parameters Name Type Description Default attribute str

Name of the attribute whose animation should be stopped.

required complete bool

Should the animation be set to its final value?

True Note

If there is no animation scheduled or running, this is a no-op.

"},{"location":"api/app/#textual.app.App.switch_mode","title":"switch_mode method","text":"
def switch_mode(self, mode):\n

Switch to a given mode.

Parameters Name Type Description Default mode str

The mode to switch to.

required Raises Type Description UnknownModeError

If trying to switch to an unknown mode.

"},{"location":"api/app/#textual.app.App.switch_screen","title":"switch_screen method","text":"
def switch_screen(self, screen):\n

Switch to another screen by replacing the top of the screen stack with a new screen.

Parameters Name Type Description Default screen Screen | str

Either a Screen object or screen name (the name argument when installed).

required"},{"location":"api/app/#textual.app.App.uninstall_screen","title":"uninstall_screen method","text":"
def uninstall_screen(self, screen):\n

Uninstall a screen.

If the screen was not previously installed then this method is a null-op. Uninstalling a screen allows Textual to delete it when it is popped or switched. Note that uninstalling a screen is only required if you have previously installed it with install_screen. Textual will also uninstall screens automatically on exit.

Parameters Name Type Description Default screen Screen | str

The screen to uninstall or the name of a installed screen.

required Returns Type Description str | None

The name of the screen that was uninstalled, or None if no screen was uninstalled.

"},{"location":"api/app/#textual.app.App.update_styles","title":"update_styles method","text":"
def update_styles(self, node):\n

Immediately update the styles of this node and all descendant nodes.

Should be called whenever CSS classes / pseudo classes change. For example, when you hover over a button, the :hover pseudo class will be added, and this method is called to apply the corresponding :hover styles.

"},{"location":"api/app/#textual.app.App.validate_sub_title","title":"validate_sub_title method","text":"
def validate_sub_title(self, sub_title):\n

Make sure the sub-title is set to a string.

"},{"location":"api/app/#textual.app.App.validate_title","title":"validate_title method","text":"
def validate_title(self, title):\n

Make sure the title is set to a string.

"},{"location":"api/app/#textual.app.App.watch_dark","title":"watch_dark method","text":"
def watch_dark(self, dark):\n

Watches the dark bool.

This method handles the transition between light and dark mode when you change the dark attribute.

"},{"location":"api/app/#textual.app.AppError","title":"AppError class","text":"

Bases: Exception

Base class for general App related exceptions.

"},{"location":"api/app/#textual.app.InvalidModeError","title":"InvalidModeError class","text":"

Bases: ModeError

Raised if there is an issue with a mode name.

"},{"location":"api/app/#textual.app.ModeError","title":"ModeError class","text":"

Bases: Exception

Base class for exceptions related to modes.

"},{"location":"api/app/#textual.app.ScreenError","title":"ScreenError class","text":"

Bases: Exception

Base class for exceptions that relate to screens.

"},{"location":"api/app/#textual.app.ScreenStackError","title":"ScreenStackError class","text":"

Bases: ScreenError

Raised when trying to manipulate the screen stack incorrectly.

"},{"location":"api/app/#textual.app.UnknownModeError","title":"UnknownModeError class","text":"

Bases: ModeError

Raised when attempting to use a mode that is not known.

"},{"location":"api/await_remove/","title":"Await remove","text":"

An optionally awaitable object returned by methods that remove widgets.

"},{"location":"api/await_remove/#textual.await_remove.AwaitRemove","title":"AwaitRemove class","text":"
def __init__(self, finished_flag, task):\n

An awaitable returned by a method that removes DOM nodes.

Returned by Widget.remove and DOMQuery.remove.

Parameters Name Type Description Default finished_flag Event

The asyncio event to wait on.

required task Task

The task which does the remove (required to keep a reference).

required"},{"location":"api/binding/","title":"Binding","text":"

A binding maps a key press on to an action.

See bindings in the guide for details.

"},{"location":"api/binding/#textual.binding.Binding","title":"Binding class","text":"

The configuration of a key binding.

"},{"location":"api/binding/#textual.binding.Binding.action","title":"action instance-attribute","text":"
action: str\n

Action to bind to.

"},{"location":"api/binding/#textual.binding.Binding.description","title":"description instance-attribute class-attribute","text":"
description: str = ''\n

Description of action.

"},{"location":"api/binding/#textual.binding.Binding.key","title":"key instance-attribute","text":"
key: str\n

Key to bind. This can also be a comma-separated list of keys to map multiple keys to a single action.

"},{"location":"api/binding/#textual.binding.Binding.key_display","title":"key_display instance-attribute class-attribute","text":"
key_display: str | None = None\n

How the key should be shown in footer.

"},{"location":"api/binding/#textual.binding.Binding.priority","title":"priority instance-attribute class-attribute","text":"
priority: bool = False\n

Enable priority binding for this key.

"},{"location":"api/binding/#textual.binding.Binding.show","title":"show instance-attribute class-attribute","text":"
show: bool = True\n

Show the action in Footer, or False to hide.

"},{"location":"api/binding/#textual.binding.BindingError","title":"BindingError class","text":"

Bases: Exception

A binding related error.

"},{"location":"api/binding/#textual.binding.InvalidBinding","title":"InvalidBinding class","text":"

Bases: Exception

Binding key is in an invalid format.

"},{"location":"api/binding/#textual.binding.NoBinding","title":"NoBinding class","text":"

Bases: Exception

A binding was not found.

"},{"location":"api/color/","title":"Color","text":"

This module contains a powerful Color class which Textual uses to manipulate colors.

"},{"location":"api/color/#textual.color--named-colors","title":"Named colors","text":"

The following named colors are used by the parse method.

colors \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Name\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503hex\u00a0\u00a0\u00a0\u00a0\u2503RGB\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Color\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503 \u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529 \u2502\"aliceblue\"\u2502#F0F8FF\u2502rgb(240,\u00a0248,\u00a0255)\u2502\u2502 \u2502\"ansi_black\"\u2502#000000\u2502rgb(0,\u00a00,\u00a00)\u2502\u2502 \u2502\"ansi_blue\"\u2502#000080\u2502rgb(0,\u00a00,\u00a0128)\u2502\u2502 \u2502\"ansi_bright_black\"\u2502#808080\u2502rgb(128,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"ansi_bright_blue\"\u2502#0000FF\u2502rgb(0,\u00a00,\u00a0255)\u2502\u2502 \u2502\"ansi_bright_cyan\"\u2502#00FFFF\u2502rgb(0,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"ansi_bright_green\"\u2502#00FF00\u2502rgb(0,\u00a0255,\u00a00)\u2502\u2502 \u2502\"ansi_bright_magenta\"\u2502#FF00FF\u2502rgb(255,\u00a00,\u00a0255)\u2502\u2502 \u2502\"ansi_bright_red\"\u2502#FF0000\u2502rgb(255,\u00a00,\u00a00)\u2502\u2502 \u2502\"ansi_bright_white\"\u2502#FFFFFF\u2502rgb(255,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"ansi_bright_yellow\"\u2502#FFFF00\u2502rgb(255,\u00a0255,\u00a00)\u2502\u2502 \u2502\"ansi_cyan\"\u2502#008080\u2502rgb(0,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"ansi_green\"\u2502#008000\u2502rgb(0,\u00a0128,\u00a00)\u2502\u2502 \u2502\"ansi_magenta\"\u2502#800080\u2502rgb(128,\u00a00,\u00a0128)\u2502\u2502 \u2502\"ansi_red\"\u2502#800000\u2502rgb(128,\u00a00,\u00a00)\u2502\u2502 \u2502\"ansi_white\"\u2502#C0C0C0\u2502rgb(192,\u00a0192,\u00a0192)\u2502\u2502 \u2502\"ansi_yellow\"\u2502#808000\u2502rgb(128,\u00a0128,\u00a00)\u2502\u2502 \u2502\"antiquewhite\"\u2502#FAEBD7\u2502rgb(250,\u00a0235,\u00a0215)\u2502\u2502 \u2502\"aqua\"\u2502#00FFFF\u2502rgb(0,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"aquamarine\"\u2502#7FFFD4\u2502rgb(127,\u00a0255,\u00a0212)\u2502\u2502 \u2502\"azure\"\u2502#F0FFFF\u2502rgb(240,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"beige\"\u2502#F5F5DC\u2502rgb(245,\u00a0245,\u00a0220)\u2502\u2502 \u2502\"bisque\"\u2502#FFE4C4\u2502rgb(255,\u00a0228,\u00a0196)\u2502\u2502 \u2502\"black\"\u2502#000000\u2502rgb(0,\u00a00,\u00a00)\u2502\u2502 \u2502\"blanchedalmond\"\u2502#FFEBCD\u2502rgb(255,\u00a0235,\u00a0205)\u2502\u2502 \u2502\"blue\"\u2502#0000FF\u2502rgb(0,\u00a00,\u00a0255)\u2502\u2502 \u2502\"blueviolet\"\u2502#8A2BE2\u2502rgb(138,\u00a043,\u00a0226)\u2502\u2502 \u2502\"brown\"\u2502#A52A2A\u2502rgb(165,\u00a042,\u00a042)\u2502\u2502 \u2502\"burlywood\"\u2502#DEB887\u2502rgb(222,\u00a0184,\u00a0135)\u2502\u2502 \u2502\"cadetblue\"\u2502#5F9EA0\u2502rgb(95,\u00a0158,\u00a0160)\u2502\u2502 \u2502\"chartreuse\"\u2502#7FFF00\u2502rgb(127,\u00a0255,\u00a00)\u2502\u2502 \u2502\"chocolate\"\u2502#D2691E\u2502rgb(210,\u00a0105,\u00a030)\u2502\u2502 \u2502\"coral\"\u2502#FF7F50\u2502rgb(255,\u00a0127,\u00a080)\u2502\u2502 \u2502\"cornflowerblue\"\u2502#6495ED\u2502rgb(100,\u00a0149,\u00a0237)\u2502\u2502 \u2502\"cornsilk\"\u2502#FFF8DC\u2502rgb(255,\u00a0248,\u00a0220)\u2502\u2502 \u2502\"crimson\"\u2502#DC143C\u2502rgb(220,\u00a020,\u00a060)\u2502\u2502 \u2502\"cyan\"\u2502#00FFFF\u2502rgb(0,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"darkblue\"\u2502#00008B\u2502rgb(0,\u00a00,\u00a0139)\u2502\u2502 \u2502\"darkcyan\"\u2502#008B8B\u2502rgb(0,\u00a0139,\u00a0139)\u2502\u2502 \u2502\"darkgoldenrod\"\u2502#B8860B\u2502rgb(184,\u00a0134,\u00a011)\u2502\u2502 \u2502\"darkgray\"\u2502#A9A9A9\u2502rgb(169,\u00a0169,\u00a0169)\u2502\u2502 \u2502\"darkgreen\"\u2502#006400\u2502rgb(0,\u00a0100,\u00a00)\u2502\u2502 \u2502\"darkgrey\"\u2502#A9A9A9\u2502rgb(169,\u00a0169,\u00a0169)\u2502\u2502 \u2502\"darkkhaki\"\u2502#BDB76B\u2502rgb(189,\u00a0183,\u00a0107)\u2502\u2502 \u2502\"darkmagenta\"\u2502#8B008B\u2502rgb(139,\u00a00,\u00a0139)\u2502\u2502 \u2502\"darkolivegreen\"\u2502#556B2F\u2502rgb(85,\u00a0107,\u00a047)\u2502\u2502 \u2502\"darkorange\"\u2502#FF8C00\u2502rgb(255,\u00a0140,\u00a00)\u2502\u2502 \u2502\"darkorchid\"\u2502#9932CC\u2502rgb(153,\u00a050,\u00a0204)\u2502\u2502 \u2502\"darkred\"\u2502#8B0000\u2502rgb(139,\u00a00,\u00a00)\u2502\u2502 \u2502\"darksalmon\"\u2502#E9967A\u2502rgb(233,\u00a0150,\u00a0122)\u2502\u2502 \u2502\"darkseagreen\"\u2502#8FBC8F\u2502rgb(143,\u00a0188,\u00a0143)\u2502\u2502 \u2502\"darkslateblue\"\u2502#483D8B\u2502rgb(72,\u00a061,\u00a0139)\u2502\u2502 \u2502\"darkslategray\"\u2502#2F4F4F\u2502rgb(47,\u00a079,\u00a079)\u2502\u2502 \u2502\"darkslategrey\"\u2502#2F4F4F\u2502rgb(47,\u00a079,\u00a079)\u2502\u2502 \u2502\"darkturquoise\"\u2502#00CED1\u2502rgb(0,\u00a0206,\u00a0209)\u2502\u2502 \u2502\"darkviolet\"\u2502#9400D3\u2502rgb(148,\u00a00,\u00a0211)\u2502\u2502 \u2502\"deeppink\"\u2502#FF1493\u2502rgb(255,\u00a020,\u00a0147)\u2502\u2502 \u2502\"deepskyblue\"\u2502#00BFFF\u2502rgb(0,\u00a0191,\u00a0255)\u2502\u2502 \u2502\"dimgray\"\u2502#696969\u2502rgb(105,\u00a0105,\u00a0105)\u2502\u2502 \u2502\"dimgrey\"\u2502#696969\u2502rgb(105,\u00a0105,\u00a0105)\u2502\u2502 \u2502\"dodgerblue\"\u2502#1E90FF\u2502rgb(30,\u00a0144,\u00a0255)\u2502\u2502 \u2502\"firebrick\"\u2502#B22222\u2502rgb(178,\u00a034,\u00a034)\u2502\u2502 \u2502\"floralwhite\"\u2502#FFFAF0\u2502rgb(255,\u00a0250,\u00a0240)\u2502\u2502 \u2502\"forestgreen\"\u2502#228B22\u2502rgb(34,\u00a0139,\u00a034)\u2502\u2502 \u2502\"fuchsia\"\u2502#FF00FF\u2502rgb(255,\u00a00,\u00a0255)\u2502\u2502 \u2502\"gainsboro\"\u2502#DCDCDC\u2502rgb(220,\u00a0220,\u00a0220)\u2502\u2502 \u2502\"ghostwhite\"\u2502#F8F8FF\u2502rgb(248,\u00a0248,\u00a0255)\u2502\u2502 \u2502\"gold\"\u2502#FFD700\u2502rgb(255,\u00a0215,\u00a00)\u2502\u2502 \u2502\"goldenrod\"\u2502#DAA520\u2502rgb(218,\u00a0165,\u00a032)\u2502\u2502 \u2502\"gray\"\u2502#808080\u2502rgb(128,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"green\"\u2502#008000\u2502rgb(0,\u00a0128,\u00a00)\u2502\u2502 \u2502\"greenyellow\"\u2502#ADFF2F\u2502rgb(173,\u00a0255,\u00a047)\u2502\u2502 \u2502\"grey\"\u2502#808080\u2502rgb(128,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"honeydew\"\u2502#F0FFF0\u2502rgb(240,\u00a0255,\u00a0240)\u2502\u2502 \u2502\"hotpink\"\u2502#FF69B4\u2502rgb(255,\u00a0105,\u00a0180)\u2502\u2502 \u2502\"indianred\"\u2502#CD5C5C\u2502rgb(205,\u00a092,\u00a092)\u2502\u2502 \u2502\"indigo\"\u2502#4B0082\u2502rgb(75,\u00a00,\u00a0130)\u2502\u2502 \u2502\"ivory\"\u2502#FFFFF0\u2502rgb(255,\u00a0255,\u00a0240)\u2502\u2502 \u2502\"khaki\"\u2502#F0E68C\u2502rgb(240,\u00a0230,\u00a0140)\u2502\u2502 \u2502\"lavender\"\u2502#E6E6FA\u2502rgb(230,\u00a0230,\u00a0250)\u2502\u2502 \u2502\"lavenderblush\"\u2502#FFF0F5\u2502rgb(255,\u00a0240,\u00a0245)\u2502\u2502 \u2502\"lawngreen\"\u2502#7CFC00\u2502rgb(124,\u00a0252,\u00a00)\u2502\u2502 \u2502\"lemonchiffon\"\u2502#FFFACD\u2502rgb(255,\u00a0250,\u00a0205)\u2502\u2502 \u2502\"lightblue\"\u2502#ADD8E6\u2502rgb(173,\u00a0216,\u00a0230)\u2502\u2502 \u2502\"lightcoral\"\u2502#F08080\u2502rgb(240,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"lightcyan\"\u2502#E0FFFF\u2502rgb(224,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"lightgoldenrodyellow\"\u2502#FAFAD2\u2502rgb(250,\u00a0250,\u00a0210)\u2502\u2502 \u2502\"lightgray\"\u2502#D3D3D3\u2502rgb(211,\u00a0211,\u00a0211)\u2502\u2502 \u2502\"lightgreen\"\u2502#90EE90\u2502rgb(144,\u00a0238,\u00a0144)\u2502\u2502 \u2502\"lightgrey\"\u2502#D3D3D3\u2502rgb(211,\u00a0211,\u00a0211)\u2502\u2502 \u2502\"lightpink\"\u2502#FFB6C1\u2502rgb(255,\u00a0182,\u00a0193)\u2502\u2502 \u2502\"lightsalmon\"\u2502#FFA07A\u2502rgb(255,\u00a0160,\u00a0122)\u2502\u2502 \u2502\"lightseagreen\"\u2502#20B2AA\u2502rgb(32,\u00a0178,\u00a0170)\u2502\u2502 \u2502\"lightskyblue\"\u2502#87CEFA\u2502rgb(135,\u00a0206,\u00a0250)\u2502\u2502 \u2502\"lightslategray\"\u2502#778899\u2502rgb(119,\u00a0136,\u00a0153)\u2502\u2502 \u2502\"lightslategrey\"\u2502#778899\u2502rgb(119,\u00a0136,\u00a0153)\u2502\u2502 \u2502\"lightsteelblue\"\u2502#B0C4DE\u2502rgb(176,\u00a0196,\u00a0222)\u2502\u2502 \u2502\"lightyellow\"\u2502#FFFFE0\u2502rgb(255,\u00a0255,\u00a0224)\u2502\u2502 \u2502\"lime\"\u2502#00FF00\u2502rgb(0,\u00a0255,\u00a00)\u2502\u2502 \u2502\"limegreen\"\u2502#32CD32\u2502rgb(50,\u00a0205,\u00a050)\u2502\u2502 \u2502\"linen\"\u2502#FAF0E6\u2502rgb(250,\u00a0240,\u00a0230)\u2502\u2502 \u2502\"magenta\"\u2502#FF00FF\u2502rgb(255,\u00a00,\u00a0255)\u2502\u2502 \u2502\"maroon\"\u2502#800000\u2502rgb(128,\u00a00,\u00a00)\u2502\u2502 \u2502\"mediumaquamarine\"\u2502#66CDAA\u2502rgb(102,\u00a0205,\u00a0170)\u2502\u2502 \u2502\"mediumblue\"\u2502#0000CD\u2502rgb(0,\u00a00,\u00a0205)\u2502\u2502 \u2502\"mediumorchid\"\u2502#BA55D3\u2502rgb(186,\u00a085,\u00a0211)\u2502\u2502 \u2502\"mediumpurple\"\u2502#9370DB\u2502rgb(147,\u00a0112,\u00a0219)\u2502\u2502 \u2502\"mediumseagreen\"\u2502#3CB371\u2502rgb(60,\u00a0179,\u00a0113)\u2502\u2502 \u2502\"mediumslateblue\"\u2502#7B68EE\u2502rgb(123,\u00a0104,\u00a0238)\u2502\u2502 \u2502\"mediumspringgreen\"\u2502#00FA9A\u2502rgb(0,\u00a0250,\u00a0154)\u2502\u2502 \u2502\"mediumturquoise\"\u2502#48D1CC\u2502rgb(72,\u00a0209,\u00a0204)\u2502\u2502 \u2502\"mediumvioletred\"\u2502#C71585\u2502rgb(199,\u00a021,\u00a0133)\u2502\u2502 \u2502\"midnightblue\"\u2502#191970\u2502rgb(25,\u00a025,\u00a0112)\u2502\u2502 \u2502\"mintcream\"\u2502#F5FFFA\u2502rgb(245,\u00a0255,\u00a0250)\u2502\u2502 \u2502\"mistyrose\"\u2502#FFE4E1\u2502rgb(255,\u00a0228,\u00a0225)\u2502\u2502 \u2502\"moccasin\"\u2502#FFE4B5\u2502rgb(255,\u00a0228,\u00a0181)\u2502\u2502 \u2502\"navajowhite\"\u2502#FFDEAD\u2502rgb(255,\u00a0222,\u00a0173)\u2502\u2502 \u2502\"navy\"\u2502#000080\u2502rgb(0,\u00a00,\u00a0128)\u2502\u2502 \u2502\"oldlace\"\u2502#FDF5E6\u2502rgb(253,\u00a0245,\u00a0230)\u2502\u2502 \u2502\"olive\"\u2502#808000\u2502rgb(128,\u00a0128,\u00a00)\u2502\u2502 \u2502\"olivedrab\"\u2502#6B8E23\u2502rgb(107,\u00a0142,\u00a035)\u2502\u2502 \u2502\"orange\"\u2502#FFA500\u2502rgb(255,\u00a0165,\u00a00)\u2502\u2502 \u2502\"orangered\"\u2502#FF4500\u2502rgb(255,\u00a069,\u00a00)\u2502\u2502 \u2502\"orchid\"\u2502#DA70D6\u2502rgb(218,\u00a0112,\u00a0214)\u2502\u2502 \u2502\"palegoldenrod\"\u2502#EEE8AA\u2502rgb(238,\u00a0232,\u00a0170)\u2502\u2502 \u2502\"palegreen\"\u2502#98FB98\u2502rgb(152,\u00a0251,\u00a0152)\u2502\u2502 \u2502\"paleturquoise\"\u2502#AFEEEE\u2502rgb(175,\u00a0238,\u00a0238)\u2502\u2502 \u2502\"palevioletred\"\u2502#DB7093\u2502rgb(219,\u00a0112,\u00a0147)\u2502\u2502 \u2502\"papayawhip\"\u2502#FFEFD5\u2502rgb(255,\u00a0239,\u00a0213)\u2502\u2502 \u2502\"peachpuff\"\u2502#FFDAB9\u2502rgb(255,\u00a0218,\u00a0185)\u2502\u2502 \u2502\"peru\"\u2502#CD853F\u2502rgb(205,\u00a0133,\u00a063)\u2502\u2502 \u2502\"pink\"\u2502#FFC0CB\u2502rgb(255,\u00a0192,\u00a0203)\u2502\u2502 \u2502\"plum\"\u2502#DDA0DD\u2502rgb(221,\u00a0160,\u00a0221)\u2502\u2502 \u2502\"powderblue\"\u2502#B0E0E6\u2502rgb(176,\u00a0224,\u00a0230)\u2502\u2502 \u2502\"purple\"\u2502#800080\u2502rgb(128,\u00a00,\u00a0128)\u2502\u2502 \u2502\"rebeccapurple\"\u2502#663399\u2502rgb(102,\u00a051,\u00a0153)\u2502\u2502 \u2502\"red\"\u2502#FF0000\u2502rgb(255,\u00a00,\u00a00)\u2502\u2502 \u2502\"rosybrown\"\u2502#BC8F8F\u2502rgb(188,\u00a0143,\u00a0143)\u2502\u2502 \u2502\"royalblue\"\u2502#4169E1\u2502rgb(65,\u00a0105,\u00a0225)\u2502\u2502 \u2502\"saddlebrown\"\u2502#8B4513\u2502rgb(139,\u00a069,\u00a019)\u2502\u2502 \u2502\"salmon\"\u2502#FA8072\u2502rgb(250,\u00a0128,\u00a0114)\u2502\u2502 \u2502\"sandybrown\"\u2502#F4A460\u2502rgb(244,\u00a0164,\u00a096)\u2502\u2502 \u2502\"seagreen\"\u2502#2E8B57\u2502rgb(46,\u00a0139,\u00a087)\u2502\u2502 \u2502\"seashell\"\u2502#FFF5EE\u2502rgb(255,\u00a0245,\u00a0238)\u2502\u2502 \u2502\"sienna\"\u2502#A0522D\u2502rgb(160,\u00a082,\u00a045)\u2502\u2502 \u2502\"silver\"\u2502#C0C0C0\u2502rgb(192,\u00a0192,\u00a0192)\u2502\u2502 \u2502\"skyblue\"\u2502#87CEEB\u2502rgb(135,\u00a0206,\u00a0235)\u2502\u2502 \u2502\"slateblue\"\u2502#6A5ACD\u2502rgb(106,\u00a090,\u00a0205)\u2502\u2502 \u2502\"slategray\"\u2502#708090\u2502rgb(112,\u00a0128,\u00a0144)\u2502\u2502 \u2502\"slategrey\"\u2502#708090\u2502rgb(112,\u00a0128,\u00a0144)\u2502\u2502 \u2502\"snow\"\u2502#FFFAFA\u2502rgb(255,\u00a0250,\u00a0250)\u2502\u2502 \u2502\"springgreen\"\u2502#00FF7F\u2502rgb(0,\u00a0255,\u00a0127)\u2502\u2502 \u2502\"steelblue\"\u2502#4682B4\u2502rgb(70,\u00a0130,\u00a0180)\u2502\u2502 \u2502\"tan\"\u2502#D2B48C\u2502rgb(210,\u00a0180,\u00a0140)\u2502\u2502 \u2502\"teal\"\u2502#008080\u2502rgb(0,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"thistle\"\u2502#D8BFD8\u2502rgb(216,\u00a0191,\u00a0216)\u2502\u2502 \u2502\"tomato\"\u2502#FF6347\u2502rgb(255,\u00a099,\u00a071)\u2502\u2502 \u2502\"turquoise\"\u2502#40E0D0\u2502rgb(64,\u00a0224,\u00a0208)\u2502\u2502 \u2502\"violet\"\u2502#EE82EE\u2502rgb(238,\u00a0130,\u00a0238)\u2502\u2502 \u2502\"wheat\"\u2502#F5DEB3\u2502rgb(245,\u00a0222,\u00a0179)\u2502\u2502 \u2502\"white\"\u2502#FFFFFF\u2502rgb(255,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"whitesmoke\"\u2502#F5F5F5\u2502rgb(245,\u00a0245,\u00a0245)\u2502\u2502 \u2502\"yellow\"\u2502#FFFF00\u2502rgb(255,\u00a0255,\u00a00)\u2502\u2502 \u2502\"yellowgreen\"\u2502#9ACD32\u2502rgb(154,\u00a0205,\u00a050)\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

"},{"location":"api/color/#textual.color.BLACK","title":"BLACK module-attribute","text":"
BLACK: Final = Color(0, 0, 0)\n

A constant for pure black.

"},{"location":"api/color/#textual.color.WHITE","title":"WHITE module-attribute","text":"
WHITE: Final = Color(255, 255, 255)\n

A constant for pure white.

"},{"location":"api/color/#textual.color.Color","title":"Color class","text":"

Bases: NamedTuple

A class to represent a color.

Colors are stored as three values representing the degree of red, green, and blue in a color, and a fourth \"alpha\" value which defines where the color lies on a gradient of opaque to transparent.

Example
>>> from textual.color import Color\n>>> color = Color.parse(\"red\")\n>>> color\nColor(255, 0, 0)\n>>> color.darken(0.5)\nColor(98, 0, 0)\n>>> color + Color.parse(\"green\")\nColor(0, 128, 0)\n>>> color_with_alpha = Color(100, 50, 25, 0.5)\n>>> color_with_alpha\nColor(100, 50, 25, a=0.5)\n>>> color + color_with_alpha\nColor(177, 25, 12)\n
"},{"location":"api/color/#textual.color.Color.a","title":"a instance-attribute class-attribute","text":"
a: float = 1.0\n

Alpha (opacity) component in range 0 to 1.

"},{"location":"api/color/#textual.color.Color.b","title":"b instance-attribute","text":"
b: int\n

Blue component in range 0 to 255.

"},{"location":"api/color/#textual.color.Color.brightness","title":"brightness property","text":"
brightness: float\n

The human perceptual brightness.

A value of 1 is returned for pure white, and 0 for pure black. Other colors lie on a gradient between the two extremes.

"},{"location":"api/color/#textual.color.Color.clamped","title":"clamped property","text":"
clamped: Color\n

A clamped color (this color with all values in expected range).

"},{"location":"api/color/#textual.color.Color.css","title":"css property","text":"
css: str\n

The color in CSS RGB or RGBA form.

For example, \"rgb(10,20,30)\" for an RGB color, or \"rgb(50,70,80,0.5)\" for an RGBA color.

"},{"location":"api/color/#textual.color.Color.g","title":"g instance-attribute","text":"
g: int\n

Green component in range 0 to 255.

"},{"location":"api/color/#textual.color.Color.hex","title":"hex property","text":"
hex: str\n

The color in CSS hex form, with 6 digits for RGB, and 8 digits for RGBA.

For example, \"#46b3de\" for an RGB color, or \"#3342457f\" for a color with alpha.

"},{"location":"api/color/#textual.color.Color.hex6","title":"hex6 property","text":"
hex6: str\n

The color in CSS hex form, with 6 digits for RGB. Alpha is ignored.

For example, \"#46b3de\".

"},{"location":"api/color/#textual.color.Color.hsl","title":"hsl property","text":"
hsl: HSL\n

This color in HSL format.

HSL color is an alternative way of representing a color, which can be used in certain color calculations.

Returns Type Description HSL

Color encoded in HSL format.

"},{"location":"api/color/#textual.color.Color.inverse","title":"inverse property","text":"
inverse: Color\n

The inverse of this color.

Returns Type Description Color

Inverse color.

"},{"location":"api/color/#textual.color.Color.is_transparent","title":"is_transparent property","text":"
is_transparent: bool\n

Is the color transparent (i.e. has 0 alpha)?

"},{"location":"api/color/#textual.color.Color.monochrome","title":"monochrome property","text":"
monochrome: Color\n

A monochrome version of this color.

Returns Type Description Color

The monochrome (black and white) version of this color.

"},{"location":"api/color/#textual.color.Color.normalized","title":"normalized property","text":"
normalized: tuple[float, float, float]\n

A tuple of the color components normalized to between 0 and 1.

Returns Type Description tuple[float, float, float]

Normalized components.

"},{"location":"api/color/#textual.color.Color.r","title":"r instance-attribute","text":"
r: int\n

Red component in range 0 to 255.

"},{"location":"api/color/#textual.color.Color.rgb","title":"rgb property","text":"
rgb: tuple[int, int, int]\n

The red, green, and blue color components as a tuple of ints.

"},{"location":"api/color/#textual.color.Color.rich_color","title":"rich_color property","text":"
rich_color: RichColor\n

This color encoded in Rich's Color class.

Returns Type Description RichColor

A color object as used by Rich.

"},{"location":"api/color/#textual.color.Color.blend","title":"blend cached","text":"
def blend(self, destination, factor, alpha=None):\n

Generate a new color between two colors.

This method calculates a new color on a gradient. The position on the gradient is given by factor, which is a float between 0 and 1, where 0 is the original color, and 1 is the destination color. A value of gradient between the two extremes produces a color somewhere between the two end points.

Parameters Name Type Description Default destination Color

Another color.

required factor float

A blend factor, 0 -> 1.

required alpha float | None

New alpha for result.

None Returns Type Description Color

A new color.

"},{"location":"api/color/#textual.color.Color.darken","title":"darken cached","text":"
def darken(self, amount, alpha=None):\n

Darken the color by a given amount.

Parameters Name Type Description Default amount float

Value between 0-1 to reduce luminance by.

required alpha float | None

Alpha component for new color or None to copy alpha.

None Returns Type Description Color

New color.

"},{"location":"api/color/#textual.color.Color.from_hsl","title":"from_hsl classmethod","text":"
def from_hsl(cls, h, s, l):\n

Create a color from HLS components.

Parameters Name Type Description Default h float

Hue.

required l float

Lightness.

required s float

Saturation.

required Returns Type Description Color

A new color.

"},{"location":"api/color/#textual.color.Color.from_rich_color","title":"from_rich_color classmethod","text":"
def from_rich_color(cls, rich_color):\n

Create a new color from Rich's Color class.

Parameters Name Type Description Default rich_color RichColor

An instance of Rich color.

required Returns Type Description Color

A new Color instance.

"},{"location":"api/color/#textual.color.Color.get_contrast_text","title":"get_contrast_text cached","text":"
def get_contrast_text(self, alpha=0.95):\n

Get a light or dark color that best contrasts this color, for use with text.

Parameters Name Type Description Default alpha float

An alpha value to apply to the result.

0.95 Returns Type Description Color

A new color, either an off-white or off-black.

"},{"location":"api/color/#textual.color.Color.lighten","title":"lighten method","text":"
def lighten(self, amount, alpha=None):\n

Lighten the color by a given amount.

Parameters Name Type Description Default amount float

Value between 0-1 to increase luminance by.

required alpha float | None

Alpha component for new color or None to copy alpha.

None Returns Type Description Color

New color.

"},{"location":"api/color/#textual.color.Color.multiply_alpha","title":"multiply_alpha method","text":"
def multiply_alpha(self, alpha):\n

Create a new color, multiplying the alpha by a constant.

Parameters Name Type Description Default alpha float

A value to multiple the alpha by (expected to be in the range 0 to 1).

required Returns Type Description Color

A new color.

"},{"location":"api/color/#textual.color.Color.parse","title":"parse cached classmethod","text":"
def parse(cls, color_text):\n

Parse a string containing a named color or CSS-style color.

Colors may be parsed from the following formats:

  • Text beginning with a # is parsed as a hexadecimal color code, where R, G, B, and A must be hexadecimal digits (0-9A-F):

    • #RGB
    • #RGBA
    • #RRGGBB
    • #RRGGBBAA
  • Alternatively, RGB colors can also be specified in the format that follows, where R, G, and B must be numbers between 0 and 255 and A must be a value between 0 and 1:

    • rgb(R,G,B)
    • rgb(R,G,B,A)
  • The HSL model can also be used, with a syntax similar to the above, if H is a value between 0 and 360, S and L are percentages, and A is a value between 0 and 1:

    • hsl(H,S,L)
    • hsla(H,S,L,A)

Any other formats will raise a ColorParseError.

Parameters Name Type Description Default color_text str | Color

Text with a valid color format. Color objects will be returned unmodified.

required Raises Type Description ColorParseError

If the color is not encoded correctly.

Returns Type Description Color

Instance encoding the color specified by the argument.

"},{"location":"api/color/#textual.color.Color.with_alpha","title":"with_alpha method","text":"
def with_alpha(self, alpha):\n

Create a new color with the given alpha.

Parameters Name Type Description Default alpha float

New value for alpha.

required Returns Type Description Color

A new color.

"},{"location":"api/color/#textual.color.ColorParseError","title":"ColorParseError class","text":"
def __init__(self, message, suggested_color=None):\n

Bases: Exception

A color failed to parse.

Parameters Name Type Description Default message str

The error message

required suggested_color str | None

A close color we can suggest.

None"},{"location":"api/color/#textual.color.Gradient","title":"Gradient class","text":"
def __init__(self, *stops):\n

Defines a color gradient.

A gradient is defined by a sequence of \"stops\" consisting of a float and a color. The stop indicate the color at that point on a spectrum between 0 and 1.

Parameters Name Type Description Default stops tuple[float, Color]

A colors stop.

() Raises Type Description ValueError

If any stops are missing (must be at least a stop for 0 and 1).

"},{"location":"api/color/#textual.color.Gradient.get_color","title":"get_color method","text":"
def get_color(self, position):\n

Get a color from the gradient at a position between 0 and 1.

Positions that are between stops will return a blended color.

Parameters Name Type Description Default position float

A number between 0 and 1, where 0 is the first stop, and 1 is the last.

required Returns Type Description Color

A color.

"},{"location":"api/color/#textual.color.HSL","title":"HSL class","text":"

Bases: NamedTuple

A color in HLS (Hue, Saturation, Lightness) format.

"},{"location":"api/color/#textual.color.HSL.css","title":"css property","text":"
css: str\n

HSL in css format.

"},{"location":"api/color/#textual.color.HSL.h","title":"h instance-attribute","text":"
h: float\n

Hue in range 0 to 1.

"},{"location":"api/color/#textual.color.HSL.l","title":"l instance-attribute","text":"
l: float\n

Lightness in range 0 to 1.

"},{"location":"api/color/#textual.color.HSL.s","title":"s instance-attribute","text":"
s: float\n

Saturation in range 0 to 1.

"},{"location":"api/color/#textual.color.HSV","title":"HSV class","text":"

Bases: NamedTuple

A color in HSV (Hue, Saturation, Value) format.

"},{"location":"api/color/#textual.color.HSV.h","title":"h instance-attribute","text":"
h: float\n

Hue in range 0 to 1.

"},{"location":"api/color/#textual.color.HSV.s","title":"s instance-attribute","text":"
s: float\n

Saturation in range 0 to 1.

"},{"location":"api/color/#textual.color.HSV.v","title":"v instance-attribute","text":"
v: float\n

Value un range 0 to 1.

"},{"location":"api/color/#textual.color.Lab","title":"Lab class","text":"

Bases: NamedTuple

A color in CIE-L*ab format.

"},{"location":"api/color/#textual.color.Lab.L","title":"L instance-attribute","text":"
L: float\n

Lightness in range 0 to 100.

"},{"location":"api/color/#textual.color.Lab.a","title":"a instance-attribute","text":"
a: float\n

A axis in range -127 to 128.

"},{"location":"api/color/#textual.color.Lab.b","title":"b instance-attribute","text":"
b: float\n

B axis in range -127 to 128.

"},{"location":"api/color/#textual.color.lab_to_rgb","title":"lab_to_rgb function","text":"
def lab_to_rgb(lab, alpha=1.0):\n

Convert a CIE-L*ab color to RGB.

Uses the standard RGB color space with a D65/2\u2070 standard illuminant. Conversion passes through the XYZ color space. Cf. http://www.easyrgb.com/en/math.php.

"},{"location":"api/color/#textual.color.rgb_to_lab","title":"rgb_to_lab function","text":"
def rgb_to_lab(rgb):\n

Convert an RGB color to the CIE-L*ab format.

Uses the standard RGB color space with a D65/2\u2070 standard illuminant. Conversion passes through the XYZ color space. Cf. http://www.easyrgb.com/en/math.php.

"},{"location":"api/command/","title":"Command","text":"

The Textual command palette.

See the guide on the Command Palette for full details.

"},{"location":"api/command/#textual.command.Hits","title":"Hits module-attribute","text":"
Hits: TypeAlias = AsyncIterator[Hit]\n

Return type for the command provider's search method.

"},{"location":"api/command/#textual.command.Command","title":"Command class","text":"
def __init__(self, prompt, command, id=None, disabled=False):\n

Bases: Option

Class that holds a command in the CommandList.

Parameters Name Type Description Default prompt RenderableType

The prompt for the option.

required command Hit

The details of the command associated with the option.

required id str | None

The optional ID for the option.

None disabled bool

The initial enabled/disabled state. Enabled by default.

False"},{"location":"api/command/#textual.command.Command.command","title":"command instance-attribute","text":"
command = command\n

The details of the command associated with the option.

"},{"location":"api/command/#textual.command.CommandInput","title":"CommandInput class","text":"

Bases: Input

The command palette input control.

"},{"location":"api/command/#textual.command.CommandList","title":"CommandList class","text":"

Bases: OptionList

The command palette command list.

"},{"location":"api/command/#textual.command.CommandPalette","title":"CommandPalette class","text":"
def __init__(self):\n

Bases: ModalScreen[CallbackType]

The Textual command palette.

"},{"location":"api/command/#textual.command.CommandPalette.BINDINGS","title":"BINDINGS class-attribute","text":"
BINDINGS: list[BindingType] = [\nBinding(\n\"ctrl+end, shift+end\",\n\"command_list('last')\",\nshow=False,\n),\nBinding(\n\"ctrl+home, shift+home\",\n\"command_list('first')\",\nshow=False,\n),\nBinding(\"down\", \"cursor_down\", show=False),\nBinding(\"escape\", \"escape\", \"Exit the command palette\"),\nBinding(\n\"pagedown\", \"command_list('page_down')\", show=False\n),\nBinding(\n\"pageup\", \"command_list('page_up')\", show=False\n),\nBinding(\"up\", \"command_list('cursor_up')\", show=False),\n]\n
Key(s) Description ctrl+end, shift+end Jump to the last available commands. ctrl+home, shift+home Jump to the first available commands. down Navigate down through the available commands. escape Exit the command palette. pagedown Navigate down a page through the available commands. pageup Navigate up a page through the available commands. up Navigate up through the available commands."},{"location":"api/command/#textual.command.CommandPalette.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
COMPONENT_CLASSES: set[str] = {\n\"command-palette--help-text\",\n\"command-palette--highlight\",\n}\n
Class Description command-palette--help-text Targets the help text of a matched command. command-palette--highlight Targets the highlights of a matched command."},{"location":"api/command/#textual.command.CommandPalette.run_on_select","title":"run_on_select class-attribute","text":"
run_on_select: bool = True\n

A flag to say if a command should be run when selected by the user.

If True then when a user hits Enter on a command match in the result list, or if they click on one with the mouse, the command will be selected and run. If set to False the input will be filled with the command and then Enter should be pressed on the keyboard or the 'go' button should be pressed.

"},{"location":"api/command/#textual.command.CommandPalette.is_open","title":"is_open staticmethod","text":"
def is_open(app):\n

Is the command palette current open?

Parameters Name Type Description Default app App

The app to test.

required Returns Type Description bool

True if the command palette is currently open, False if not.

"},{"location":"api/command/#textual.command.CommandPalette.on_mount","title":"on_mount method","text":"
def on_mount(self, _):\n

Capture the calling screen.

"},{"location":"api/command/#textual.command.CommandPalette.on_unmount","title":"on_unmount async","text":"
def on_unmount(self):\n

Shutdown providers when command palette is closed.

"},{"location":"api/command/#textual.command.Hit","title":"Hit class","text":"

Holds the details of a single command search hit.

"},{"location":"api/command/#textual.command.Hit.command","title":"command instance-attribute","text":"
command: IgnoreReturnCallbackType\n

The function to call when the command is chosen.

"},{"location":"api/command/#textual.command.Hit.help","title":"help instance-attribute class-attribute","text":"
help: str | None = None\n

Optional help text for the command.

"},{"location":"api/command/#textual.command.Hit.match_display","title":"match_display instance-attribute","text":"
match_display: RenderableType\n

A string or Rich renderable representation of the hit.

"},{"location":"api/command/#textual.command.Hit.score","title":"score instance-attribute","text":"
score: float\n

The score of the command hit.

The value should be between 0 (no match) and 1 (complete match).

"},{"location":"api/command/#textual.command.Hit.text","title":"text instance-attribute class-attribute","text":"
text: str | None = None\n

The command text associated with the hit, as plain text.

If match_display is not simple text, this attribute should be provided by the Provider object.

"},{"location":"api/command/#textual.command.Matcher","title":"Matcher class","text":"
def __init__(\nself, query, *, match_style=None, case_sensitive=False\n):\n

A fuzzy matcher.

Parameters Name Type Description Default query str

A query as typed in by the user.

required match_style Style | None

The style to use to highlight matched portions of a string.

None case_sensitive bool

Should matching be case sensitive?

False"},{"location":"api/command/#textual.fuzzy.Matcher.case_sensitive","title":"case_sensitive property","text":"
case_sensitive: bool\n

Is this matcher case sensitive?

"},{"location":"api/command/#textual.fuzzy.Matcher.match_style","title":"match_style property","text":"
match_style: Style\n

The style that will be used to highlight hits in the matched text.

"},{"location":"api/command/#textual.fuzzy.Matcher.query","title":"query property","text":"
query: str\n

The query string to look for.

"},{"location":"api/command/#textual.fuzzy.Matcher.query_pattern","title":"query_pattern property","text":"
query_pattern: str\n

The regular expression pattern built from the query.

"},{"location":"api/command/#textual.fuzzy.Matcher.highlight","title":"highlight method","text":"
def highlight(self, candidate):\n

Highlight the candidate with the fuzzy match.

Parameters Name Type Description Default candidate str

The candidate string to match against the query.

required Returns Type Description Text

A [rich.text.Text][Text] object with highlighted matches.

"},{"location":"api/command/#textual.fuzzy.Matcher.match","title":"match method","text":"
def match(self, candidate):\n

Match the candidate against the query.

Parameters Name Type Description Default candidate str

Candidate string to match against the query.

required Returns Type Description float

Strength of the match from 0 to 1.

"},{"location":"api/command/#textual.command.Provider","title":"Provider class","text":"
def __init__(self, screen, match_style=None):\n

Bases: ABC

Base class for command palette command providers.

To create new command provider, inherit from this class and implement search.

Parameters Name Type Description Default screen Screen[Any]

A reference to the active screen.

required"},{"location":"api/command/#textual.command.Provider.app","title":"app property","text":"
app: App[object]\n

A reference to the application.

"},{"location":"api/command/#textual.command.Provider.focused","title":"focused property","text":"
focused: Widget | None\n

The currently-focused widget in the currently-active screen in the application.

If no widget has focus this will be None.

"},{"location":"api/command/#textual.command.Provider.match_style","title":"match_style property","text":"
match_style: Style | None\n

The preferred style to use when highlighting matching portions of the match_display.

"},{"location":"api/command/#textual.command.Provider.screen","title":"screen property","text":"
screen: Screen[object]\n

The currently-active screen in the application.

"},{"location":"api/command/#textual.command.Provider.matcher","title":"matcher method","text":"
def matcher(self, user_input, case_sensitive=False):\n

Create a fuzzy matcher for the given user input.

Parameters Name Type Description Default user_input str

The text that the user has input.

required case_sensitive bool

Should matching be case sensitive?

False Returns Type Description Matcher

A fuzzy matcher object for matching against candidate hits.

"},{"location":"api/command/#textual.command.Provider.search","title":"search async abstractmethod","text":"
def search(self, query):\n

A request to search for commands relevant to the given query.

Parameters Name Type Description Default query str

The user input to be matched.

required

Yields:

Type Description Hits

Instances of Hit.

"},{"location":"api/command/#textual.command.Provider.shutdown","title":"shutdown async","text":"
def shutdown(self):\n

Called when the Provider is shutdown.

Use this method to perform an cleanup, if required.

"},{"location":"api/command/#textual.command.Provider.startup","title":"startup async","text":"
def startup(self):\n

Called after the Provider is initialized, but before any calls to search.

"},{"location":"api/command/#textual.command.SearchIcon","title":"SearchIcon class","text":"

Bases: Static

Widget for displaying a search icon before the command input.

"},{"location":"api/command/#textual.command.SearchIcon.icon","title":"icon instance-attribute class-attribute","text":"
icon: var[str] = var(\nEmoji.replace(\":magnifying_glass_tilted_right:\")\n)\n

The icon to display.

"},{"location":"api/containers/","title":"Containers","text":"

Container widgets for quick styling.

With the exception of Center and Middle containers will fill all of the space in the parent widget.

"},{"location":"api/containers/#textual.containers.Center","title":"Center class","text":"

Bases: Widget

A container which aligns children on the X axis.

"},{"location":"api/containers/#textual.containers.Container","title":"Container class","text":"

Bases: Widget

Simple container widget, with vertical layout.

"},{"location":"api/containers/#textual.containers.Grid","title":"Grid class","text":"

Bases: Widget

A container with grid layout.

"},{"location":"api/containers/#textual.containers.Horizontal","title":"Horizontal class","text":"

Bases: Widget

A container with horizontal layout and no scrollbars.

"},{"location":"api/containers/#textual.containers.HorizontalScroll","title":"HorizontalScroll class","text":"

Bases: ScrollableContainer

A container with horizontal layout and an automatic scrollbar on the Y axis.

"},{"location":"api/containers/#textual.containers.Middle","title":"Middle class","text":"

Bases: Widget

A container which aligns children on the Y axis.

"},{"location":"api/containers/#textual.containers.ScrollableContainer","title":"ScrollableContainer class","text":"

Bases: Widget

A scrollable container with vertical layout, and auto scrollbars on both axis.

"},{"location":"api/containers/#textual.containers.ScrollableContainer.BINDINGS","title":"BINDINGS class-attribute","text":"
BINDINGS: list[BindingType] = [\nBinding(\"up\", \"scroll_up\", \"Scroll Up\", show=False),\nBinding(\n\"down\", \"scroll_down\", \"Scroll Down\", show=False\n),\nBinding(\"left\", \"scroll_left\", \"Scroll Up\", show=False),\nBinding(\n\"right\", \"scroll_right\", \"Scroll Right\", show=False\n),\nBinding(\n\"home\", \"scroll_home\", \"Scroll Home\", show=False\n),\nBinding(\"end\", \"scroll_end\", \"Scroll End\", show=False),\nBinding(\"pageup\", \"page_up\", \"Page Up\", show=False),\nBinding(\n\"pagedown\", \"page_down\", \"Page Down\", show=False\n),\n]\n

Keyboard bindings for scrollable containers.

Key(s) Description up Scroll up, if vertical scrolling is available. down Scroll down, if vertical scrolling is available. left Scroll left, if horizontal scrolling is available. right Scroll right, if horizontal scrolling is available. home Scroll to the home position, if scrolling is available. end Scroll to the end position, if scrolling is available. pageup Scroll up one page, if vertical scrolling is available. pagedown Scroll down one page, if vertical scrolling is available."},{"location":"api/containers/#textual.containers.Vertical","title":"Vertical class","text":"

Bases: Widget

A container with vertical layout and no scrollbars.

"},{"location":"api/containers/#textual.containers.VerticalScroll","title":"VerticalScroll class","text":"

Bases: ScrollableContainer

A container with vertical layout and an automatic scrollbar on the Y axis.

"},{"location":"api/content_switcher/","title":"Content switcher","text":""},{"location":"api/content_switcher/#textual.widgets.ContentSwitcher","title":"textual.widgets.ContentSwitcher class","text":"
def __init__(\nself,\n*children,\nname=None,\nid=None,\nclasses=None,\ndisabled=False,\ninitial=None\n):\n

Bases: Container

A widget for switching between different children.

Note

All child widgets that are to be switched between need a unique ID. Children that have no ID will be hidden and ignored.

Parameters Name Type Description Default *children Widget

The widgets to switch between.

() name str | None

The name of the content switcher.

None id str | None

The ID of the content switcher in the DOM.

None classes str | None

The CSS classes of the content switcher.

None disabled bool

Whether the content switcher is disabled or not.

False initial str | None

The ID of the initial widget to show, None or empty string for the first tab.

None Note

If initial is not supplied no children will be shown to start with.

"},{"location":"api/content_switcher/#textual.widgets._content_switcher.ContentSwitcher.current","title":"current instance-attribute class-attribute","text":"
current: reactive[str | None] = reactive[Optional[str]](\nNone, init=False\n)\n

The ID of the currently-displayed widget.

If set to None then no widget is visible.

Note

If set to an unknown ID, this will result in NoMatches being raised.

"},{"location":"api/content_switcher/#textual.widgets._content_switcher.ContentSwitcher.visible_content","title":"visible_content property","text":"
visible_content: Widget | None\n

A reference to the currently-visible widget.

None if nothing is visible.

"},{"location":"api/content_switcher/#textual.widgets._content_switcher.ContentSwitcher.watch_current","title":"watch_current method","text":"
def watch_current(self, old, new):\n

React to the current visible child choice being changed.

Parameters Name Type Description Default old str | None

The old widget ID (or None if there was no widget).

required new str | None

The new widget ID (or None if nothing should be shown).

required"},{"location":"api/coordinate/","title":"Coordinate","text":"

A class to store a coordinate, used by the DataTable.

"},{"location":"api/coordinate/#textual.coordinate.Coordinate","title":"Coordinate class","text":"

Bases: NamedTuple

An object representing a row/column coordinate within a grid.

"},{"location":"api/coordinate/#textual.coordinate.Coordinate.column","title":"column instance-attribute","text":"
column: int\n

The column of the coordinate within a grid.

"},{"location":"api/coordinate/#textual.coordinate.Coordinate.row","title":"row instance-attribute","text":"
row: int\n

The row of the coordinate within a grid.

"},{"location":"api/coordinate/#textual.coordinate.Coordinate.down","title":"down method","text":"
def down(self):\n

Get the coordinate below.

Returns Type Description Coordinate

The coordinate below.

"},{"location":"api/coordinate/#textual.coordinate.Coordinate.left","title":"left method","text":"
def left(self):\n

Get the coordinate to the left.

Returns Type Description Coordinate

The coordinate to the left.

"},{"location":"api/coordinate/#textual.coordinate.Coordinate.right","title":"right method","text":"
def right(self):\n

Get the coordinate to the right.

Returns Type Description Coordinate

The coordinate to the right.

"},{"location":"api/coordinate/#textual.coordinate.Coordinate.up","title":"up method","text":"
def up(self):\n

Get the coordinate above.

Returns Type Description Coordinate

The coordinate above.

"},{"location":"api/dom_node/","title":"Dom node","text":"

A DOMNode is a base class for any object within the Textual Document Object Model, which includes all Widgets, Screens, and Apps.

"},{"location":"api/dom_node/#textual.dom.WalkMethod","title":"WalkMethod module-attribute","text":"
WalkMethod: TypeAlias = Literal['depth', 'breadth']\n

Valid walking methods for the DOMNode.walk_children method.

"},{"location":"api/dom_node/#textual.dom.BadIdentifier","title":"BadIdentifier class","text":"

Bases: Exception

Exception raised if you supply a id attribute or class name in the wrong format.

"},{"location":"api/dom_node/#textual.dom.DOMError","title":"DOMError class","text":"

Bases: Exception

Base exception class for errors relating to the DOM.

"},{"location":"api/dom_node/#textual.dom.DOMNode","title":"DOMNode class","text":"
def __init__(self, *, name=None, id=None, classes=None):\n

Bases: MessagePump

The base class for object that can be in the Textual DOM (App and Widget)

"},{"location":"api/dom_node/#textual.dom.DOMNode.SCOPED_CSS","title":"SCOPED_CSS class-attribute","text":"
SCOPED_CSS: bool = True\n

Should default css be limited to the widget type?

"},{"location":"api/dom_node/#textual.dom.DOMNode.ancestors","title":"ancestors property","text":"
ancestors: list[DOMNode]\n

A list of ancestor nodes Nodes by tracing ancestors all the way back to App.

Returns Type Description list[DOMNode]

A list of nodes.

"},{"location":"api/dom_node/#textual.dom.DOMNode.ancestors_with_self","title":"ancestors_with_self property","text":"
ancestors_with_self: list[DOMNode]\n

A list of Nodes by tracing a path all the way back to App.

Note

This is inclusive of self.

Returns Type Description list[DOMNode]

A list of nodes.

"},{"location":"api/dom_node/#textual.dom.DOMNode.auto_refresh","title":"auto_refresh writable property","text":"
auto_refresh: float | None\n

Number of seconds between automatic refresh, or None for no automatic refresh.

"},{"location":"api/dom_node/#textual.dom.DOMNode.background_colors","title":"background_colors property","text":"
background_colors: tuple[Color, Color]\n

The background color and the color of the parent's background.

Returns Type Description tuple[Color, Color]

(<background color>, <color>)

"},{"location":"api/dom_node/#textual.dom.DOMNode.children","title":"children property","text":"
children: Sequence['Widget']\n

A view on to the children.

Returns Type Description Sequence['Widget']

The node's children.

"},{"location":"api/dom_node/#textual.dom.DOMNode.classes","title":"classes instance-attribute class-attribute","text":"
classes = _ClassesDescriptor()\n

CSS class names for this node.

"},{"location":"api/dom_node/#textual.dom.DOMNode.colors","title":"colors property","text":"
colors: tuple[Color, Color, Color, Color]\n

The widget's background and foreground colors, and the parent's background and foreground colors.

Returns Type Description tuple[Color, Color, Color, Color]

(<parent background>, <parent color>, <background>, <color>)

"},{"location":"api/dom_node/#textual.dom.DOMNode.css_identifier","title":"css_identifier property","text":"
css_identifier: str\n

A CSS selector that identifies this DOM node.

"},{"location":"api/dom_node/#textual.dom.DOMNode.css_identifier_styled","title":"css_identifier_styled property","text":"
css_identifier_styled: Text\n

A syntax highlighted CSS identifier.

Returns Type Description Text

A Rich Text object.

"},{"location":"api/dom_node/#textual.dom.DOMNode.css_path_nodes","title":"css_path_nodes property","text":"
css_path_nodes: list[DOMNode]\n

A list of nodes from the App to this node, forming a \"path\".

Returns Type Description list[DOMNode]

A list of nodes, where the first item is the App, and the last is this node.

"},{"location":"api/dom_node/#textual.dom.DOMNode.css_tree","title":"css_tree property","text":"
css_tree: Tree\n

A Rich tree to display the DOM, annotated with the node's CSS.

Log this to visualize your app in the textual console.

Example
self.log(self.css_tree)\n
Returns Type Description Tree

A Tree renderable.

"},{"location":"api/dom_node/#textual.dom.DOMNode.display","title":"display writable property","text":"
display: bool\n

Should the DOM node be displayed?

May be set to a boolean to show or hide the node, or to any valid value for the display rule.

Example
my_widget.display = False  # Hide my_widget\n
"},{"location":"api/dom_node/#textual.dom.DOMNode.displayed_children","title":"displayed_children property","text":"
displayed_children: list[Widget]\n

The child nodes which will be displayed.

Returns Type Description list[Widget]

A list of nodes.

"},{"location":"api/dom_node/#textual.dom.DOMNode.id","title":"id writable property","text":"
id: str | None\n

The ID of this node, or None if the node has no ID.

"},{"location":"api/dom_node/#textual.dom.DOMNode.is_modal","title":"is_modal property","text":"
is_modal: bool\n

Is the node a modal?

"},{"location":"api/dom_node/#textual.dom.DOMNode.name","title":"name property","text":"
name: str | None\n

The name of the node.

"},{"location":"api/dom_node/#textual.dom.DOMNode.parent","title":"parent property","text":"
parent: DOMNode | None\n

The parent node.

All nodes have parent once added to the DOM, with the exception of the App which is the root node.

"},{"location":"api/dom_node/#textual.dom.DOMNode.pseudo_classes","title":"pseudo_classes property","text":"
pseudo_classes: frozenset[str]\n

A (frozen) set of all pseudo classes.

"},{"location":"api/dom_node/#textual.dom.DOMNode.rich_style","title":"rich_style property","text":"
rich_style: Style\n

Get a Rich Style object for this DOMNode.

Returns Type Description Style

A Rich style.

"},{"location":"api/dom_node/#textual.dom.DOMNode.screen","title":"screen property","text":"
screen: 'Screen[object]'\n

The screen containing this node.

Returns Type Description 'Screen[object]'

A screen object.

Raises Type Description NoScreen

If this node isn't mounted (and has no screen).

"},{"location":"api/dom_node/#textual.dom.DOMNode.text_style","title":"text_style property","text":"
text_style: Style\n

Get the text style object.

A widget's style is influenced by its parent. for instance if a parent is bold, then the child will also be bold.

Returns Type Description Style

A Rich Style.

"},{"location":"api/dom_node/#textual.dom.DOMNode.tree","title":"tree property","text":"
tree: Tree\n

A Rich tree to display the DOM.

Log this to visualize your app in the textual console.

Example
self.log(self.tree)\n
Returns Type Description Tree

A Tree renderable.

"},{"location":"api/dom_node/#textual.dom.DOMNode.visible","title":"visible writable property","text":"
visible: bool\n

Is this widget visible in the DOM?

If a widget hasn't had its visibility set explicitly, then it inherits it from its DOM ancestors.

This may be set explicitly to override inherited values. The valid values include the valid values for the visibility rule and the booleans True or False, to set the widget to be visible or invisible, respectively.

When a node is invisible, Textual will reserve space for it, but won't display anything.

"},{"location":"api/dom_node/#textual.dom.DOMNode.workers","title":"workers property","text":"
workers: WorkerManager\n

The app's worker manager. Shortcut for self.app.workers.

"},{"location":"api/dom_node/#textual.dom.DOMNode.add_class","title":"add_class method","text":"
def add_class(self, *class_names):\n

Add class names to this Node.

Parameters Name Type Description Default *class_names str

CSS class names to add.

() Returns Type Description Self

Self.

"},{"location":"api/dom_node/#textual.dom.DOMNode.get_component_styles","title":"get_component_styles method","text":"
def get_component_styles(self, name):\n

Get a \"component\" styles object (must be defined in COMPONENT_CLASSES classvar).

Parameters Name Type Description Default name str

Name of the component.

required Raises Type Description KeyError

If the component class doesn't exist.

Returns Type Description RenderStyles

A Styles object.

"},{"location":"api/dom_node/#textual.dom.DOMNode.get_pseudo_classes","title":"get_pseudo_classes method","text":"
def get_pseudo_classes(self):\n

Get any pseudo classes applicable to this Node, e.g. hover, focus.

Returns Type Description Iterable[str]

Iterable of strings, such as a generator.

"},{"location":"api/dom_node/#textual.dom.DOMNode.has_class","title":"has_class method","text":"
def has_class(self, *class_names):\n

Check if the Node has all the given class names.

Parameters Name Type Description Default *class_names str

CSS class names to check.

() Returns Type Description bool

True if the node has all the given class names, otherwise False.

"},{"location":"api/dom_node/#textual.dom.DOMNode.has_pseudo_class","title":"has_pseudo_class method","text":"
def has_pseudo_class(self, *class_names):\n

Check for pseudo classes (such as hover, focus etc)

Parameters Name Type Description Default *class_names str

The pseudo classes to check for.

() Returns Type Description bool

True if the DOM node has those pseudo classes, False if not.

"},{"location":"api/dom_node/#textual.dom.DOMNode.notify_style_update","title":"notify_style_update method","text":"
def notify_style_update(self):\n

Called after styles are updated.

Implement this in a subclass if you want to clear any cached data when the CSS is reloaded.

"},{"location":"api/dom_node/#textual.dom.DOMNode.query","title":"query method","text":"
def query(self, selector=None):\n

Get a DOM query matching a selector.

Parameters Name Type Description Default selector str | type[QueryType] | None

A CSS selector or None for all nodes.

None Returns Type Description DOMQuery[Widget] | DOMQuery[QueryType]

A query object.

"},{"location":"api/dom_node/#textual.dom.DOMNode.query_one","title":"query_one method","text":"
def query_one(self, selector, expect_type=None):\n

Get a single Widget matching the given selector or selector type.

Parameters Name Type Description Default selector str | type[QueryType]

A selector.

required expect_type type[QueryType] | None

Require the object be of the supplied type, or None for any type.

None Raises Type Description WrongType

If the wrong type was found.

NoMatches

If no node matches the query.

TooManyMatches

If there is more than one matching node in the query.

Returns Type Description QueryType | Widget

A widget matching the selector.

"},{"location":"api/dom_node/#textual.dom.DOMNode.remove_class","title":"remove_class method","text":"
def remove_class(self, *class_names):\n

Remove class names from this Node.

Parameters Name Type Description Default *class_names str

CSS class names to remove.

() Returns Type Description Self

Self.

"},{"location":"api/dom_node/#textual.dom.DOMNode.reset_styles","title":"reset_styles method","text":"
def reset_styles(self):\n

Reset styles back to their initial state.

"},{"location":"api/dom_node/#textual.dom.DOMNode.run_worker","title":"run_worker method","text":"
def run_worker(\nself,\nwork,\nname=\"\",\ngroup=\"default\",\ndescription=\"\",\nexit_on_error=True,\nstart=True,\nexclusive=False,\nthread=False,\n):\n

Run work in a worker.

A worker runs a function, coroutine, or awaitable, in the background as an async task or as a thread.

Parameters Name Type Description Default work WorkType[ResultType]

A function, async function, or an awaitable object to run in a worker.

required name str | None

A short string to identify the worker (in logs and debugging).

'' group str

A short string to identify a group of workers.

'default' description str

A longer string to store longer information on the worker.

'' exit_on_error bool

Exit the app if the worker raises an error. Set to False to suppress exceptions.

True start bool

Start the worker immediately.

True exclusive bool

Cancel all workers in the same group.

False thread bool

Mark the worker as a thread worker.

False Returns Type Description Worker[ResultType]

New Worker instance.

"},{"location":"api/dom_node/#textual.dom.DOMNode.set_class","title":"set_class method","text":"
def set_class(self, add, *class_names):\n

Add or remove class(es) based on a condition.

Parameters Name Type Description Default add bool

Add the classes if True, otherwise remove them.

required Returns Type Description Self

Self.

"},{"location":"api/dom_node/#textual.dom.DOMNode.set_classes","title":"set_classes method","text":"
def set_classes(self, classes):\n

Replace all classes.

Parameters Name Type Description Default classes str | Iterable[str]

A string containing space separated classes, or an iterable of class names.

required Returns Type Description Self

Self.

"},{"location":"api/dom_node/#textual.dom.DOMNode.set_styles","title":"set_styles method","text":"
def set_styles(self, css=None, **update_styles):\n

Set custom styles on this object.

Parameters Name Type Description Default css str | None

Styles in CSS format.

None **update_styles

Keyword arguments map style names on to style.

{} Returns Type Description Self

Self.

"},{"location":"api/dom_node/#textual.dom.DOMNode.toggle_class","title":"toggle_class method","text":"
def toggle_class(self, *class_names):\n

Toggle class names on this Node.

Parameters Name Type Description Default *class_names str

CSS class names to toggle.

() Returns Type Description Self

Self.

"},{"location":"api/dom_node/#textual.dom.DOMNode.walk_children","title":"walk_children method","text":"
def walk_children(\nself,\nfilter_type=None,\n*,\nwith_self=False,\nmethod=\"depth\",\nreverse=False\n):\n

Walk the subtree rooted at this node, and return every descendant encountered in a list.

Parameters Name Type Description Default filter_type type[WalkType] | None

Filter only this type, or None for no filter.

None with_self bool

Also yield self in addition to descendants.

False method WalkMethod

One of \"depth\" or \"breadth\".

'depth' reverse bool

Reverse the order (bottom up).

False Returns Type Description list[DOMNode] | list[WalkType]

A list of nodes.

"},{"location":"api/dom_node/#textual.dom.DOMNode.watch","title":"watch method","text":"
def watch(self, obj, attribute_name, callback, init=True):\n

Watches for modifications to reactive attributes on another object.

Example

Here's how you could detect when the app changes from dark to light mode (and vice versa).

def on_dark_change(old_value:bool, new_value:bool):\n# Called when app.dark changes.\nprint(\"App.dark when from {old_value} to {new_value}\")\nself.watch(self.app, \"dark\", self.on_dark_change, init=False)\n
Parameters Name Type Description Default obj DOMNode

Object containing attribute to watch.

required attribute_name str

Attribute to watch.

required callback WatchCallbackType

A callback to run when attribute changes.

required init bool

Check watchers on first call.

True"},{"location":"api/dom_node/#textual.dom.NoScreen","title":"NoScreen class","text":"

Bases: DOMError

Raised when the node has no associated screen.

"},{"location":"api/dom_node/#textual.dom.check_identifiers","title":"check_identifiers function","text":"
def check_identifiers(description, *names):\n

Validate identifier and raise an error if it fails.

Parameters Name Type Description Default description str

Description of where identifier is used for error message.

required *names str

Identifiers to check.

()"},{"location":"api/errors/","title":"Errors","text":"

General exception classes.

"},{"location":"api/errors/#textual.errors.DuplicateKeyHandlers","title":"DuplicateKeyHandlers class","text":"

Bases: TextualError

More than one handler for a single key press.

For example, if the handlers key_ctrl_i and key_tab were defined on the same widget, then this error would be raised.

"},{"location":"api/errors/#textual.errors.NoWidget","title":"NoWidget class","text":"

Bases: TextualError

Specified widget was not found.

"},{"location":"api/errors/#textual.errors.RenderError","title":"RenderError class","text":"

Bases: TextualError

An object could not be rendered.

"},{"location":"api/errors/#textual.errors.TextualError","title":"TextualError class","text":"

Bases: Exception

Base class for Textual errors.

"},{"location":"api/events/","title":"Events","text":"

Builtin events sent by Textual.

Events may be marked as \"Bubbles\" and \"Verbose\". See the events guide for an explanation of bubbling. Verbose events are excluded from the textual console, unless you explicitly request them with the -v switch as follows:

textual console -v\n
"},{"location":"api/events/#textual.events.Blur","title":"Blur class","text":"

Bases: Event

Sent when a widget is blurred (un-focussed).

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.Click","title":"Click class","text":"

Bases: MouseEvent

Sent when a widget is clicked.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.Compose","title":"Compose class","text":"

Bases: Event

Sent to a widget to request it to compose and mount children.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.DescendantBlur","title":"DescendantBlur class","text":"

Bases: Event

Sent when a child widget is blurred.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.DescendantBlur.control","title":"control property","text":"
control: Widget\n

The widget that was blurred (alias of widget).

"},{"location":"api/events/#textual.events.DescendantBlur.widget","title":"widget instance-attribute","text":"
widget: Widget\n

The widget that was blurred.

"},{"location":"api/events/#textual.events.DescendantFocus","title":"DescendantFocus class","text":"

Bases: Event

Sent when a child widget is focussed.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.DescendantFocus.control","title":"control property","text":"
control: Widget\n

The widget that was focused (alias of widget).

"},{"location":"api/events/#textual.events.DescendantFocus.widget","title":"widget instance-attribute","text":"
widget: Widget\n

The widget that was focused.

"},{"location":"api/events/#textual.events.Enter","title":"Enter class","text":"

Bases: Event

Sent when the mouse is moved over a widget.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.Event","title":"Event class","text":"

Bases: Message

The base class for all events.

"},{"location":"api/events/#textual.events.Focus","title":"Focus class","text":"

Bases: Event

Sent when a widget is focussed.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.Hide","title":"Hide class","text":"

Bases: Event

Sent when a widget has been hidden.

  • Bubbles
  • Verbose

A widget may be hidden by setting its visible flag to False, if it is no longer in a layout, or if it has been offset beyond the edges of the terminal.

"},{"location":"api/events/#textual.events.Idle","title":"Idle class","text":"

Bases: Event

Sent when there are no more items in the message queue.

This is a pseudo-event in that it is created by the Textual system and doesn't go through the usual message queue.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.InputEvent","title":"InputEvent class","text":"

Bases: Event

Base class for input events.

"},{"location":"api/events/#textual.events.Key","title":"Key class","text":"
def __init__(self, key, character):\n

Bases: InputEvent

Sent when the user hits a key on the keyboard.

  • Bubbles
  • Verbose
Parameters Name Type Description Default key str

The key that was pressed.

required character str | None

A printable character or None if it is not printable.

required Attributes Name Type Description aliases list[str]

The aliases for the key, including the key itself.

"},{"location":"api/events/#textual.events.Key.is_printable","title":"is_printable property","text":"
is_printable: bool\n

Check if the key is printable (produces a unicode character).

Returns Type Description bool

True if the key is printable.

"},{"location":"api/events/#textual.events.Key.name","title":"name property","text":"
name: str\n

Name of a key suitable for use as a Python identifier.

"},{"location":"api/events/#textual.events.Key.name_aliases","title":"name_aliases property","text":"
name_aliases: list[str]\n

The corresponding name for every alias in aliases list.

"},{"location":"api/events/#textual.events.Leave","title":"Leave class","text":"

Bases: Event

Sent when the mouse is moved away from a widget.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.Load","title":"Load class","text":"

Bases: Event

Sent when the App is running but before the terminal is in application mode.

Use this event to run any set up that doesn't require any visuals such as loading configuration and binding keys.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.Mount","title":"Mount class","text":"

Bases: Event

Sent when a widget is mounted and may receive messages.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.MouseCapture","title":"MouseCapture class","text":"
def __init__(self, mouse_position):\n

Bases: Event

Sent when the mouse has been captured.

  • Bubbles
  • Verbose

When a mouse has been captured, all further mouse events will be sent to the capturing widget.

Parameters Name Type Description Default mouse_position Offset

The position of the mouse when captured.

required"},{"location":"api/events/#textual.events.MouseDown","title":"MouseDown class","text":"

Bases: MouseEvent

Sent when a mouse button is pressed.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.MouseEvent","title":"MouseEvent class","text":"
def __init__(\nself,\nx,\ny,\ndelta_x,\ndelta_y,\nbutton,\nshift,\nmeta,\nctrl,\nscreen_x=None,\nscreen_y=None,\nstyle=None,\n):\n

Bases: InputEvent

Sent in response to a mouse event.

  • Bubbles
  • Verbose
Parameters Name Type Description Default x int

The relative x coordinate.

required y int

The relative y coordinate.

required delta_x int

Change in x since the last message.

required delta_y int

Change in y since the last message.

required button int

Indexed of the pressed button.

required shift bool

True if the shift key is pressed.

required meta bool

True if the meta key is pressed.

required ctrl bool

True if the ctrl key is pressed.

required screen_x int | None

The absolute x coordinate.

None screen_y int | None

The absolute y coordinate.

None style Style | None

The Rich Style under the mouse cursor.

None"},{"location":"api/events/#textual.events.MouseEvent.delta","title":"delta property","text":"
delta: Offset\n

Mouse coordinate delta (change since last event).

Returns Type Description Offset

Mouse coordinate.

"},{"location":"api/events/#textual.events.MouseEvent.offset","title":"offset property","text":"
offset: Offset\n

The mouse coordinate as an offset.

Returns Type Description Offset

Mouse coordinate.

"},{"location":"api/events/#textual.events.MouseEvent.screen_offset","title":"screen_offset property","text":"
screen_offset: Offset\n

Mouse coordinate relative to the screen.

Returns Type Description Offset

Mouse coordinate.

"},{"location":"api/events/#textual.events.MouseEvent.style","title":"style writable property","text":"
style: Style\n

The (Rich) Style under the cursor.

"},{"location":"api/events/#textual.events.MouseEvent.get_content_offset","title":"get_content_offset method","text":"
def get_content_offset(self, widget):\n

Get offset within a widget's content area, or None if offset is not in content (i.e. padding or border).

Parameters Name Type Description Default widget Widget

Widget receiving the event.

required Returns Type Description Offset | None

An offset where the origin is at the top left of the content area.

"},{"location":"api/events/#textual.events.MouseEvent.get_content_offset_capture","title":"get_content_offset_capture method","text":"
def get_content_offset_capture(self, widget):\n

Get offset from a widget's content area.

This method works even if the offset is outside the widget content region.

Parameters Name Type Description Default widget Widget

Widget receiving the event.

required Returns Type Description Offset

An offset where the origin is at the top left of the content area.

"},{"location":"api/events/#textual.events.MouseMove","title":"MouseMove class","text":"

Bases: MouseEvent

Sent when the mouse cursor moves.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.MouseRelease","title":"MouseRelease class","text":"
def __init__(self, mouse_position):\n

Bases: Event

Mouse has been released.

  • Bubbles
  • Verbose
Parameters Name Type Description Default mouse_position Offset

The position of the mouse when released.

required"},{"location":"api/events/#textual.events.MouseScrollDown","title":"MouseScrollDown class","text":"

Bases: MouseEvent

Sent when the mouse wheel is scrolled down.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.MouseScrollUp","title":"MouseScrollUp class","text":"

Bases: MouseEvent

Sent when the mouse wheel is scrolled up.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.MouseUp","title":"MouseUp class","text":"

Bases: MouseEvent

Sent when a mouse button is released.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.Paste","title":"Paste class","text":"
def __init__(self, text):\n

Bases: Event

Event containing text that was pasted into the Textual application. This event will only appear when running in a terminal emulator that supports bracketed paste mode. Textual will enable bracketed pastes when an app starts, and disable it when the app shuts down.

  • Bubbles
  • Verbose
Parameters Name Type Description Default text str

The text that has been pasted.

required"},{"location":"api/events/#textual.events.Print","title":"Print class","text":"
def __init__(self, text, stderr=False):\n

Bases: Event

Sent to a widget that is capturing prints.

  • Bubbles
  • Verbose
Parameters Name Type Description Default text str

Text that was printed.

required stderr bool

True if the print was to stderr, or False for stdout.

False"},{"location":"api/events/#textual.events.Ready","title":"Ready class","text":"

Bases: Event

Sent to the app when the DOM is ready.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.Resize","title":"Resize class","text":"
def __init__(self, size, virtual_size, container_size=None):\n

Bases: Event

Sent when the app or widget has been resized.

  • Bubbles
  • Verbose
Parameters Name Type Description Default size Size

The new size of the Widget.

required virtual_size Size

The virtual size (scrollable size) of the Widget.

required container_size Size | None

The size of the Widget's container widget.

None"},{"location":"api/events/#textual.events.ScreenResume","title":"ScreenResume class","text":"

Bases: Event

Sent to screen that has been made active.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.ScreenSuspend","title":"ScreenSuspend class","text":"

Bases: Event

Sent to screen when it is no longer active.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.Show","title":"Show class","text":"

Bases: Event

Sent when a widget has become visible.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.Timer","title":"Timer class","text":"
def __init__(self, timer, time, count=0, callback=None):\n

Bases: Event

Sent in response to a timer.

  • Bubbles
  • Verbose
"},{"location":"api/events/#textual.events.Unmount","title":"Unmount class","text":"

Bases: Event

Sent when a widget is unmounted and may not longer receive messages.

  • Bubbles
  • Verbose
"},{"location":"api/filter/","title":"Filter","text":"

Filter classes.

Note

Filters are used internally, and not recommended for use by Textual app developers.

Filters are used internally to process terminal output after it has been rendered. Currently this is used internally to convert the application to monochrome, when the NO_COLOR env var is set.

In the future, this system will be used to implement accessibility features.

"},{"location":"api/filter/#textual.filter.NO_DIM","title":"NO_DIM module-attribute","text":"
NO_DIM = Style(dim=False)\n

A Style to set dim to False.

"},{"location":"api/filter/#textual.filter.ANSIToTruecolor","title":"ANSIToTruecolor class","text":"
def __init__(self, terminal_theme):\n

Bases: LineFilter

Convert ANSI colors to their truecolor equivalents.

Parameters Name Type Description Default terminal_theme TerminalTheme

A rich terminal theme.

required"},{"location":"api/filter/#textual.filter.ANSIToTruecolor.apply","title":"apply method","text":"
def apply(self, segments, background):\n

Transform a list of segments.

Parameters Name Type Description Default segments list[Segment]

A list of segments.

required background Color

The background color.

required Returns Type Description list[Segment]

A new list of segments.

"},{"location":"api/filter/#textual.filter.ANSIToTruecolor.truecolor_style","title":"truecolor_style cached","text":"
def truecolor_style(self, style):\n

Replace system colors with truecolor equivalent.

Parameters Name Type Description Default style Style

Style to apply truecolor filter to.

required Returns Type Description Style

New style.

"},{"location":"api/filter/#textual.filter.DimFilter","title":"DimFilter class","text":"
def __init__(self, dim_factor=0.5):\n

Bases: LineFilter

Replace dim attributes with modified colors.

Parameters Name Type Description Default dim_factor float

The factor to dim by; 0 is 100% background (i.e. invisible), 1.0 is no change.

0.5"},{"location":"api/filter/#textual.filter.DimFilter.apply","title":"apply method","text":"
def apply(self, segments, background):\n

Transform a list of segments.

Parameters Name Type Description Default segments list[Segment]

A list of segments.

required background Color

The background color.

required Returns Type Description list[Segment]

A new list of segments.

"},{"location":"api/filter/#textual.filter.LineFilter","title":"LineFilter class","text":"

Bases: ABC

Base class for a line filter.

"},{"location":"api/filter/#textual.filter.LineFilter.apply","title":"apply abstractmethod","text":"
def apply(self, segments, background):\n

Transform a list of segments.

Parameters Name Type Description Default segments list[Segment]

A list of segments.

required background Color

The background color.

required Returns Type Description list[Segment]

A new list of segments.

"},{"location":"api/filter/#textual.filter.Monochrome","title":"Monochrome class","text":"

Bases: LineFilter

Convert all colors to monochrome.

"},{"location":"api/filter/#textual.filter.Monochrome.apply","title":"apply method","text":"
def apply(self, segments, background):\n

Transform a list of segments.

Parameters Name Type Description Default segments list[Segment]

A list of segments.

required background Color

The background color.

required Returns Type Description list[Segment]

A new list of segments.

"},{"location":"api/filter/#textual.filter.dim_color","title":"dim_color cached","text":"
def dim_color(background, color, factor):\n

Dim a color by blending towards the background

Parameters Name Type Description Default background RichColor

background color.

required color RichColor

Foreground color.

required factor float

Blend factor

required Returns Type Description RichColor

New dimmer color.

"},{"location":"api/filter/#textual.filter.dim_style","title":"dim_style cached","text":"
def dim_style(style, background, factor):\n

Replace dim attribute with a dim color.

Parameters Name Type Description Default style Style

Style to dim.

required factor float

Blend factor.

required Returns Type Description Style

New dimmed style.

"},{"location":"api/filter/#textual.filter.monochrome_style","title":"monochrome_style cached","text":"
def monochrome_style(style):\n

Convert colors in a style to monochrome.

Parameters Name Type Description Default style Style

A Rich Style.

required Returns Type Description Style

A new Rich style.

"},{"location":"api/fuzzy_matcher/","title":"Fuzzy matcher","text":"

Fuzzy matcher.

This class is used by the command palette to match search terms.

"},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher","title":"Matcher class","text":"
def __init__(\nself, query, *, match_style=None, case_sensitive=False\n):\n

A fuzzy matcher.

Parameters Name Type Description Default query str

A query as typed in by the user.

required match_style Style | None

The style to use to highlight matched portions of a string.

None case_sensitive bool

Should matching be case sensitive?

False"},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher.case_sensitive","title":"case_sensitive property","text":"
case_sensitive: bool\n

Is this matcher case sensitive?

"},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher.match_style","title":"match_style property","text":"
match_style: Style\n

The style that will be used to highlight hits in the matched text.

"},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher.query","title":"query property","text":"
query: str\n

The query string to look for.

"},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher.query_pattern","title":"query_pattern property","text":"
query_pattern: str\n

The regular expression pattern built from the query.

"},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher.highlight","title":"highlight method","text":"
def highlight(self, candidate):\n

Highlight the candidate with the fuzzy match.

Parameters Name Type Description Default candidate str

The candidate string to match against the query.

required Returns Type Description Text

A [rich.text.Text][Text] object with highlighted matches.

"},{"location":"api/fuzzy_matcher/#textual.fuzzy.Matcher.match","title":"match method","text":"
def match(self, candidate):\n

Match the candidate against the query.

Parameters Name Type Description Default candidate str

Candidate string to match against the query.

required Returns Type Description float

Strength of the match from 0 to 1.

"},{"location":"api/geometry/","title":"Geometry","text":"

Functions and classes to manage terminal geometry (anything involving coordinates or dimensions).

"},{"location":"api/geometry/#textual.geometry.NULL_OFFSET","title":"NULL_OFFSET module-attribute","text":"
NULL_OFFSET: Final = Offset(0, 0)\n

An offset constant for (0, 0).

"},{"location":"api/geometry/#textual.geometry.NULL_REGION","title":"NULL_REGION module-attribute","text":"
NULL_REGION: Final = Region(0, 0, 0, 0)\n

A Region constant for a null region (at the origin, with both width and height set to zero).

"},{"location":"api/geometry/#textual.geometry.NULL_SPACING","title":"NULL_SPACING module-attribute","text":"
NULL_SPACING: Final = Spacing(0, 0, 0, 0)\n

A Spacing constant for no space.

"},{"location":"api/geometry/#textual.geometry.SpacingDimensions","title":"SpacingDimensions module-attribute","text":"
SpacingDimensions: TypeAlias = Union[\nint,\nTuple[int],\nTuple[int, int],\nTuple[int, int, int, int],\n]\n

The valid ways in which you can specify spacing.

"},{"location":"api/geometry/#textual.geometry.Offset","title":"Offset class","text":"

Bases: NamedTuple

A cell offset defined by x and y coordinates.

Offsets are typically relative to the top left of the terminal or other container.

Textual prefers the names x and y, but you could consider x to be the column and y to be the row.

Offsets support addition, subtraction, multiplication, and negation.

Example
>>> from textual.geometry import Offset\n>>> offset = Offset(3, 2)\n>>> offset\nOffset(x=3, y=2)\n>>> offset += Offset(10, 0)\n>>> offset\nOffset(x=13, y=2)\n>>> -offset\nOffset(x=-13, y=-2)\n
"},{"location":"api/geometry/#textual.geometry.Offset.clamped","title":"clamped property","text":"
clamped: Offset\n

This offset with x and y restricted to values above zero.

"},{"location":"api/geometry/#textual.geometry.Offset.is_origin","title":"is_origin property","text":"
is_origin: bool\n

Is the offset at (0, 0)?

"},{"location":"api/geometry/#textual.geometry.Offset.x","title":"x instance-attribute class-attribute","text":"
x: int = 0\n

Offset in the x-axis (horizontal)

"},{"location":"api/geometry/#textual.geometry.Offset.y","title":"y instance-attribute class-attribute","text":"
y: int = 0\n

Offset in the y-axis (vertical)

"},{"location":"api/geometry/#textual.geometry.Offset.blend","title":"blend method","text":"
def blend(self, destination, factor):\n

Calculate a new offset on a line between this offset and a destination offset.

Parameters Name Type Description Default destination Offset

Point where factor would be 1.0.

required factor float

A value between 0 and 1.0.

required Returns Type Description Offset

A new point on a line between self and destination.

"},{"location":"api/geometry/#textual.geometry.Offset.get_distance_to","title":"get_distance_to method","text":"
def get_distance_to(self, other):\n

Get the distance to another offset.

Parameters Name Type Description Default other Offset

An offset.

required Returns Type Description float

Distance to other offset.

"},{"location":"api/geometry/#textual.geometry.Region","title":"Region class","text":"

Bases: NamedTuple

Defines a rectangular region.

A Region consists of a coordinate (x and y) and dimensions (width and height).

  (x, y)\n    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u25b2\n    \u2502                    \u2502 \u2502\n    \u2502                    \u2502 \u2502\n    \u2502                    \u2502 height\n    \u2502                    \u2502 \u2502\n    \u2502                    \u2502 \u2502\n    \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u25bc\n    \u25c0\u2500\u2500\u2500\u2500\u2500\u2500\u2500 width \u2500\u2500\u2500\u2500\u2500\u2500\u25b6\n
Example
>>> from textual.geometry import Region\n>>> region = Region(4, 5, 20, 10)\n>>> region\nRegion(x=4, y=5, width=20, height=10)\n>>> region.area\n200\n>>> region.size\nSize(width=20, height=10)\n>>> region.offset\nOffset(x=4, y=5)\n>>> region.contains(1, 2)\nFalse\n>>> region.contains(10, 8)\nTrue\n
"},{"location":"api/geometry/#textual.geometry.Region.area","title":"area property","text":"
area: int\n

The area under the region.

"},{"location":"api/geometry/#textual.geometry.Region.bottom","title":"bottom property","text":"
bottom: int\n

Maximum Y value (non inclusive).

"},{"location":"api/geometry/#textual.geometry.Region.bottom_left","title":"bottom_left property","text":"
bottom_left: Offset\n

Bottom left offset of the region.

Returns Type Description Offset

An offset.

"},{"location":"api/geometry/#textual.geometry.Region.bottom_right","title":"bottom_right property","text":"
bottom_right: Offset\n

Bottom right offset of the region.

Returns Type Description Offset

An offset.

"},{"location":"api/geometry/#textual.geometry.Region.center","title":"center property","text":"
center: tuple[float, float]\n

The center of the region.

Note, that this does not return an Offset, because the center may not be an integer coordinate.

Returns Type Description tuple[float, float]

Tuple of floats.

"},{"location":"api/geometry/#textual.geometry.Region.column_range","title":"column_range property","text":"
column_range: range\n

A range object for X coordinates.

"},{"location":"api/geometry/#textual.geometry.Region.column_span","title":"column_span property","text":"
column_span: tuple[int, int]\n

A pair of integers for the start and end columns (x coordinates) in this region.

The end value is exclusive.

"},{"location":"api/geometry/#textual.geometry.Region.corners","title":"corners property","text":"
corners: tuple[int, int, int, int]\n

The top left and bottom right coordinates as a tuple of four integers.

"},{"location":"api/geometry/#textual.geometry.Region.height","title":"height instance-attribute class-attribute","text":"
height: int = 0\n

The height of the region.

"},{"location":"api/geometry/#textual.geometry.Region.line_range","title":"line_range property","text":"
line_range: range\n

A range object for Y coordinates.

"},{"location":"api/geometry/#textual.geometry.Region.line_span","title":"line_span property","text":"
line_span: tuple[int, int]\n

A pair of integers for the start and end lines (y coordinates) in this region.

The end value is exclusive.

"},{"location":"api/geometry/#textual.geometry.Region.offset","title":"offset property","text":"
offset: Offset\n

The top left corner of the region.

Returns Type Description Offset

An offset.

"},{"location":"api/geometry/#textual.geometry.Region.reset_offset","title":"reset_offset property","text":"
reset_offset: Region\n

An region of the same size at (0, 0).

Returns Type Description Region

A region at the origin.

"},{"location":"api/geometry/#textual.geometry.Region.right","title":"right property","text":"
right: int\n

Maximum X value (non inclusive).

"},{"location":"api/geometry/#textual.geometry.Region.size","title":"size property","text":"
size: Size\n

Get the size of the region.

"},{"location":"api/geometry/#textual.geometry.Region.top_right","title":"top_right property","text":"
top_right: Offset\n

Top right offset of the region.

Returns Type Description Offset

An offset.

"},{"location":"api/geometry/#textual.geometry.Region.width","title":"width instance-attribute class-attribute","text":"
width: int = 0\n

The width of the region.

"},{"location":"api/geometry/#textual.geometry.Region.x","title":"x instance-attribute class-attribute","text":"
x: int = 0\n

Offset in the x-axis (horizontal).

"},{"location":"api/geometry/#textual.geometry.Region.y","title":"y instance-attribute class-attribute","text":"
y: int = 0\n

Offset in the y-axis (vertical).

"},{"location":"api/geometry/#textual.geometry.Region.at_offset","title":"at_offset method","text":"
def at_offset(self, offset):\n

Get a new Region with the same size at a given offset.

Parameters Name Type Description Default offset tuple[int, int]

An offset.

required Returns Type Description Region

New Region with adjusted offset.

"},{"location":"api/geometry/#textual.geometry.Region.clip","title":"clip method","text":"
def clip(self, width, height):\n

Clip this region to fit within width, height.

Parameters Name Type Description Default width int

Width of bounds.

required height int

Height of bounds.

required Returns Type Description Region

Clipped region.

"},{"location":"api/geometry/#textual.geometry.Region.clip_size","title":"clip_size method","text":"
def clip_size(self, size):\n

Clip the size to fit within minimum values.

Parameters Name Type Description Default size tuple[int, int]

Maximum width and height.

required Returns Type Description Region

No region, not bigger than size.

"},{"location":"api/geometry/#textual.geometry.Region.contains","title":"contains method","text":"
def contains(self, x, y):\n

Check if a point is in the region.

Parameters Name Type Description Default x int

X coordinate.

required y int

Y coordinate.

required Returns Type Description bool

True if the point is within the region.

"},{"location":"api/geometry/#textual.geometry.Region.contains_point","title":"contains_point method","text":"
def contains_point(self, point):\n

Check if a point is in the region.

Parameters Name Type Description Default point tuple[int, int]

A tuple of x and y coordinates.

required Returns Type Description bool

True if the point is within the region.

"},{"location":"api/geometry/#textual.geometry.Region.contains_region","title":"contains_region cached","text":"
def contains_region(self, other):\n

Check if a region is entirely contained within this region.

Parameters Name Type Description Default other Region

A region.

required Returns Type Description bool

True if the other region fits perfectly within this region.

"},{"location":"api/geometry/#textual.geometry.Region.crop_size","title":"crop_size method","text":"
def crop_size(self, size):\n

Get a region with the same offset, with a size no larger than size.

Parameters Name Type Description Default size tuple[int, int]

Maximum width and height (WIDTH, HEIGHT).

required Returns Type Description Region

New region that could fit within size.

"},{"location":"api/geometry/#textual.geometry.Region.expand","title":"expand method","text":"
def expand(self, size):\n

Increase the size of the region by adding a border.

Parameters Name Type Description Default size tuple[int, int]

Additional width and height.

required Returns Type Description Region

A new region.

"},{"location":"api/geometry/#textual.geometry.Region.from_corners","title":"from_corners classmethod","text":"
def from_corners(cls, x1, y1, x2, y2):\n

Construct a Region form the top left and bottom right corners.

Parameters Name Type Description Default x1 int

Top left x.

required y1 int

Top left y.

required x2 int

Bottom right x.

required y2 int

Bottom right y.

required Returns Type Description Region

A new region.

"},{"location":"api/geometry/#textual.geometry.Region.from_offset","title":"from_offset classmethod","text":"
def from_offset(cls, offset, size):\n

Create a region from offset and size.

Parameters Name Type Description Default offset tuple[int, int]

Offset (top left point).

required size tuple[int, int]

Dimensions of region.

required Returns Type Description Region

A region instance.

"},{"location":"api/geometry/#textual.geometry.Region.from_union","title":"from_union classmethod","text":"
def from_union(cls, regions):\n

Create a Region from the union of other regions.

Parameters Name Type Description Default regions Collection[Region]

One or more regions.

required Returns Type Description Region

A Region that encloses all other regions.

"},{"location":"api/geometry/#textual.geometry.Region.get_scroll_to_visible","title":"get_scroll_to_visible classmethod","text":"
def get_scroll_to_visible(\ncls, window_region, region, *, top=False\n):\n

Calculate the smallest offset required to translate a window so that it contains another region.

This method is used to calculate the required offset to scroll something in to view.

Parameters Name Type Description Default window_region Region

The window region.

required region Region

The region to move inside the window.

required top bool

Get offset to top of window.

False Returns Type Description Offset

An offset required to add to region to move it inside window_region.

"},{"location":"api/geometry/#textual.geometry.Region.grow","title":"grow cached","text":"
def grow(self, margin):\n

Grow a region by adding spacing.

Parameters Name Type Description Default margin tuple[int, int, int, int]

Grow space by (<top>, <right>, <bottom>, <left>).

required Returns Type Description Region

New region.

"},{"location":"api/geometry/#textual.geometry.Region.inflect","title":"inflect method","text":"
def inflect(self, x_axis=+1, y_axis=+1, margin=None):\n

Inflect a region around one or both axis.

The x_axis and y_axis parameters define which direction to move the region. A positive value will move the region right or down, a negative value will move the region left or up. A value of 0 will leave that axis unmodified.

\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557    \u2502\n\u2551          \u2551\n\u2551   Self   \u2551    \u2502\n\u2551          \u2551\n\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d    \u2502\n\n\u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u253c \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500\n\n                \u2502    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                     \u2502          \u2502\n                \u2502    \u2502  Result  \u2502\n                     \u2502          \u2502\n                \u2502    \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
Parameters Name Type Description Default x_axis int

+1 to inflect in the positive direction, -1 to inflect in the negative direction.

+1 y_axis int

+1 to inflect in the positive direction, -1 to inflect in the negative direction.

+1 margin Spacing | None

Additional margin.

None Returns Type Description Region

A new region.

"},{"location":"api/geometry/#textual.geometry.Region.intersection","title":"intersection cached","text":"
def intersection(self, region):\n

Get the overlapping portion of the two regions.

Parameters Name Type Description Default region Region

A region that overlaps this region.

required Returns Type Description Region

A new region that covers when the two regions overlap.

"},{"location":"api/geometry/#textual.geometry.Region.overlaps","title":"overlaps cached","text":"
def overlaps(self, other):\n

Check if another region overlaps this region.

Parameters Name Type Description Default other Region

A Region.

required Returns Type Description bool

True if other region shares any cells with this region.

"},{"location":"api/geometry/#textual.geometry.Region.shrink","title":"shrink cached","text":"
def shrink(self, margin):\n

Shrink a region by subtracting spacing.

Parameters Name Type Description Default margin tuple[int, int, int, int]

Shrink space by (<top>, <right>, <bottom>, <left>).

required Returns Type Description Region

The new, smaller region.

"},{"location":"api/geometry/#textual.geometry.Region.split","title":"split cached","text":"
def split(self, cut_x, cut_y):\n

Split a region in to 4 from given x and y offsets (cuts).

           cut_x \u2193\n        \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2510\n        \u2502        \u2502 \u2502   \u2502\n        \u2502    0   \u2502 \u2502 1 \u2502\n        \u2502        \u2502 \u2502   \u2502\ncut_y \u2192 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2518\n        \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2510\n        \u2502    2   \u2502 \u2502 3 \u2502\n        \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2518\n
Parameters Name Type Description Default cut_x int

Offset from self.x where the cut should be made. If negative, the cut is taken from the right edge.

required cut_y int

Offset from self.y where the cut should be made. If negative, the cut is taken from the lower edge.

required Returns Type Description tuple[Region, Region, Region, Region]

Four new regions which add up to the original (self).

"},{"location":"api/geometry/#textual.geometry.Region.split_horizontal","title":"split_horizontal cached","text":"
def split_horizontal(self, cut):\n

Split a region in to two, from a given y offset.

            \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n            \u2502    0    \u2502\n            \u2502         \u2502\n    cut \u2192   \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n            \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n            \u2502    1    \u2502\n            \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
Parameters Name Type Description Default cut int

An offset from self.y where the cut should be made. May be negative, for the offset to start from the lower edge.

required Returns Type Description tuple[Region, Region]

Two regions, which add up to the original (self).

"},{"location":"api/geometry/#textual.geometry.Region.split_vertical","title":"split_vertical cached","text":"
def split_vertical(self, cut):\n

Split a region in to two, from a given x offset.

         cut \u2193\n    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2510\n    \u2502    0   \u2502\u2502 1 \u2502\n    \u2502        \u2502\u2502   \u2502\n    \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2518\n
Parameters Name Type Description Default cut int

An offset from self.x where the cut should be made. If cut is negative, it is taken from the right edge.

required Returns Type Description tuple[Region, Region]

Two regions, which add up to the original (self).

"},{"location":"api/geometry/#textual.geometry.Region.translate","title":"translate cached","text":"
def translate(self, offset):\n

Move the offset of the Region.

Parameters Name Type Description Default offset tuple[int, int]

Offset to add to region.

required Returns Type Description Region

A new region shifted by (x, y)

"},{"location":"api/geometry/#textual.geometry.Region.translate_inside","title":"translate_inside method","text":"
def translate_inside(self, container, x_axis=True, y_axis=True):\n

Translate this region, so it fits within a container.

This will ensure that there is as little overlap as possible. The top left of the returned region is guaranteed to be within the container.

\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510         \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502    container     \u2502         \u2502    container     \u2502\n\u2502                  \u2502         \u2502    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502                  \u2502   \u2500\u2500\u25b6   \u2502    \u2502    return   \u2502\n\u2502       \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2510      \u2502    \u2502             \u2502\n\u2502       \u2502    self     \u2502      \u2502    \u2502             \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524             \u2502      \u2514\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n        \u2502             \u2502\n        \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
Parameters Name Type Description Default container Region

A container region.

required x_axis bool

Allow translation of X axis.

True y_axis bool

Allow translation of Y axis.

True Returns Type Description Region

A new region with same dimensions that fits with inside container.

"},{"location":"api/geometry/#textual.geometry.Region.union","title":"union cached","text":"
def union(self, region):\n

Get the smallest region that contains both regions.

Parameters Name Type Description Default region Region

Another region.

required Returns Type Description Region

An optimally sized region to cover both regions.

"},{"location":"api/geometry/#textual.geometry.Size","title":"Size class","text":"

Bases: NamedTuple

The dimensions (width and height) of a rectangular region.

Example
>>> from textual.geometry import Size\n>>> size = Size(2, 3)\n>>> size\nSize(width=2, height=3)\n>>> size.area\n6\n>>> size + Size(10, 20)\nSize(width=12, height=23)\n
"},{"location":"api/geometry/#textual.geometry.Size.area","title":"area property","text":"
area: int\n

The area occupied by a region of this size.

"},{"location":"api/geometry/#textual.geometry.Size.height","title":"height instance-attribute class-attribute","text":"
height: int = 0\n

The height in cells.

"},{"location":"api/geometry/#textual.geometry.Size.line_range","title":"line_range property","text":"
line_range: range\n

A range object that covers values between 0 and height.

"},{"location":"api/geometry/#textual.geometry.Size.region","title":"region property","text":"
region: Region\n

A region of the same size, at the origin.

"},{"location":"api/geometry/#textual.geometry.Size.width","title":"width instance-attribute class-attribute","text":"
width: int = 0\n

The width in cells.

"},{"location":"api/geometry/#textual.geometry.Size.contains","title":"contains method","text":"
def contains(self, x, y):\n

Check if a point is in area defined by the size.

Parameters Name Type Description Default x int

X coordinate.

required y int

Y coordinate.

required Returns Type Description bool

True if the point is within the region.

"},{"location":"api/geometry/#textual.geometry.Size.contains_point","title":"contains_point method","text":"
def contains_point(self, point):\n

Check if a point is in the area defined by the size.

Parameters Name Type Description Default point tuple[int, int]

A tuple of x and y coordinates.

required Returns Type Description bool

True if the point is within the region.

"},{"location":"api/geometry/#textual.geometry.Spacing","title":"Spacing class","text":"

Bases: NamedTuple

The spacing around a renderable, such as padding and border.

Spacing is defined by four integers for the space at the top, right, bottom, and left of a region.

\u250c \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500\u25b2\u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2510\n               \u2502 top\n\u2502        \u250f\u2501\u2501\u2501\u2501\u2501\u25bc\u2501\u2501\u2501\u2501\u2501\u2501\u2513         \u2502\n \u25c0\u2500\u2500\u2500\u2500\u2500\u2500\u25b6\u2503            \u2503\u25c0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25b6\n\u2502  left  \u2503            \u2503 right   \u2502\n         \u2503            \u2503\n\u2502        \u2517\u2501\u2501\u2501\u2501\u2501\u25b2\u2501\u2501\u2501\u2501\u2501\u2501\u251b         \u2502\n               \u2502 bottom\n\u2514 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500\u25bc\u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2500 \u2518\n
Example
>>> from textual.geometry import Region, Spacing\n>>> region = Region(2, 3, 20, 10)\n>>> spacing = Spacing(1, 2, 3, 4)\n>>> region.grow(spacing)\nRegion(x=-2, y=2, width=26, height=14)\n>>> region.shrink(spacing)\nRegion(x=6, y=4, width=14, height=6)\n>>> spacing.css\n'1 2 3 4'\n
"},{"location":"api/geometry/#textual.geometry.Spacing.bottom","title":"bottom instance-attribute class-attribute","text":"
bottom: int = 0\n

Space from the bottom of a region.

"},{"location":"api/geometry/#textual.geometry.Spacing.bottom_right","title":"bottom_right property","text":"
bottom_right: tuple[int, int]\n

A pair of integers for the right, and bottom space.

"},{"location":"api/geometry/#textual.geometry.Spacing.css","title":"css property","text":"
css: str\n

A string containing the spacing in CSS format.

For example: \"1\" or \"2 4\" or \"4 2 8 2\".

"},{"location":"api/geometry/#textual.geometry.Spacing.height","title":"height property","text":"
height: int\n

Total space in the y axis.

"},{"location":"api/geometry/#textual.geometry.Spacing.left","title":"left instance-attribute class-attribute","text":"
left: int = 0\n

Space from the left of a region.

"},{"location":"api/geometry/#textual.geometry.Spacing.right","title":"right instance-attribute class-attribute","text":"
right: int = 0\n

Space from the right of a region.

"},{"location":"api/geometry/#textual.geometry.Spacing.top","title":"top instance-attribute class-attribute","text":"
top: int = 0\n

Space from the top of a region.

"},{"location":"api/geometry/#textual.geometry.Spacing.top_left","title":"top_left property","text":"
top_left: tuple[int, int]\n

A pair of integers for the left, and top space.

"},{"location":"api/geometry/#textual.geometry.Spacing.totals","title":"totals property","text":"
totals: tuple[int, int]\n

A pair of integers for the total horizontal and vertical space.

"},{"location":"api/geometry/#textual.geometry.Spacing.width","title":"width property","text":"
width: int\n

Total space in the x axis.

"},{"location":"api/geometry/#textual.geometry.Spacing.all","title":"all classmethod","text":"
def all(cls, amount):\n

Construct a Spacing with a given amount of spacing on all edges.

Parameters Name Type Description Default amount int

The magnitude of spacing to apply to all edges

required Returns Type Description Spacing

Spacing(amount, amount, amount, amount)

"},{"location":"api/geometry/#textual.geometry.Spacing.grow_maximum","title":"grow_maximum method","text":"
def grow_maximum(self, other):\n

Grow spacing with a maximum.

Parameters Name Type Description Default other Spacing

Spacing object.

required Returns Type Description Spacing

New spacing where the values are maximum of the two values.

"},{"location":"api/geometry/#textual.geometry.Spacing.horizontal","title":"horizontal classmethod","text":"
def horizontal(cls, amount):\n

Construct a Spacing with a given amount of spacing on horizontal edges, and no vertical spacing.

Parameters Name Type Description Default amount int

The magnitude of spacing to apply to horizontal edges

required Returns Type Description Spacing

Spacing(0, amount, 0, amount)

"},{"location":"api/geometry/#textual.geometry.Spacing.unpack","title":"unpack classmethod","text":"
def unpack(cls, pad):\n

Unpack padding specified in CSS style.

Parameters Name Type Description Default pad SpacingDimensions

An integer, or tuple of 1, 2, or 4 integers.

required Raises Type Description ValueError

If pad is an invalid value.

Returns Type Description Spacing

New Spacing object.

"},{"location":"api/geometry/#textual.geometry.Spacing.vertical","title":"vertical classmethod","text":"
def vertical(cls, amount):\n

Construct a Spacing with a given amount of spacing on vertical edges, and no horizontal spacing.

Parameters Name Type Description Default amount int

The magnitude of spacing to apply to vertical edges

required Returns Type Description Spacing

Spacing(amount, 0, amount, 0)

"},{"location":"api/geometry/#textual.geometry.clamp","title":"clamp function","text":"
def clamp(value, minimum, maximum):\n

Adjust a value so it is not less than a minimum and not greater than a maximum value.

Parameters Name Type Description Default value T

A value.

required minimum T

Minimum value.

required maximum T

Maximum value.

required Returns Type Description T

New value that is not less than the minimum or greater than the maximum.

"},{"location":"api/logger/","title":"Logger","text":"

A logger class that logs to the Textual console.

"},{"location":"api/logger/#textual.Logger","title":"textual.Logger class","text":"
def __init__(\nself,\nlog_callable,\ngroup=LogGroup.INFO,\nverbosity=LogVerbosity.NORMAL,\n):\n

A Textual logger.

"},{"location":"api/logger/#textual.Logger.debug","title":"debug property","text":"
debug: Logger\n

Logs debug messages.

"},{"location":"api/logger/#textual.Logger.error","title":"error property","text":"
error: Logger\n

Logs errors.

"},{"location":"api/logger/#textual.Logger.event","title":"event property","text":"
event: Logger\n

Logs events.

"},{"location":"api/logger/#textual.Logger.info","title":"info property","text":"
info: Logger\n

Logs information.

"},{"location":"api/logger/#textual.Logger.logging","title":"logging property","text":"
logging: Logger\n

Logs from stdlib logging module.

"},{"location":"api/logger/#textual.Logger.system","title":"system property","text":"
system: Logger\n

Logs system information.

"},{"location":"api/logger/#textual.Logger.verbose","title":"verbose property","text":"
verbose: Logger\n

A verbose logger.

"},{"location":"api/logger/#textual.Logger.warning","title":"warning property","text":"
warning: Logger\n

Logs warnings.

"},{"location":"api/logger/#textual.Logger.worker","title":"worker property","text":"
worker: Logger\n

Logs worker information.

"},{"location":"api/logger/#textual.Logger.verbosity","title":"verbosity method","text":"
def verbosity(self, verbose):\n

Get a new logger with selective verbosity.

Parameters Name Type Description Default verbose bool

True to use HIGH verbosity, otherwise NORMAL.

required Returns Type Description Logger

New logger.

"},{"location":"api/logging/","title":"Logging","text":"

A Textual Logging handler.

If there is an active Textual app, then log messages will go via the app (and logged via textual console).

If there is no active app, then log messages will go to stderr or stdout, depending on configuration.

"},{"location":"api/logging/#textual.logging.TextualHandler","title":"TextualHandler class","text":"
def __init__(self, stderr=True, stdout=False):\n

Bases: Handler

A Logging handler for Textual apps.

Parameters Name Type Description Default stderr bool

Log to stderr when there is no active app.

True stdout bool

Log to stdout when there is not active app.

False"},{"location":"api/logging/#textual.logging.TextualHandler.emit","title":"emit method","text":"
def emit(self, record):\n

Invoked by logging.

"},{"location":"api/map_geometry/","title":"Map geometry","text":"

A data structure returned by screen.find_widget.

"},{"location":"api/map_geometry/#textual._compositor.MapGeometry","title":"textual._compositor.MapGeometry class","text":"

Bases: NamedTuple

Defines the absolute location of a Widget.

"},{"location":"api/map_geometry/#textual._compositor.MapGeometry.clip","title":"clip instance-attribute","text":"
clip: Region\n

A region to clip the widget by (if a Widget is within a container).

"},{"location":"api/map_geometry/#textual._compositor.MapGeometry.container_size","title":"container_size instance-attribute","text":"
container_size: Size\n

The container size (area not occupied by scrollbars).

"},{"location":"api/map_geometry/#textual._compositor.MapGeometry.dock_gutter","title":"dock_gutter instance-attribute","text":"
dock_gutter: Spacing\n

Space from the container reserved by docked widgets.

"},{"location":"api/map_geometry/#textual._compositor.MapGeometry.order","title":"order instance-attribute","text":"
order: tuple[tuple[int, int, int], ...]\n

Tuple of tuples defining the painting order of the widget.

Each successive triple represents painting order information with regards to ancestors in the DOM hierarchy and the last triple provides painting order information for this specific widget.

"},{"location":"api/map_geometry/#textual._compositor.MapGeometry.region","title":"region instance-attribute","text":"
region: Region\n

The (screen) region occupied by the widget.

"},{"location":"api/map_geometry/#textual._compositor.MapGeometry.virtual_region","title":"virtual_region instance-attribute","text":"
virtual_region: Region\n

The region relative to the container (but not necessarily visible).

"},{"location":"api/map_geometry/#textual._compositor.MapGeometry.virtual_size","title":"virtual_size instance-attribute","text":"
virtual_size: Size\n

The virtual size (scrollable area) of a widget if it is a container.

"},{"location":"api/map_geometry/#textual._compositor.MapGeometry.visible_region","title":"visible_region property","text":"
visible_region: Region\n

The Widget region after clipping.

"},{"location":"api/message/","title":"Message","text":"

The base class for all messages (including events).

"},{"location":"api/message/#textual.message.Message","title":"Message class","text":"
def __init__(self):\n

Base class for a message.

"},{"location":"api/message/#textual.message.Message.ALLOW_SELECTOR_MATCH","title":"ALLOW_SELECTOR_MATCH class-attribute","text":"
ALLOW_SELECTOR_MATCH: set[str] = set()\n

Additional attributes that can be used with the on decorator.

These attributes must be widgets.

"},{"location":"api/message/#textual.message.Message.control","title":"control property","text":"
control: Widget | None\n

The widget associated with this message, or None by default.

"},{"location":"api/message/#textual.message.Message.handler_name","title":"handler_name class-attribute","text":"
handler_name: str\n

Name of the default message handler.

"},{"location":"api/message/#textual.message.Message.is_forwarded","title":"is_forwarded property","text":"
is_forwarded: bool\n

Has the message been forwarded?

"},{"location":"api/message/#textual.message.Message.prevent_default","title":"prevent_default method","text":"
def prevent_default(self, prevent=True):\n

Suppress the default action(s). This will prevent handlers in any base classes from being called.

Parameters Name Type Description Default prevent bool

True if the default action should be suppressed, or False if the default actions should be performed.

True"},{"location":"api/message/#textual.message.Message.stop","title":"stop method","text":"
def stop(self, stop=True):\n

Stop propagation of the message to parent.

Parameters Name Type Description Default stop bool

The stop flag.

True"},{"location":"api/message_pump/","title":"Message pump","text":"

A MessagePump is a base class for any object which processes messages, which includes Widget, Screen, and App.

Tip

Most of the method here are useful in general app development.

"},{"location":"api/message_pump/#textual.message_pump.MessagePump","title":"MessagePump class","text":"
def __init__(self, parent=None):\n

Base class which supplies a message pump.

"},{"location":"api/message_pump/#textual.message_pump.MessagePump.app","title":"app property","text":"
app: 'App[object]'\n

Get the current app.

Returns Type Description 'App[object]'

The current app.

Raises Type Description NoActiveAppError

if no active app could be found for the current asyncio context

"},{"location":"api/message_pump/#textual.message_pump.MessagePump.has_parent","title":"has_parent property","text":"
has_parent: bool\n

Does this object have a parent?

"},{"location":"api/message_pump/#textual.message_pump.MessagePump.is_attached","title":"is_attached property","text":"
is_attached: bool\n

Is the node attached to the app via the DOM?

"},{"location":"api/message_pump/#textual.message_pump.MessagePump.is_parent_active","title":"is_parent_active property","text":"
is_parent_active: bool\n

Is the parent active?

"},{"location":"api/message_pump/#textual.message_pump.MessagePump.is_running","title":"is_running property","text":"
is_running: bool\n

Is the message pump running (potentially processing messages)?

"},{"location":"api/message_pump/#textual.message_pump.MessagePump.log","title":"log property","text":"
log: Logger\n

Get a logger for this object.

Returns Type Description Logger

A logger.

"},{"location":"api/message_pump/#textual.message_pump.MessagePump.call_after_refresh","title":"call_after_refresh method","text":"
def call_after_refresh(self, callback, *args, **kwargs):\n

Schedule a callback to run after all messages are processed and the screen has been refreshed. Positional and keyword arguments are passed to the callable.

Parameters Name Type Description Default callback Callback

A callable.

required Returns Type Description bool

True if the callback was scheduled, or False if the callback could not be scheduled (may occur if the message pump was closed or closing).

"},{"location":"api/message_pump/#textual.message_pump.MessagePump.call_later","title":"call_later method","text":"
def call_later(self, callback, *args, **kwargs):\n

Schedule a callback to run after all messages are processed in this object. Positional and keywords arguments are passed to the callable.

Parameters Name Type Description Default callback Callback

Callable to call next.

required *args Any

Positional arguments to pass to the callable.

() **kwargs Any

Keyword arguments to pass to the callable.

{} Returns Type Description bool

True if the callback was scheduled, or False if the callback could not be scheduled (may occur if the message pump was closed or closing).

"},{"location":"api/message_pump/#textual.message_pump.MessagePump.call_next","title":"call_next method","text":"
def call_next(self, callback, *args, **kwargs):\n

Schedule a callback to run immediately after processing the current message.

Parameters Name Type Description Default callback Callback

Callable to run after current event.

required *args Any

Positional arguments to pass to the callable.

() **kwargs Any

Keyword arguments to pass to the callable.

{}"},{"location":"api/message_pump/#textual.message_pump.MessagePump.check_idle","title":"check_idle method","text":"
def check_idle(self):\n

Prompt the message pump to call idle if the queue is empty.

"},{"location":"api/message_pump/#textual.message_pump.MessagePump.check_message_enabled","title":"check_message_enabled method","text":"
def check_message_enabled(self, message):\n

Check if a given message is enabled (allowed to be sent).

Parameters Name Type Description Default message Message

A message object.

required Returns Type Description bool

True if the message will be sent, or False if it is disabled.

"},{"location":"api/message_pump/#textual.message_pump.MessagePump.disable_messages","title":"disable_messages method","text":"
def disable_messages(self, *messages):\n

Disable message types from being processed.

"},{"location":"api/message_pump/#textual.message_pump.MessagePump.dispatch_key","title":"dispatch_key async","text":"
def dispatch_key(self, event):\n

Dispatch a key event to method.

This method will call the method named 'key_' if it exists. Some keys have aliases. The first alias found will be invoked if it exists. If multiple handlers exist that match the key, an exception is raised. Parameters Name Type Description Default event events.Key

A key event.

required Returns Type Description bool

True if key was handled, otherwise False.

Raises Type Description DuplicateKeyHandlers

When there's more than 1 handler that could handle this key.

"},{"location":"api/message_pump/#textual.message_pump.MessagePump.enable_messages","title":"enable_messages method","text":"
def enable_messages(self, *messages):\n

Enable processing of messages types.

"},{"location":"api/message_pump/#textual.message_pump.MessagePump.on_event","title":"on_event async","text":"
def on_event(self, event):\n

Called to process an event.

Parameters Name Type Description Default event events.Event

An Event object.

required"},{"location":"api/message_pump/#textual.message_pump.MessagePump.post_message","title":"post_message method","text":"
def post_message(self, message):\n

Posts a message on to this widget's queue.

Parameters Name Type Description Default message Message

A message (including Event).

required Returns Type Description bool

True if the messages was processed, False if it wasn't.

"},{"location":"api/message_pump/#textual.message_pump.MessagePump.prevent","title":"prevent method","text":"
def prevent(self, *message_types):\n

A context manager to temporarily prevent the given message types from being posted.

Example
input = self.query_one(Input)\nwith self.prevent(Input.Changed):\ninput.value = \"foo\"\n
"},{"location":"api/message_pump/#textual.message_pump.MessagePump.set_interval","title":"set_interval method","text":"
def set_interval(\nself,\ninterval,\ncallback=None,\n*,\nname=None,\nrepeat=0,\npause=False\n):\n

Call a function at periodic intervals.

Parameters Name Type Description Default interval float

Time between calls.

required callback TimerCallback | None

Function to call.

None name str | None

Name of the timer object.

None repeat int

Number of times to repeat the call or 0 for continuous.

0 pause bool

Start the timer paused.

False Returns Type Description Timer

A timer object.

"},{"location":"api/message_pump/#textual.message_pump.MessagePump.set_timer","title":"set_timer method","text":"
def set_timer(\nself, delay, callback=None, *, name=None, pause=False\n):\n

Make a function call after a delay.

Parameters Name Type Description Default delay float

Time to wait before invoking callback.

required callback TimerCallback | None

Callback to call after time has expired.

None name str | None

Name of the timer (for debug).

None pause bool

Start timer paused.

False Returns Type Description Timer

A timer object.

"},{"location":"api/on/","title":"On","text":"

Decorator to declare that the method is a message handler.

The decorator accepts an optional CSS selector that will be matched against a widget exposed by a control attribute on the message.

Example
# Handle the press of buttons with ID \"#quit\".\n@on(Button.Pressed, \"#quit\")\ndef quit_button(self) -> None:\nself.app.quit()\n

Keyword arguments can be used to match additional selectors for attributes listed in ALLOW_SELECTOR_MATCH.

Example
# Handle the activation of the tab \"#home\" within the `TabbedContent` \"#tabs\".\n@on(TabbedContent.TabActivated, \"#tabs\", tab=\"#home\")\ndef switch_to_home(self) -> None:\nself.log(\"Switching back to the home tab.\")\n...\n
Parameters Name Type Description Default message_type type[Message]

The message type (i.e. the class).

required selector str | None

An optional selector. If supplied, the handler will only be called if selector matches the widget from the control attribute of the message.

None **kwargs str

Additional selectors for other attributes of the message.

{}"},{"location":"api/pilot/","title":"Pilot","text":"

The pilot object is used by App.run_test to programmatically operate an app.

See the guide on how to test Textual apps.

"},{"location":"api/pilot/#textual.pilot.Pilot","title":"Pilot class","text":"
def __init__(self, app):\n

Bases: Generic[ReturnType]

Pilot object to drive an app.

"},{"location":"api/pilot/#textual.pilot.Pilot.app","title":"app property","text":"
app: App[ReturnType]\n
"},{"location":"api/pilot/#textual.pilot.Pilot.click","title":"click async","text":"
def click(\nself,\nselector=None,\noffset=(0, 0),\nshift=False,\nmeta=False,\ncontrol=False,\n):\n

Simulate clicking with the mouse.

Parameters Name Type Description Default selector type[Widget] | str | None

The widget that should be clicked. If None, then the click will occur relative to the screen. Note that this simply causes a click to occur at the location of the widget. If the widget is currently hidden or obscured by another widget, then the click may not land on it.

None offset tuple[int, int]

The offset to click within the selected widget.

(0, 0) shift bool

Click with the shift key held down.

False meta bool

Click with the meta key held down.

False control bool

Click with the control key held down.

False"},{"location":"api/pilot/#textual.pilot.Pilot.exit","title":"exit async","text":"
def exit(self, result):\n

Exit the app with the given result.

Parameters Name Type Description Default result ReturnType

The app result returned by run or run_async.

required"},{"location":"api/pilot/#textual.pilot.Pilot.hover","title":"hover async","text":"
def hover(self, selector=None, offset=(0, 0)):\n

Simulate hovering with the mouse cursor.

Parameters Name Type Description Default selector type[Widget] | str | None | None

The widget that should be hovered. If None, then the click will occur relative to the screen. Note that this simply causes a hover to occur at the location of the widget. If the widget is currently hidden or obscured by another widget, then the hover may not land on it.

None offset tuple[int, int]

The offset to hover over within the selected widget.

(0, 0)"},{"location":"api/pilot/#textual.pilot.Pilot.pause","title":"pause async","text":"
def pause(self, delay=None):\n

Insert a pause.

Parameters Name Type Description Default delay float | None

Seconds to pause, or None to wait for cpu idle.

None"},{"location":"api/pilot/#textual.pilot.Pilot.press","title":"press async","text":"
def press(self, *keys):\n

Simulate key-presses.

Parameters Name Type Description Default *keys str

Keys to press.

()"},{"location":"api/pilot/#textual.pilot.Pilot.wait_for_animation","title":"wait_for_animation async","text":"
def wait_for_animation(self):\n

Wait for any current animation to complete.

"},{"location":"api/pilot/#textual.pilot.Pilot.wait_for_scheduled_animations","title":"wait_for_scheduled_animations async","text":"
def wait_for_scheduled_animations(self):\n

Wait for any current and scheduled animations to complete.

"},{"location":"api/pilot/#textual.pilot.WaitForScreenTimeout","title":"WaitForScreenTimeout class","text":"

Bases: Exception

Exception raised if messages aren't being processed quickly enough.

If this occurs, the most likely explanation is some kind of deadlock in the app code.

"},{"location":"api/query/","title":"Query","text":"

A DOMQuery is a set of DOM nodes returned by query.

The set of nodes may be further refined with filter and exclude. Additional methods apply actions to all nodes in the query.

Info

If this sounds like JQuery, a (once) popular JS library, it is no coincidence.

"},{"location":"api/query/#textual.css.query.DOMQuery","title":"DOMQuery class","text":"
def __init__(\nself, node, *, filter=None, exclude=None, parent=None\n):\n

Bases: Generic[QueryType]

Warning

You won't need to construct this manually, as DOMQuery objects are returned by query.

Parameters Name Type Description Default node DOMNode

A DOM node.

required filter str | None

Query to filter children in the node.

None exclude str | None

Query to exclude children in the node.

None parent DOMQuery | None

The parent query, if this is the result of filtering another query.

None Raises Type Description InvalidQueryFormat

If the format of the query is invalid.

"},{"location":"api/query/#textual.css.query.DOMQuery.node","title":"node property","text":"
node: DOMNode\n

The node being queried.

"},{"location":"api/query/#textual.css.query.DOMQuery.nodes","title":"nodes property","text":"
nodes: list[QueryType]\n

Lazily evaluate nodes.

"},{"location":"api/query/#textual.css.query.DOMQuery.add_class","title":"add_class method","text":"
def add_class(self, *class_names):\n

Add the given class name(s) to nodes.

"},{"location":"api/query/#textual.css.query.DOMQuery.exclude","title":"exclude method","text":"
def exclude(self, selector):\n

Exclude nodes that match a given selector.

Parameters Name Type Description Default selector str

A CSS selector.

required Returns Type Description DOMQuery[QueryType]

New DOM query.

"},{"location":"api/query/#textual.css.query.DOMQuery.filter","title":"filter method","text":"
def filter(self, selector):\n

Filter this set by the given CSS selector.

Parameters Name Type Description Default selector str

A CSS selector.

required Returns Type Description DOMQuery[QueryType]

New DOM Query.

"},{"location":"api/query/#textual.css.query.DOMQuery.first","title":"first method","text":"
def first(self, expect_type=None):\n

Get the first matching node.

Parameters Name Type Description Default expect_type type[ExpectType] | None

Require matched node is of this type, or None for any type.

None Raises Type Description WrongType

If the wrong type was found.

NoMatches

If there are no matching nodes in the query.

Returns Type Description QueryType | ExpectType

The matching Widget.

"},{"location":"api/query/#textual.css.query.DOMQuery.last","title":"last method","text":"
def last(self, expect_type=None):\n

Get the last matching node.

Parameters Name Type Description Default expect_type type[ExpectType] | None

Require matched node is of this type, or None for any type.

None Raises Type Description WrongType

If the wrong type was found.

NoMatches

If there are no matching nodes in the query.

Returns Type Description QueryType | ExpectType

The matching Widget.

"},{"location":"api/query/#textual.css.query.DOMQuery.only_one","title":"only_one method","text":"
def only_one(self, expect_type=None):\n

Get the only matching node.

Parameters Name Type Description Default expect_type type[ExpectType] | None

Require matched node is of this type, or None for any type.

None Raises Type Description WrongType

If the wrong type was found.

NoMatches

If no node matches the query.

TooManyMatches

If there is more than one matching node in the query.

Returns Type Description Widget | ExpectType

The matching Widget.

"},{"location":"api/query/#textual.css.query.DOMQuery.refresh","title":"refresh method","text":"
def refresh(self, *, repaint=True, layout=False):\n

Refresh matched nodes.

Parameters Name Type Description Default repaint bool

Repaint node(s).

True layout bool

Layout node(s).

False Returns Type Description DOMQuery[QueryType]

Query for chaining.

"},{"location":"api/query/#textual.css.query.DOMQuery.remove","title":"remove method","text":"
def remove(self):\n

Remove matched nodes from the DOM.

Returns Type Description AwaitRemove

An awaitable object that waits for the widgets to be removed.

"},{"location":"api/query/#textual.css.query.DOMQuery.remove_class","title":"remove_class method","text":"
def remove_class(self, *class_names):\n

Remove the given class names from the nodes.

"},{"location":"api/query/#textual.css.query.DOMQuery.results","title":"results method","text":"
def results(self, filter_type=None):\n

Get query results, optionally filtered by a given type.

Parameters Name Type Description Default filter_type type[ExpectType] | None

A Widget class to filter results, or None for no filter.

None

Yields:

Type Description Widget | ExpectType

Iterator[Widget | ExpectType]: An iterator of Widget instances.

"},{"location":"api/query/#textual.css.query.DOMQuery.set_class","title":"set_class method","text":"
def set_class(self, add, *class_names):\n

Set the given class name(s) according to a condition.

Parameters Name Type Description Default add bool

Add the classes if True, otherwise remove them.

required Returns Type Description DOMQuery[QueryType]

Self.

"},{"location":"api/query/#textual.css.query.DOMQuery.set_classes","title":"set_classes method","text":"
def set_classes(self, classes):\n

Set the classes on nodes to exactly the given set.

Parameters Name Type Description Default classes str | Iterable[str]

A string of space separated classes, or an iterable of class names.

required Returns Type Description DOMQuery[QueryType]

Self.

"},{"location":"api/query/#textual.css.query.DOMQuery.set_styles","title":"set_styles method","text":"
def set_styles(self, css=None, **update_styles):\n

Set styles on matched nodes.

Parameters Name Type Description Default css str | None

CSS declarations to parser, or None.

None"},{"location":"api/query/#textual.css.query.DOMQuery.toggle_class","title":"toggle_class method","text":"
def toggle_class(self, *class_names):\n

Toggle the given class names from matched nodes.

"},{"location":"api/query/#textual.css.query.InvalidQueryFormat","title":"InvalidQueryFormat class","text":"

Bases: QueryError

Query did not parse correctly.

"},{"location":"api/query/#textual.css.query.NoMatches","title":"NoMatches class","text":"

Bases: QueryError

No nodes matched the query.

"},{"location":"api/query/#textual.css.query.QueryError","title":"QueryError class","text":"

Bases: Exception

Base class for a query related error.

"},{"location":"api/query/#textual.css.query.TooManyMatches","title":"TooManyMatches class","text":"

Bases: QueryError

Too many nodes matched the query.

"},{"location":"api/query/#textual.css.query.WrongType","title":"WrongType class","text":"

Bases: QueryError

Query result was not of the correct type.

"},{"location":"api/reactive/","title":"Reactive","text":"

The Reactive class implements reactivity.

"},{"location":"api/reactive/#textual.reactive.Reactive","title":"Reactive class","text":"
def __init__(\nself,\ndefault,\n*,\nlayout=False,\nrepaint=True,\ninit=False,\nalways_update=False,\ncompute=True\n):\n

Bases: Generic[ReactiveType]

Reactive descriptor.

Parameters Name Type Description Default default ReactiveType | Callable[[], ReactiveType]

A default value or callable that returns a default.

required layout bool

Perform a layout on change.

False repaint bool

Perform a repaint on change.

True init bool

Call watchers on initialize (post mount).

False always_update bool

Call watchers even when the new value equals the old value.

False compute bool

Run compute methods when attribute is changed.

True"},{"location":"api/reactive/#textual.reactive.TooManyComputesError","title":"TooManyComputesError class","text":"

Bases: Exception

Raised when an attribute has public and private compute methods.

"},{"location":"api/reactive/#textual.reactive.reactive","title":"reactive class","text":"
def __init__(\nself,\ndefault,\n*,\nlayout=False,\nrepaint=True,\ninit=True,\nalways_update=False\n):\n

Bases: Reactive[ReactiveType]

Create a reactive attribute.

Parameters Name Type Description Default default ReactiveType | Callable[[], ReactiveType]

A default value or callable that returns a default.

required layout bool

Perform a layout on change.

False repaint bool

Perform a repaint on change.

True init bool

Call watchers on initialize (post mount).

True always_update bool

Call watchers even when the new value equals the old value.

False"},{"location":"api/reactive/#textual.reactive.var","title":"var class","text":"
def __init__(self, default, init=True, always_update=False):\n

Bases: Reactive[ReactiveType]

Create a reactive attribute (with no auto-refresh).

Parameters Name Type Description Default default ReactiveType | Callable[[], ReactiveType]

A default value or callable that returns a default.

required init bool

Call watchers on initialize (post mount).

True always_update bool

Call watchers even when the new value equals the old value.

False"},{"location":"api/screen/","title":"Screen","text":"

The Screen class is a special widget which represents the content in the terminal. See Screens for details.

"},{"location":"api/screen/#textual.screen.ScreenResultCallbackType","title":"ScreenResultCallbackType module-attribute","text":"
ScreenResultCallbackType = Union[\nCallable[[ScreenResultType], None],\nCallable[[ScreenResultType], Awaitable[None]],\n]\n

Type of a screen result callback function.

"},{"location":"api/screen/#textual.screen.ScreenResultType","title":"ScreenResultType module-attribute","text":"
ScreenResultType = TypeVar('ScreenResultType')\n

The result type of a screen.

"},{"location":"api/screen/#textual.screen.ModalScreen","title":"ModalScreen class","text":"
def __init__(self, name=None, id=None, classes=None):\n

Bases: Screen[ScreenResultType]

A screen with bindings that take precedence over the App's key bindings.

The default styling of a modal screen will dim the screen underneath.

"},{"location":"api/screen/#textual.screen.ResultCallback","title":"ResultCallback class","text":"
def __init__(self, requester, callback):\n

Bases: Generic[ScreenResultType]

Holds the details of a callback.

Parameters Name Type Description Default requester MessagePump

The object making a request for the callback.

required callback ScreenResultCallbackType[ScreenResultType] | None

The callback function.

required"},{"location":"api/screen/#textual.screen.ResultCallback.callback","title":"callback instance-attribute","text":"
callback: ScreenResultCallbackType | None = callback\n

The callback function.

"},{"location":"api/screen/#textual.screen.ResultCallback.requester","title":"requester instance-attribute","text":"
requester = requester\n

The object in the DOM that requested the callback.

"},{"location":"api/screen/#textual.screen.Screen","title":"Screen class","text":"
def __init__(self, name=None, id=None, classes=None):\n

Bases: Generic[ScreenResultType], Widget

The base class for screens.

Parameters Name Type Description Default name str | None

The name of the screen.

None id str | None

The ID of the screen in the DOM.

None classes str | None

The CSS classes for the screen.

None"},{"location":"api/screen/#textual.screen.Screen.AUTO_FOCUS","title":"AUTO_FOCUS class-attribute","text":"
AUTO_FOCUS: str | None = None\n

A selector to determine what to focus automatically when the screen is activated.

The widget focused is the first that matches the given CSS selector. Set to None to inherit the value from the screen's app. Set to \"\" to disable auto focus.

"},{"location":"api/screen/#textual.screen.Screen.COMMANDS","title":"COMMANDS class-attribute","text":"
COMMANDS: set[type[Provider]] = set()\n

Command providers used by the command palette, associated with the screen.

Should be a set of command.Provider classes.

"},{"location":"api/screen/#textual.screen.Screen.CSS","title":"CSS class-attribute","text":"
CSS: str = ''\n

Inline CSS, useful for quick scripts. Rules here take priority over CSS_PATH.

Note

This CSS applies to the whole app.

"},{"location":"api/screen/#textual.screen.Screen.CSS_PATH","title":"CSS_PATH class-attribute","text":"
CSS_PATH: CSSPathType | None = None\n

File paths to load CSS from.

Note

This CSS applies to the whole app.

"},{"location":"api/screen/#textual.screen.Screen.SUB_TITLE","title":"SUB_TITLE class-attribute","text":"
SUB_TITLE: str | None = None\n

A class variable to set the default sub-title for the screen.

This overrides the app sub-title. To update the sub-title while the screen is running, you can set the sub_title attribute.

"},{"location":"api/screen/#textual.screen.Screen.TITLE","title":"TITLE class-attribute","text":"
TITLE: str | None = None\n

A class variable to set the default title for the screen.

This overrides the app title. To update the title while the screen is running, you can set the title attribute.

"},{"location":"api/screen/#textual.screen.Screen.focus_chain","title":"focus_chain property","text":"
focus_chain: list[Widget]\n

A list of widgets that may receive focus, in focus order.

"},{"location":"api/screen/#textual.screen.Screen.focused","title":"focused instance-attribute class-attribute","text":"
focused: Reactive[Widget | None] = Reactive(None)\n

The focused widget or None for no focus.

"},{"location":"api/screen/#textual.screen.Screen.is_current","title":"is_current property","text":"
is_current: bool\n

Is the screen current (i.e. visible to user)?

"},{"location":"api/screen/#textual.screen.Screen.is_modal","title":"is_modal property","text":"
is_modal: bool\n

Is the screen modal?

"},{"location":"api/screen/#textual.screen.Screen.layers","title":"layers property","text":"
layers: tuple[str, ...]\n

Layers from parent.

Returns Type Description tuple[str, ...]

Tuple of layer names.

"},{"location":"api/screen/#textual.screen.Screen.stack_updates","title":"stack_updates instance-attribute class-attribute","text":"
stack_updates: Reactive[int] = Reactive(0, repaint=False)\n

An integer that updates when the screen is resumed.

"},{"location":"api/screen/#textual.screen.Screen.sub_title","title":"sub_title instance-attribute class-attribute","text":"
sub_title: Reactive[str | None] = self.SUB_TITLE\n

Screen sub-title to override the app sub-title.

"},{"location":"api/screen/#textual.screen.Screen.title","title":"title instance-attribute class-attribute","text":"
title: Reactive[str | None] = self.TITLE\n

Screen title to override the app title.

"},{"location":"api/screen/#textual.screen.Screen.action_dismiss","title":"action_dismiss method","text":"
def action_dismiss(self, result=_NoResult):\n

A wrapper around dismiss that can be called as an action.

Parameters Name Type Description Default result ScreenResultType | Type[_NoResult]

The optional result to be passed to the result callback.

_NoResult"},{"location":"api/screen/#textual.screen.Screen.can_view","title":"can_view method","text":"
def can_view(self, widget):\n

Check if a given widget is in the current view (scrollable area).

Note: This doesn't necessarily equate to a widget being visible. There are other reasons why a widget may not be visible.

Parameters Name Type Description Default widget Widget

A widget that is a descendant of self.

required Returns Type Description bool

True if the entire widget is in view, False if it is partially visible or not in view.

"},{"location":"api/screen/#textual.screen.Screen.dismiss","title":"dismiss method","text":"
def dismiss(self, result=_NoResult):\n

Dismiss the screen, optionally with a result.

If result is provided and a callback was set when the screen was pushed, then the callback will be invoked with result.

Parameters Name Type Description Default result ScreenResultType | Type[_NoResult]

The optional result to be passed to the result callback.

_NoResult Raises Type Description ScreenStackError

If trying to dismiss a screen that is not at the top of the stack.

"},{"location":"api/screen/#textual.screen.Screen.find_widget","title":"find_widget method","text":"
def find_widget(self, widget):\n

Get the screen region of a Widget.

Parameters Name Type Description Default widget Widget

A Widget within the composition.

required Returns Type Description MapGeometry

Region relative to screen.

Raises Type Description NoWidget

If the widget could not be found in this screen.

"},{"location":"api/screen/#textual.screen.Screen.focus_next","title":"focus_next method","text":"
def focus_next(self, selector='*'):\n

Focus the next widget, optionally filtered by a CSS selector.

If no widget is currently focused, this will focus the first focusable widget. If no focusable widget matches the given CSS selector, focus is set to None.

Parameters Name Type Description Default selector str | type[QueryType]

CSS selector to filter what nodes can be focused.

'*' Returns Type Description Widget | None

Newly focused widget, or None for no focus. If the return is not None, then it is guaranteed that the widget returned matches the CSS selectors given in the argument.

"},{"location":"api/screen/#textual.screen.Screen.focus_previous","title":"focus_previous method","text":"
def focus_previous(self, selector='*'):\n

Focus the previous widget, optionally filtered by a CSS selector.

If no widget is currently focused, this will focus the first focusable widget. If no focusable widget matches the given CSS selector, focus is set to None.

Parameters Name Type Description Default selector str | type[QueryType]

CSS selector to filter what nodes can be focused.

'*' Returns Type Description Widget | None

Newly focused widget, or None for no focus. If the return is not None, then it is guaranteed that the widget returned matches the CSS selectors given in the argument.

"},{"location":"api/screen/#textual.screen.Screen.get_offset","title":"get_offset method","text":"
def get_offset(self, widget):\n

Get the absolute offset of a given Widget.

Parameters Name Type Description Default widget Widget

A widget

required Returns Type Description Offset

The widget's offset relative to the top left of the terminal.

"},{"location":"api/screen/#textual.screen.Screen.get_style_at","title":"get_style_at method","text":"
def get_style_at(self, x, y):\n

Get the style under a given coordinate.

Parameters Name Type Description Default x int

X Coordinate.

required y int

Y Coordinate.

required Returns Type Description Style

Rich Style object.

"},{"location":"api/screen/#textual.screen.Screen.get_widget_at","title":"get_widget_at method","text":"
def get_widget_at(self, x, y):\n

Get the widget at a given coordinate.

Parameters Name Type Description Default x int

X Coordinate.

required y int

Y Coordinate.

required Returns Type Description tuple[Widget, Region]

Widget and screen region.

"},{"location":"api/screen/#textual.screen.Screen.get_widgets_at","title":"get_widgets_at method","text":"
def get_widgets_at(self, x, y):\n

Get all widgets under a given coordinate.

Parameters Name Type Description Default x int

X coordinate.

required y int

Y coordinate.

required Returns Type Description Iterable[tuple[Widget, Region]]

Sequence of (WIDGET, REGION) tuples.

"},{"location":"api/screen/#textual.screen.Screen.set_focus","title":"set_focus method","text":"
def set_focus(self, widget, scroll_visible=True):\n

Focus (or un-focus) a widget. A focused widget will receive key events first.

Parameters Name Type Description Default widget Widget | None

Widget to focus, or None to un-focus.

required scroll_visible bool

Scroll widget in to view.

True"},{"location":"api/screen/#textual.screen.Screen.validate_sub_title","title":"validate_sub_title method","text":"
def validate_sub_title(self, sub_title):\n

Ensure the sub-title is a string or None.

"},{"location":"api/screen/#textual.screen.Screen.validate_title","title":"validate_title method","text":"
def validate_title(self, title):\n

Ensure the title is a string or None.

"},{"location":"api/scroll_view/","title":"Scroll view","text":"

ScrollView is a base class for line api widgets.

"},{"location":"api/scroll_view/#textual.scroll_view.ScrollView","title":"ScrollView class","text":"

Bases: ScrollableContainer

A base class for a Widget that handles its own scrolling (i.e. doesn't rely on the compositor to render children).

"},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.is_scrollable","title":"is_scrollable property","text":"
is_scrollable: bool\n

Always scrollable.

"},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.refresh_lines","title":"refresh_lines method","text":"
def refresh_lines(self, y_start, line_count=1):\n

Refresh one or more lines.

Parameters Name Type Description Default y_start int

First line to refresh.

required line_count int

Total number of lines to refresh.

1"},{"location":"api/scroll_view/#textual.scroll_view.ScrollView.scroll_to","title":"scroll_to method","text":"
def scroll_to(\nself,\nx=None,\ny=None,\n*,\nanimate=True,\nspeed=None,\nduration=None,\neasing=None,\nforce=False,\non_complete=None\n):\n

Scroll to a given (absolute) coordinate, optionally animating.

Parameters Name Type Description Default x float | None

X coordinate (column) to scroll to, or None for no change.

None y float | None

Y coordinate (row) to scroll to, or None for no change.

None animate bool

Animate to new scroll position.

True speed float | None

Speed of scroll if animate is True; or None to use duration.

None duration float | None

Duration of animation, if animate is True and speed is None.

None easing EasingFunction | str | None

An easing method for the scrolling animation.

None force bool

Force scrolling even when prohibited by overflow styling.

False on_complete CallbackType | None

A callable to invoke when the animation is finished.

None"},{"location":"api/scrollbar/","title":"Scrollbar","text":"

Implements the scrollbar-related widgets for internal use.

You will not need to use the widgets defined in this module.

"},{"location":"api/scrollbar/#textual.scrollbar.ScrollBar","title":"ScrollBar class","text":"
def __init__(self, vertical=True, name=None, *, thickness=1):\n

Bases: Widget

"},{"location":"api/scrollbar/#textual.scrollbar.ScrollBar.renderer","title":"renderer class-attribute","text":"
renderer: Type[ScrollBarRender] = ScrollBarRender\n

The class used for rendering scrollbars. This can be overridden and set to a ScrollBarRender-derived class in order to delegate all scrollbar rendering to that class. E.g.:

class MyScrollBarRender(ScrollBarRender): ...\n\napp = MyApp()\nScrollBar.renderer = MyScrollBarRender\napp.run()\n

Because this variable is accessed through specific instances (rather than through the class ScrollBar itself) it is also possible to set this on specific scrollbar instance to change only that instance:

my_widget.horizontal_scrollbar.renderer = MyScrollBarRender\n
"},{"location":"api/scrollbar/#textual.scrollbar.ScrollBar.action_grab","title":"action_grab method","text":"
def action_grab(self):\n

Begin capturing the mouse cursor.

"},{"location":"api/scrollbar/#textual.scrollbar.ScrollBar.action_scroll_down","title":"action_scroll_down method","text":"
def action_scroll_down(self):\n

Scroll vertical scrollbars down, horizontal scrollbars right.

"},{"location":"api/scrollbar/#textual.scrollbar.ScrollBar.action_scroll_up","title":"action_scroll_up method","text":"
def action_scroll_up(self):\n

Scroll vertical scrollbars up, horizontal scrollbars left.

"},{"location":"api/scrollbar/#textual.scrollbar.ScrollBarCorner","title":"ScrollBarCorner class","text":"
def __init__(self, name=None):\n

Bases: Widget

Widget which fills the gap between horizontal and vertical scrollbars, should they both be present.

"},{"location":"api/scrollbar/#textual.scrollbar.ScrollDown","title":"ScrollDown class","text":"

Bases: ScrollMessage

Message sent when clicking below handle.

"},{"location":"api/scrollbar/#textual.scrollbar.ScrollLeft","title":"ScrollLeft class","text":"

Bases: ScrollMessage

Message sent when clicking above handle.

"},{"location":"api/scrollbar/#textual.scrollbar.ScrollMessage","title":"ScrollMessage class","text":"

Bases: Message

Base class for all scrollbar messages.

"},{"location":"api/scrollbar/#textual.scrollbar.ScrollRight","title":"ScrollRight class","text":"

Bases: ScrollMessage

Message sent when clicking below handle.

"},{"location":"api/scrollbar/#textual.scrollbar.ScrollTo","title":"ScrollTo class","text":"
def __init__(self, x=None, y=None, animate=True):\n

Bases: ScrollMessage

Message sent when click and dragging handle.

"},{"location":"api/scrollbar/#textual.scrollbar.ScrollUp","title":"ScrollUp class","text":"

Bases: ScrollMessage

Message sent when clicking above handle.

"},{"location":"api/strip/","title":"Strip","text":"

A Strip contains the result of rendering a widget. See line API for how to use Strips.

"},{"location":"api/strip/#textual.strip.Strip","title":"Strip class","text":"
def __init__(self, segments, cell_length=None):\n

Represents a 'strip' (horizontal line) of a Textual Widget.

A Strip is like an immutable list of Segments. The immutability allows for effective caching.

Parameters Name Type Description Default segments Iterable[Segment]

An iterable of segments.

required cell_length int | None

The cell length if known, or None to calculate on demand.

None"},{"location":"api/strip/#textual.strip.Strip.cell_length","title":"cell_length property","text":"
cell_length: int\n

Get the number of cells required to render this object.

"},{"location":"api/strip/#textual.strip.Strip.link_ids","title":"link_ids property","text":"
link_ids: set[str]\n

A set of the link ids in this Strip.

"},{"location":"api/strip/#textual.strip.Strip.text","title":"text property","text":"
text: str\n

Segment text.

"},{"location":"api/strip/#textual.strip.Strip.adjust_cell_length","title":"adjust_cell_length method","text":"
def adjust_cell_length(self, cell_length, style=None):\n

Adjust the cell length, possibly truncating or extending.

Parameters Name Type Description Default cell_length int

New desired cell length.

required style Style | None

Style when extending, or None.

None Returns Type Description Strip

A new strip with the supplied cell length.

"},{"location":"api/strip/#textual.strip.Strip.apply_filter","title":"apply_filter method","text":"
def apply_filter(self, filter, background):\n

Apply a filter to all segments in the strip.

Parameters Name Type Description Default filter LineFilter

A line filter object.

required Returns Type Description Strip

A new Strip.

"},{"location":"api/strip/#textual.strip.Strip.apply_style","title":"apply_style method","text":"
def apply_style(self, style):\n

Apply a style to the Strip.

Parameters Name Type Description Default style Style

A Rich style.

required Returns Type Description Strip

A new strip.

"},{"location":"api/strip/#textual.strip.Strip.blank","title":"blank classmethod","text":"
def blank(cls, cell_length, style=None):\n

Create a blank strip.

Parameters Name Type Description Default cell_length int

Desired cell length.

required style StyleType | None

Style of blank.

None Returns Type Description Strip

New strip.

"},{"location":"api/strip/#textual.strip.Strip.crop","title":"crop method","text":"
def crop(self, start, end=None):\n

Crop a strip between two cell positions.

Parameters Name Type Description Default start int

The start cell position (inclusive).

required end int | None

The end cell position (exclusive).

None Returns Type Description Strip

A new Strip.

"},{"location":"api/strip/#textual.strip.Strip.crop_extend","title":"crop_extend method","text":"
def crop_extend(self, start, end, style):\n

Crop between two points, extending the length if required.

Parameters Name Type Description Default start int

Start offset of crop.

required end int

End offset of crop.

required style Style | None

Style of additional padding.

required Returns Type Description Strip

New cropped Strip.

"},{"location":"api/strip/#textual.strip.Strip.divide","title":"divide method","text":"
def divide(self, cuts):\n

Divide the strip in to multiple smaller strips by cutting at given (cell) indices.

Parameters Name Type Description Default cuts Iterable[int]

An iterable of cell positions as ints.

required Returns Type Description Sequence[Strip]

A new list of strips.

"},{"location":"api/strip/#textual.strip.Strip.extend_cell_length","title":"extend_cell_length method","text":"
def extend_cell_length(self, cell_length, style=None):\n

Extend the cell length if it is less than the given value.

Parameters Name Type Description Default cell_length int

Required minimum cell length.

required style Style | None

Style for padding if the cell length is extended.

None Returns Type Description Strip

A new Strip.

"},{"location":"api/strip/#textual.strip.Strip.from_lines","title":"from_lines classmethod","text":"
def from_lines(cls, lines, cell_length=None):\n

Convert lines (lists of segments) to a list of Strips.

Parameters Name Type Description Default lines list[list[Segment]]

List of lines, where a line is a list of segments.

required cell_length int | None

Cell length of lines (must be same) or None if not known.

None Returns Type Description list[Strip]

List of strips.

"},{"location":"api/strip/#textual.strip.Strip.index_to_cell_position","title":"index_to_cell_position method","text":"
def index_to_cell_position(self, index):\n

Given a character index, return the cell position of that character. This is the sum of the cell lengths of all the characters before the character at index.

Parameters Name Type Description Default index int

The index to convert.

required Returns Type Description int

The cell position of the character at index.

"},{"location":"api/strip/#textual.strip.Strip.join","title":"join classmethod","text":"
def join(cls, strips):\n

Join a number of strips in to one.

Parameters Name Type Description Default strips Iterable[Strip | None]

An iterable of Strips.

required Returns Type Description Strip

A new combined strip.

"},{"location":"api/strip/#textual.strip.Strip.simplify","title":"simplify method","text":"
def simplify(self):\n

Simplify the segments (join segments with same style)

Returns Type Description Strip

New strip.

"},{"location":"api/strip/#textual.strip.Strip.style_links","title":"style_links method","text":"
def style_links(self, link_id, link_style):\n

Apply a style to Segments with the given link_id.

Parameters Name Type Description Default link_id str

A link id.

required link_style Style

Style to apply.

required Returns Type Description Strip

New strip (or same Strip if no changes).

"},{"location":"api/strip/#textual.strip.StripRenderable","title":"StripRenderable class","text":"
def __init__(self, strips):\n

A renderable which renders a list of strips in to lines.

"},{"location":"api/strip/#textual.strip.get_line_length","title":"get_line_length function","text":"
def get_line_length(segments):\n

Get the line length (total length of all segments).

Parameters Name Type Description Default segments Iterable[Segment]

Iterable of segments.

required Returns Type Description int

Length of line in cells.

"},{"location":"api/suggester/","title":"Suggester","text":"

The Suggester class is used by the Input widget.

"},{"location":"api/suggester/#textual.suggester.SuggestFromList","title":"SuggestFromList class","text":"
def __init__(self, suggestions, *, case_sensitive=True):\n

Bases: Suggester

Give completion suggestions based on a fixed list of options.

Example
countries = [\"England\", \"Scotland\", \"Portugal\", \"Spain\", \"France\"]\nclass MyApp(App[None]):\ndef compose(self) -> ComposeResult:\nyield Input(suggester=SuggestFromList(countries, case_sensitive=False))\n

If the user types P inside the input widget, a completion suggestion for \"Portugal\" appears.

Parameters Name Type Description Default suggestions Iterable[str]

Valid suggestions sorted by decreasing priority.

required case_sensitive bool

Whether suggestions are computed in a case sensitive manner or not. The values provided in the argument suggestions represent the canonical representation of the completions and they will be suggested with that same casing.

True"},{"location":"api/suggester/#textual.suggester.SuggestFromList.get_suggestion","title":"get_suggestion async","text":"
def get_suggestion(self, value):\n

Gets a completion from the given possibilities.

Parameters Name Type Description Default value str

The current value.

required Returns Type Description str | None

A valid completion suggestion or None.

"},{"location":"api/suggester/#textual.suggester.Suggester","title":"Suggester class","text":"
def __init__(self, *, use_cache=True, case_sensitive=False):\n

Bases: ABC

Defines how widgets generate completion suggestions.

To define a custom suggester, subclass Suggester and implement the async method get_suggestion. See SuggestFromList for an example.

Parameters Name Type Description Default use_cache bool

Whether to cache suggestion results.

True case_sensitive bool

Whether suggestions are case sensitive or not. If they are not, incoming values are casefolded before generating the suggestion.

False"},{"location":"api/suggester/#textual.suggester.Suggester.cache","title":"cache instance-attribute","text":"
cache: LRUCache[str, str | None] | None = (\nLRUCache(1024) if use_cache else None\n)\n

Suggestion cache, if used.

"},{"location":"api/suggester/#textual.suggester.Suggester.get_suggestion","title":"get_suggestion async abstractmethod","text":"
def get_suggestion(self, value):\n

Try to get a completion suggestion for the given input value.

Custom suggesters should implement this method.

Note

The value argument will be casefolded if self.case_sensitive is False.

Note

If your implementation is not deterministic, you may need to disable caching.

Parameters Name Type Description Default value str

The current value of the requester widget.

required Returns Type Description str | None

A valid suggestion or None.

"},{"location":"api/suggester/#textual.suggester.SuggestionReady","title":"SuggestionReady class","text":"

Bases: Message

Sent when a completion suggestion is ready.

"},{"location":"api/suggester/#textual.suggester.SuggestionReady.suggestion","title":"suggestion instance-attribute","text":"
suggestion: str\n

The string suggestion.

"},{"location":"api/suggester/#textual.suggester.SuggestionReady.value","title":"value instance-attribute","text":"
value: str\n

The value to which the suggestion is for.

"},{"location":"api/system_commands_source/","title":"System commands source","text":"

A command palette command provider for Textual system commands.

This is a simple command provider that makes the most obvious application actions available via the command palette.

"},{"location":"api/system_commands_source/#textual._system_commands.SystemCommands","title":"SystemCommands class","text":"

Bases: Provider

A source of command palette commands that run app-wide tasks.

Used by default in App.COMMANDS.

"},{"location":"api/system_commands_source/#textual._system_commands.SystemCommands.search","title":"search async","text":"
def search(self, query):\n

Handle a request to search for system commands that match the query.

Parameters Name Type Description Default user_input

The user input to be matched.

required

Yields:

Type Description Hits

Command hits for use in the command palette.

"},{"location":"api/timer/","title":"Timer","text":"

Timer objects are created by set_interval or set_timer.

"},{"location":"api/timer/#textual.timer.TimerCallback","title":"TimerCallback module-attribute","text":"
TimerCallback = Union[\nCallable[[], Awaitable[None]], Callable[[], None]\n]\n

Type of valid callbacks to be used with timers.

"},{"location":"api/timer/#textual.timer.Timer","title":"Timer class","text":"
def __init__(\nself,\nevent_target,\ninterval,\n*,\nname=None,\ncallback=None,\nrepeat=None,\nskip=True,\npause=False\n):\n

A class to send timer-based events.

Parameters Name Type Description Default event_target MessageTarget

The object which will receive the timer events.

required interval float

The time between timer events, in seconds.

required name str | None

A name to assign the event (for debugging).

None callback TimerCallback | None

A optional callback to invoke when the event is handled.

None repeat int | None

The number of times to repeat the timer, or None to repeat forever.

None skip bool

Enable skipping of scheduled events that couldn't be sent in time.

True pause bool

Start the timer paused.

False"},{"location":"api/timer/#textual.timer.Timer.pause","title":"pause method","text":"
def pause(self):\n

Pause the timer.

A paused timer will not send events until it is resumed.

"},{"location":"api/timer/#textual.timer.Timer.reset","title":"reset method","text":"
def reset(self):\n

Reset the timer, so it starts from the beginning.

"},{"location":"api/timer/#textual.timer.Timer.resume","title":"resume method","text":"
def resume(self):\n

Resume a paused timer.

"},{"location":"api/timer/#textual.timer.Timer.stop","title":"stop method","text":"
def stop(self):\n

Stop the timer.

"},{"location":"api/types/","title":"Types","text":"

Export some objects that are used by Textual and that help document other features.

"},{"location":"api/types/#textual.types.ActionParseResult","title":"ActionParseResult module-attribute","text":"
ActionParseResult: TypeAlias = \"tuple[str, tuple[Any, ...]]\"\n

An action is its name and the arbitrary tuple of its arguments.

"},{"location":"api/types/#textual.types.CSSPathType","title":"CSSPathType module-attribute","text":"
CSSPathType: TypeAlias = Union[\nstr, PurePath, List[Union[str, PurePath]]\n]\n

Valid ways of specifying paths to CSS files.

"},{"location":"api/types/#textual.types.CallbackType","title":"CallbackType module-attribute","text":"
CallbackType = Union[\nCallable[[], Awaitable[None]], Callable[[], None]\n]\n

Type used for arbitrary callables used in callbacks.

"},{"location":"api/types/#textual.types.CursorType","title":"CursorType module-attribute","text":"
CursorType = Literal['cell', 'row', 'column', 'none']\n

The valid types of cursors for DataTable.cursor_type.

"},{"location":"api/types/#textual.types.EasingFunction","title":"EasingFunction module-attribute","text":"
EasingFunction = Callable[[float], float]\n

Signature for a function that parametrises animation speed.

An easing function must map the interval [0, 1] into the interval [0, 1].

"},{"location":"api/types/#textual.types.IgnoreReturnCallbackType","title":"IgnoreReturnCallbackType module-attribute","text":"
IgnoreReturnCallbackType = Union[\nCallable[[], Awaitable[Any]], Callable[[], Any]\n]\n

A callback which ignores the return type.

"},{"location":"api/types/#textual.types.InputValidationOn","title":"InputValidationOn module-attribute","text":"
InputValidationOn = Literal['blur', 'changed', 'submitted']\n

Possible messages that trigger input validation.

"},{"location":"api/types/#textual.types.WatchCallbackType","title":"WatchCallbackType module-attribute","text":"
WatchCallbackType = Union[\nCallable[[], Awaitable[None]],\nCallable[[Any], Awaitable[None]],\nCallable[[Any, Any], Awaitable[None]],\nCallable[[], None],\nCallable[[Any], None],\nCallable[[Any, Any], None],\n]\n

Type used for callbacks passed to the watch method of widgets.

"},{"location":"api/types/#textual.types.Animatable","title":"Animatable class","text":"

Bases: Protocol

Protocol for objects that can have their intrinsic values animated.

For example, the transition between two colors can be animated because the class Color satisfies this protocol.

"},{"location":"api/types/#textual.types.CSSPathError","title":"CSSPathError class","text":"

Bases: Exception

Raised when supplied CSS path(s) are invalid.

"},{"location":"api/types/#textual.types.MessageTarget","title":"MessageTarget class","text":"

Bases: Protocol

Protocol that must be followed by objects that can receive messages.

"},{"location":"api/types/#textual.types.NoActiveAppError","title":"NoActiveAppError class","text":"

Bases: RuntimeError

Runtime error raised if we try to retrieve the active app when there is none.

"},{"location":"api/types/#textual.types.RenderStyles","title":"RenderStyles class","text":"
def __init__(self, node, base, inline_styles):\n

Bases: StylesBase

Presents a combined view of two Styles object: a base Styles and inline Styles.

"},{"location":"api/types/#textual.css.styles.RenderStyles.base","title":"base property","text":"
base: Styles\n

Quick access to base (css) style.

"},{"location":"api/types/#textual.css.styles.RenderStyles.css","title":"css property","text":"
css: str\n

Get the CSS for the combined styles.

"},{"location":"api/types/#textual.css.styles.RenderStyles.gutter","title":"gutter property","text":"
gutter: Spacing\n

Get space around widget.

Returns Type Description Spacing

Space around widget content.

"},{"location":"api/types/#textual.css.styles.RenderStyles.inline","title":"inline property","text":"
inline: Styles\n

Quick access to the inline styles.

"},{"location":"api/types/#textual.css.styles.RenderStyles.rich_style","title":"rich_style property","text":"
rich_style: Style\n

Get a Rich style for this Styles object.

"},{"location":"api/types/#textual.css.styles.RenderStyles.animate","title":"animate method","text":"
def animate(\nself,\nattribute,\nvalue,\n*,\nfinal_value=Ellipsis,\nduration=None,\nspeed=None,\ndelay=0.0,\neasing=DEFAULT_EASING,\non_complete=None\n):\n

Animate an attribute.

Parameters Name Type Description Default attribute str

Name of the attribute to animate.

required value str | float | Animatable

The value to animate to.

required final_value object

The final value of the animation. Defaults to value if not set.

Ellipsis duration float | None

The duration of the animate.

None speed float | None

The speed of the animation.

None delay float

A delay (in seconds) before the animation starts.

0.0 easing EasingFunction | str

An easing method.

DEFAULT_EASING on_complete CallbackType | None

A callable to invoke when the animation is finished.

None"},{"location":"api/types/#textual.css.styles.RenderStyles.clear_rule","title":"clear_rule method","text":"
def clear_rule(self, rule_name):\n

Clear a rule (from inline).

"},{"location":"api/types/#textual.css.styles.RenderStyles.get_rules","title":"get_rules method","text":"
def get_rules(self):\n

Get rules as a dictionary

"},{"location":"api/types/#textual.css.styles.RenderStyles.has_rule","title":"has_rule method","text":"
def has_rule(self, rule):\n

Check if a rule has been set.

"},{"location":"api/types/#textual.css.styles.RenderStyles.merge","title":"merge method","text":"
def merge(self, other):\n

Merge values from another Styles.

Parameters Name Type Description Default other StylesBase

A Styles object.

required"},{"location":"api/types/#textual.css.styles.RenderStyles.reset","title":"reset method","text":"
def reset(self):\n

Reset the rules to initial state.

"},{"location":"api/types/#textual.types.UnusedParameter","title":"UnusedParameter class","text":"

Helper type for a parameter that isn't specified in a method call.

"},{"location":"api/validation/","title":"Validation","text":"

Framework for validating string values

"},{"location":"api/validation/#textual.validation.Failure","title":"Failure class","text":"

Information about a validation failure.

"},{"location":"api/validation/#textual.validation.Failure.description","title":"description instance-attribute class-attribute","text":"
description: str | None = None\n

An optional override for describing this failure. Takes precedence over any messages set in the Validator.

"},{"location":"api/validation/#textual.validation.Failure.validator","title":"validator instance-attribute","text":"
validator: Validator\n

The Validator which produced the failure.

"},{"location":"api/validation/#textual.validation.Failure.value","title":"value instance-attribute class-attribute","text":"
value: str | None = None\n

The value which resulted in validation failing.

"},{"location":"api/validation/#textual.validation.Function","title":"Function class","text":"
def __init__(self, function, failure_description=None):\n

Bases: Validator

A flexible validator which allows you to provide custom validation logic.

"},{"location":"api/validation/#textual.validation.Function.function","title":"function instance-attribute","text":"
function = function\n

Function which takes the value to validate and returns True if valid, and False otherwise.

"},{"location":"api/validation/#textual.validation.Function.ReturnedFalse","title":"ReturnedFalse class","text":"

Bases: Failure

Indicates validation failed because the supplied function returned False.

"},{"location":"api/validation/#textual.validation.Function.describe_failure","title":"describe_failure method","text":"
def describe_failure(self, failure):\n

Describes why the validator failed.

Parameters Name Type Description Default failure Failure

Information about why the validation failed.

required Returns Type Description str | None

A string description of the failure.

"},{"location":"api/validation/#textual.validation.Function.validate","title":"validate method","text":"
def validate(self, value):\n

Validate that the supplied function returns True.

Parameters Name Type Description Default value str

The value to pass into the supplied function.

required Returns Type Description ValidationResult

A ValidationResult indicating success if the function returned True, and failure if the function return False.

"},{"location":"api/validation/#textual.validation.Integer","title":"Integer class","text":"

Bases: Number

Validator which ensures the value is an integer which falls within a range.

"},{"location":"api/validation/#textual.validation.Integer.NotAnInteger","title":"NotAnInteger class","text":"

Bases: Failure

Indicates a failure due to the value not being a valid integer.

"},{"location":"api/validation/#textual.validation.Integer.describe_failure","title":"describe_failure method","text":"
def describe_failure(self, failure):\n

Describes why the validator failed.

Parameters Name Type Description Default failure Failure

Information about why the validation failed.

required Returns Type Description str | None

A string description of the failure.

"},{"location":"api/validation/#textual.validation.Integer.validate","title":"validate method","text":"
def validate(self, value):\n

Ensure that value is an integer, optionally within a range.

Parameters Name Type Description Default value str

The value to validate.

required Returns Type Description ValidationResult

The result of the validation.

"},{"location":"api/validation/#textual.validation.Length","title":"Length class","text":"
def __init__(\nself,\nminimum=None,\nmaximum=None,\nfailure_description=None,\n):\n

Bases: Validator

Validate that a string is within a range (inclusive).

"},{"location":"api/validation/#textual.validation.Length.maximum","title":"maximum instance-attribute","text":"
maximum = maximum\n

The inclusive maximum length of the value, or None if unbounded.

"},{"location":"api/validation/#textual.validation.Length.minimum","title":"minimum instance-attribute","text":"
minimum = minimum\n

The inclusive minimum length of the value, or None if unbounded.

"},{"location":"api/validation/#textual.validation.Length.Incorrect","title":"Incorrect class","text":"

Bases: Failure

Indicates a failure due to the length of the value being outside the range.

"},{"location":"api/validation/#textual.validation.Length.describe_failure","title":"describe_failure method","text":"
def describe_failure(self, failure):\n

Describes why the validator failed.

Parameters Name Type Description Default failure Failure

Information about why the validation failed.

required Returns Type Description str | None

A string description of the failure.

"},{"location":"api/validation/#textual.validation.Length.validate","title":"validate method","text":"
def validate(self, value):\n

Ensure that value falls within the maximum and minimum length constraints.

Parameters Name Type Description Default value str

The value to validate.

required Returns Type Description ValidationResult

The result of the validation.

"},{"location":"api/validation/#textual.validation.Number","title":"Number class","text":"
def __init__(\nself,\nminimum=None,\nmaximum=None,\nfailure_description=None,\n):\n

Bases: Validator

Validator that ensures the value is a number, with an optional range check.

"},{"location":"api/validation/#textual.validation.Number.maximum","title":"maximum instance-attribute","text":"
maximum = maximum\n

The maximum value of the number, inclusive. If None, the maximum is unbounded.

"},{"location":"api/validation/#textual.validation.Number.minimum","title":"minimum instance-attribute","text":"
minimum = minimum\n

The minimum value of the number, inclusive. If None, the minimum is unbounded.

"},{"location":"api/validation/#textual.validation.Number.NotANumber","title":"NotANumber class","text":"

Bases: Failure

Indicates a failure due to the value not being a valid number (decimal/integer, inc. scientific notation)

"},{"location":"api/validation/#textual.validation.Number.NotInRange","title":"NotInRange class","text":"

Bases: Failure

Indicates a failure due to the number not being within the range [minimum, maximum].

"},{"location":"api/validation/#textual.validation.Number.describe_failure","title":"describe_failure method","text":"
def describe_failure(self, failure):\n

Describes why the validator failed.

Parameters Name Type Description Default failure Failure

Information about why the validation failed.

required Returns Type Description str | None

A string description of the failure.

"},{"location":"api/validation/#textual.validation.Number.validate","title":"validate method","text":"
def validate(self, value):\n

Ensure that value is a valid number, optionally within a range.

Parameters Name Type Description Default value str

The value to validate.

required Returns Type Description ValidationResult

The result of the validation.

"},{"location":"api/validation/#textual.validation.Regex","title":"Regex class","text":"
def __init__(self, regex, flags=0, failure_description=None):\n

Bases: Validator

A validator that checks the value matches a regex (via re.fullmatch).

"},{"location":"api/validation/#textual.validation.Regex.flags","title":"flags instance-attribute","text":"
flags = flags\n

The flags to pass to re.fullmatch.

"},{"location":"api/validation/#textual.validation.Regex.regex","title":"regex instance-attribute","text":"
regex = regex\n

The regex which we'll validate is matched by the value.

"},{"location":"api/validation/#textual.validation.Regex.NoResults","title":"NoResults class","text":"

Bases: Failure

Indicates validation failed because the regex could not be found within the value string.

"},{"location":"api/validation/#textual.validation.Regex.describe_failure","title":"describe_failure method","text":"
def describe_failure(self, failure):\n

Describes why the validator failed.

Parameters Name Type Description Default failure Failure

Information about why the validation failed.

required Returns Type Description str | None

A string description of the failure.

"},{"location":"api/validation/#textual.validation.Regex.validate","title":"validate method","text":"
def validate(self, value):\n

Ensure that the value matches the regex.

Parameters Name Type Description Default value str

The value that should match the regex.

required Returns Type Description ValidationResult

The result of the validation.

"},{"location":"api/validation/#textual.validation.URL","title":"URL class","text":"

Bases: Validator

Validator that checks if a URL is valid (ensuring a scheme is present).

"},{"location":"api/validation/#textual.validation.URL.InvalidURL","title":"InvalidURL class","text":"

Bases: Failure

Indicates that the URL is not valid.

"},{"location":"api/validation/#textual.validation.URL.describe_failure","title":"describe_failure method","text":"
def describe_failure(self, failure):\n

Describes why the validator failed.

Parameters Name Type Description Default failure Failure

Information about why the validation failed.

required Returns Type Description str | None

A string description of the failure.

"},{"location":"api/validation/#textual.validation.URL.validate","title":"validate method","text":"
def validate(self, value):\n

Validates that value is a valid URL (contains a scheme).

Parameters Name Type Description Default value str

The value to validate.

required Returns Type Description ValidationResult

The result of the validation.

"},{"location":"api/validation/#textual.validation.ValidationResult","title":"ValidationResult class","text":"

The result of calling a Validator.validate method.

"},{"location":"api/validation/#textual.validation.ValidationResult.failure_descriptions","title":"failure_descriptions property","text":"
failure_descriptions: list[str]\n

Utility for extracting failure descriptions as strings.

Useful if you don't care about the additional metadata included in the Failure objects.

Returns Type Description list[str]

A list of the string descriptions explaining the failing validations.

"},{"location":"api/validation/#textual.validation.ValidationResult.failures","title":"failures instance-attribute class-attribute","text":"
failures: Sequence[Failure] = field(default_factory=list)\n

A list of reasons why the value was invalid. Empty if valid=True

"},{"location":"api/validation/#textual.validation.ValidationResult.is_valid","title":"is_valid property","text":"
is_valid: bool\n

True if the validation was successful.

"},{"location":"api/validation/#textual.validation.ValidationResult.failure","title":"failure staticmethod","text":"
def failure(failures):\n

Construct a failure ValidationResult.

Parameters Name Type Description Default failures Sequence[Failure]

The failures.

required Returns Type Description ValidationResult

A failure ValidationResult.

"},{"location":"api/validation/#textual.validation.ValidationResult.merge","title":"merge staticmethod","text":"
def merge(results):\n

Merge multiple ValidationResult objects into one.

Parameters Name Type Description Default results Sequence['ValidationResult']

List of ValidationResult objects to merge.

required Returns Type Description 'ValidationResult'

Merged ValidationResult object.

"},{"location":"api/validation/#textual.validation.ValidationResult.success","title":"success staticmethod","text":"
def success():\n

Construct a successful ValidationResult.

Returns Type Description ValidationResult

A successful ValidationResult.

"},{"location":"api/validation/#textual.validation.Validator","title":"Validator class","text":"
def __init__(self, failure_description=None):\n

Bases: ABC

Base class for the validation of string values.

Commonly used in conjunction with the Input widget, which accepts a list of validators via its constructor. This validation framework can also be used to validate any 'stringly-typed' values (for example raw command line input from sys.args).

To implement your own Validator, subclass this class.

Example
class Palindrome(Validator):\ndef validate(self, value: str) -> ValidationResult:\ndef is_palindrome(value: str) -> bool:\nreturn value == value[::-1]\nreturn self.success() if is_palindrome(value) else self.failure(\"Not palindrome!\")\n
"},{"location":"api/validation/#textual.validation.Validator.failure_description","title":"failure_description instance-attribute","text":"
failure_description = failure_description\n

A description of why the validation failed.

The description (intended to be user-facing) to attached to the Failure if the validation fails. This failure description is ultimately accessible at the time of validation failure via the Input.Changed or Input.Submitted event, and you can access it on your message handler (a method called, for example, on_input_changed or a method decorated with @on(Input.Changed).

"},{"location":"api/validation/#textual.validation.Validator.describe_failure","title":"describe_failure method","text":"
def describe_failure(self, failure):\n

Return a string description of the Failure.

Used to provide a more fine-grained description of the failure. A Validator could fail for multiple reasons, so this method could be used to provide a different reason for different types of failure.

Warning

This method is only called if no other description has been supplied. If you supply a description inside a call to self.failure(description=\"...\"), or pass a description into the constructor of the validator, those will take priority, and this method won't be called.

Parameters Name Type Description Default failure Failure

Information about why the validation failed.

required Returns Type Description str | None

A string description of the failure.

"},{"location":"api/validation/#textual.validation.Validator.failure","title":"failure method","text":"
def failure(self, description=None, value=None, failures=None):\n

Shorthand for signaling validation failure.

You can return failure(...) from a Validator.validate implementation to signal validation succeeded.

Parameters Name Type Description Default description str | None

The failure description that will be used. When used in conjunction with the Input widget, this is the description that will ultimately be available inside the handler for Input.Changed. If not supplied, the failure_description from the Validator will be used. If that is not supplied either, then the describe_failure method on Validator will be called.

None value str | None

The value that was considered invalid. This is optional, and only needs to be supplied if required in your Input.Changed handler.

None failures Failure | Sequence[Failure] | None

The reasons the validator failed. If not supplied, a generic Failure will be included in the ValidationResult returned from this function.

None Returns Type Description ValidationResult

A ValidationResult representing failed validation, and containing the metadata supplied to this function.

"},{"location":"api/validation/#textual.validation.Validator.success","title":"success method","text":"
def success(self):\n

Shorthand for ValidationResult(True).

You can return success() from a Validator.validate method implementation to signal that validation has succeeded.

Returns Type Description ValidationResult

A ValidationResult indicating validation succeeded.

"},{"location":"api/validation/#textual.validation.Validator.validate","title":"validate abstractmethod","text":"
def validate(self, value):\n

Validate the value and return a ValidationResult describing the outcome of the validation.

Parameters Name Type Description Default value str

The value to validate.

required Returns Type Description ValidationResult

The result of the validation.

"},{"location":"api/walk/","title":"Walk","text":"

Functions for walking the DOM.

Note

For most purposes you would be better off using query, which uses these functions internally.

"},{"location":"api/walk/#textual.walk.walk_breadth_first","title":"walk_breadth_first function","text":"
def walk_breadth_first(\nroot, filter_type=None, *, with_root=True\n):\n

Walk the tree breadth first (children first).

Note

Avoid changing the DOM (mounting, removing etc.) while iterating with this function. Consider walk_children which doesn't have this limitation.

Parameters Name Type Description Default root DOMNode

The root note (starting point).

required filter_type type[WalkType] | None

Optional DOMNode subclass to filter by, or None for no filter.

None with_root bool

Include the root in the walk.

True Returns Type Description Iterable[DOMNode] | Iterable[WalkType]

An iterable of DOMNodes, or the type specified in filter_type.

"},{"location":"api/walk/#textual.walk.walk_depth_first","title":"walk_depth_first function","text":"
def walk_depth_first(root, filter_type=None, *, with_root=True):\n

Walk the tree depth first (parents first).

Note

Avoid changing the DOM (mounting, removing etc.) while iterating with this function. Consider walk_children which doesn't have this limitation.

Parameters Name Type Description Default root DOMNode

The root note (starting point).

required filter_type type[WalkType] | None

Optional DOMNode subclass to filter by, or None for no filter.

None with_root bool

Include the root in the walk.

True Returns Type Description Iterable[DOMNode] | Iterable[WalkType]

An iterable of DOMNodes, or the type specified in filter_type.

"},{"location":"api/widget/","title":"Widget","text":"

The base class for widgets.

"},{"location":"api/widget/#textual.widget.AwaitMount","title":"AwaitMount class","text":"
def __init__(self, parent, widgets):\n

An optional awaitable returned by mount and mount_all.

Example
await self.mount(Static(\"foo\"))\n
"},{"location":"api/widget/#textual.widget.MountError","title":"MountError class","text":"

Bases: WidgetError

Error raised when there was a problem with the mount request.

"},{"location":"api/widget/#textual.widget.PseudoClasses","title":"PseudoClasses class","text":"

Bases: NamedTuple

Used for render/render_line based widgets that use caching. This structure can be used as a cache-key.

"},{"location":"api/widget/#textual.widget.PseudoClasses.enabled","title":"enabled instance-attribute","text":"
enabled: bool\n

Is 'enabled' applied?

"},{"location":"api/widget/#textual.widget.PseudoClasses.focus","title":"focus instance-attribute","text":"
focus: bool\n

Is 'focus' applied?

"},{"location":"api/widget/#textual.widget.PseudoClasses.hover","title":"hover instance-attribute","text":"
hover: bool\n

Is 'hover' applied?

"},{"location":"api/widget/#textual.widget.Widget","title":"Widget class","text":"
def __init__(\nself,\n*children,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: DOMNode

A Widget is the base class for Textual widgets.

See also static for starting point for your own widgets.

Parameters Name Type Description Default *children Widget

Child widgets.

() name str | None

The name of the widget.

None id str | None

The ID of the widget in the DOM.

None classes str | None

The CSS classes for the widget.

None disabled bool

Whether the widget is disabled or not.

False"},{"location":"api/widget/#textual.widget.Widget.BORDER_SUBTITLE","title":"BORDER_SUBTITLE class-attribute","text":"
BORDER_SUBTITLE: str = ''\n

Initial value for border_subtitle attribute.

"},{"location":"api/widget/#textual.widget.Widget.BORDER_TITLE","title":"BORDER_TITLE class-attribute","text":"
BORDER_TITLE: str = ''\n

Initial value for border_title attribute.

"},{"location":"api/widget/#textual.widget.Widget.allow_horizontal_scroll","title":"allow_horizontal_scroll property","text":"
allow_horizontal_scroll: bool\n

Check if horizontal scroll is permitted.

May be overridden if you want different logic regarding allowing scrolling.

"},{"location":"api/widget/#textual.widget.Widget.allow_vertical_scroll","title":"allow_vertical_scroll property","text":"
allow_vertical_scroll: bool\n

Check if vertical scroll is permitted.

May be overridden if you want different logic regarding allowing scrolling.

"},{"location":"api/widget/#textual.widget.Widget.auto_links","title":"auto_links instance-attribute class-attribute","text":"
auto_links: Reactive[bool] = Reactive(True)\n

Widget will highlight links automatically.

"},{"location":"api/widget/#textual.widget.Widget.border_subtitle","title":"border_subtitle instance-attribute class-attribute","text":"
border_subtitle: str | Text | None = _BorderTitle()\n

A title to show in the bottom border (if there is one).

"},{"location":"api/widget/#textual.widget.Widget.border_title","title":"border_title instance-attribute class-attribute","text":"
border_title: str | Text | None = _BorderTitle()\n

A title to show in the top border (if there is one).

"},{"location":"api/widget/#textual.widget.Widget.can_focus","title":"can_focus instance-attribute class-attribute","text":"
can_focus: bool = False\n

Widget may receive focus.

"},{"location":"api/widget/#textual.widget.Widget.can_focus_children","title":"can_focus_children instance-attribute class-attribute","text":"
can_focus_children: bool = True\n

Widget's children may receive focus.

"},{"location":"api/widget/#textual.widget.Widget.container_size","title":"container_size property","text":"
container_size: Size\n

The size of the container (parent widget).

Returns Type Description Size

Container size.

"},{"location":"api/widget/#textual.widget.Widget.container_viewport","title":"container_viewport property","text":"
container_viewport: Region\n

The viewport region (parent window).

Returns Type Description Region

The region that contains this widget.

"},{"location":"api/widget/#textual.widget.Widget.content_offset","title":"content_offset property","text":"
content_offset: Offset\n

An offset from the Widget origin where the content begins.

Returns Type Description Offset

Offset from widget's origin.

"},{"location":"api/widget/#textual.widget.Widget.content_region","title":"content_region property","text":"
content_region: Region\n

Gets an absolute region containing the content (minus padding and border).

Returns Type Description Region

Screen region that contains a widget's content.

"},{"location":"api/widget/#textual.widget.Widget.content_size","title":"content_size property","text":"
content_size: Size\n

The size of the content area.

Returns Type Description Size

Content area size.

"},{"location":"api/widget/#textual.widget.Widget.disabled","title":"disabled instance-attribute class-attribute","text":"
disabled: Reactive[bool] = disabled\n

Is the widget disabled? Disabled widgets can not be interacted with, and are typically styled to look dimmer.

"},{"location":"api/widget/#textual.widget.Widget.dock_gutter","title":"dock_gutter property","text":"
dock_gutter: Spacing\n

Space allocated to docks in the parent.

Returns Type Description Spacing

Space to be subtracted from scrollable area.

"},{"location":"api/widget/#textual.widget.Widget.expand","title":"expand instance-attribute class-attribute","text":"
expand: Reactive[bool] = Reactive(False)\n

Rich renderable may expand beyond optimal size.

"},{"location":"api/widget/#textual.widget.Widget.focusable","title":"focusable property","text":"
focusable: bool\n

Can this widget currently be focused?

"},{"location":"api/widget/#textual.widget.Widget.gutter","title":"gutter property","text":"
gutter: Spacing\n

Spacing for padding / border / scrollbars.

Returns Type Description Spacing

Additional spacing around content area.

"},{"location":"api/widget/#textual.widget.Widget.has_focus","title":"has_focus instance-attribute class-attribute","text":"
has_focus: Reactive[bool] = Reactive(False, repaint=False)\n

Does this widget have focus? Read only.

"},{"location":"api/widget/#textual.widget.Widget.highlight_link_id","title":"highlight_link_id instance-attribute class-attribute","text":"
highlight_link_id: Reactive[str] = Reactive('')\n

The currently highlighted link id. Read only.

"},{"location":"api/widget/#textual.widget.Widget.horizontal_scrollbar","title":"horizontal_scrollbar property","text":"
horizontal_scrollbar: ScrollBar\n

The a horizontal scrollbar.

Note

This will create a scrollbar if one doesn't exist.

Returns Type Description ScrollBar

ScrollBar Widget.

"},{"location":"api/widget/#textual.widget.Widget.hover_style","title":"hover_style instance-attribute class-attribute","text":"
hover_style: Reactive[Style] = Reactive(\nStyle, repaint=False\n)\n

The current hover style (style under the mouse cursor). Read only.

"},{"location":"api/widget/#textual.widget.Widget.is_container","title":"is_container property","text":"
is_container: bool\n

Is this widget a container (contains other widgets)?

"},{"location":"api/widget/#textual.widget.Widget.is_horizontal_scroll_end","title":"is_horizontal_scroll_end property","text":"
is_horizontal_scroll_end: bool\n

Is the horizontal scroll position at the maximum?

"},{"location":"api/widget/#textual.widget.Widget.is_horizontal_scrollbar_grabbed","title":"is_horizontal_scrollbar_grabbed property","text":"
is_horizontal_scrollbar_grabbed: bool\n

Is the user dragging the vertical scrollbar?

"},{"location":"api/widget/#textual.widget.Widget.is_scrollable","title":"is_scrollable property","text":"
is_scrollable: bool\n

Can this widget be scrolled?

"},{"location":"api/widget/#textual.widget.Widget.is_vertical_scroll_end","title":"is_vertical_scroll_end property","text":"
is_vertical_scroll_end: bool\n

Is the vertical scroll position at the maximum?

"},{"location":"api/widget/#textual.widget.Widget.is_vertical_scrollbar_grabbed","title":"is_vertical_scrollbar_grabbed property","text":"
is_vertical_scrollbar_grabbed: bool\n

Is the user dragging the vertical scrollbar?

"},{"location":"api/widget/#textual.widget.Widget.layer","title":"layer property","text":"
layer: str\n

Get the name of this widgets layer.

Returns Type Description str

Name of layer.

"},{"location":"api/widget/#textual.widget.Widget.layers","title":"layers property","text":"
layers: tuple[str, ...]\n

Layers of from parent.

Returns Type Description tuple[str, ...]

Tuple of layer names.

"},{"location":"api/widget/#textual.widget.Widget.link_hover_style","title":"link_hover_style property","text":"
link_hover_style: Style\n

Style of links underneath the mouse cursor.

Returns Type Description Style

Rich Style.

"},{"location":"api/widget/#textual.widget.Widget.link_style","title":"link_style property","text":"
link_style: Style\n

Style of links.

Returns Type Description Style

Rich style.

"},{"location":"api/widget/#textual.widget.Widget.max_scroll_x","title":"max_scroll_x property","text":"
max_scroll_x: int\n

The maximum value of scroll_x.

"},{"location":"api/widget/#textual.widget.Widget.max_scroll_y","title":"max_scroll_y property","text":"
max_scroll_y: int\n

The maximum value of scroll_y.

"},{"location":"api/widget/#textual.widget.Widget.mouse_over","title":"mouse_over instance-attribute class-attribute","text":"
mouse_over: Reactive[bool] = Reactive(False, repaint=False)\n

Is the mouse over this widget? Read only.

"},{"location":"api/widget/#textual.widget.Widget.offset","title":"offset writable property","text":"
offset: Offset\n

Widget offset from origin.

Returns Type Description Offset

Relative offset.

"},{"location":"api/widget/#textual.widget.Widget.opacity","title":"opacity property","text":"
opacity: float\n

Total opacity of widget.

"},{"location":"api/widget/#textual.widget.Widget.outer_size","title":"outer_size property","text":"
outer_size: Size\n

The size of the widget (including padding and border).

Returns Type Description Size

Outer size.

"},{"location":"api/widget/#textual.widget.Widget.region","title":"region property","text":"
region: Region\n

The region occupied by this widget, relative to the Screen.

Raises Type Description NoScreen

If there is no screen.

errors.NoWidget

If the widget is not on the screen.

Returns Type Description Region

Region within screen occupied by widget.

"},{"location":"api/widget/#textual.widget.Widget.scroll_offset","title":"scroll_offset property","text":"
scroll_offset: Offset\n

Get the current scroll offset.

Returns Type Description Offset

Offset a container has been scrolled by.

"},{"location":"api/widget/#textual.widget.Widget.scroll_x","title":"scroll_x instance-attribute class-attribute","text":"
scroll_x: Reactive[float] = Reactive(\n0.0, repaint=False, layout=False\n)\n

The scroll position on the X axis.

"},{"location":"api/widget/#textual.widget.Widget.scroll_y","title":"scroll_y instance-attribute class-attribute","text":"
scroll_y: Reactive[float] = Reactive(\n0.0, repaint=False, layout=False\n)\n

The scroll position on the Y axis.

"},{"location":"api/widget/#textual.widget.Widget.scrollable_content_region","title":"scrollable_content_region property","text":"
scrollable_content_region: Region\n

Gets an absolute region containing the scrollable content (minus padding, border, and scrollbars).

Returns Type Description Region

Screen region that contains a widget's content.

"},{"location":"api/widget/#textual.widget.Widget.scrollbar_corner","title":"scrollbar_corner property","text":"
scrollbar_corner: ScrollBarCorner\n

The scrollbar corner.

Note

This will create a scrollbar corner if one doesn't exist.

Returns Type Description ScrollBarCorner

ScrollBarCorner Widget.

"},{"location":"api/widget/#textual.widget.Widget.scrollbar_gutter","title":"scrollbar_gutter property","text":"
scrollbar_gutter: Spacing\n

Spacing required to fit scrollbar(s).

Returns Type Description Spacing

Scrollbar gutter spacing.

"},{"location":"api/widget/#textual.widget.Widget.scrollbar_size_horizontal","title":"scrollbar_size_horizontal property","text":"
scrollbar_size_horizontal: int\n

Get the height used by the horizontal scrollbar.

Returns Type Description int

Number of rows in the horizontal scrollbar.

"},{"location":"api/widget/#textual.widget.Widget.scrollbar_size_vertical","title":"scrollbar_size_vertical property","text":"
scrollbar_size_vertical: int\n

Get the width used by the vertical scrollbar.

Returns Type Description int

Number of columns in the vertical scrollbar.

"},{"location":"api/widget/#textual.widget.Widget.scrollbars_enabled","title":"scrollbars_enabled property","text":"
scrollbars_enabled: tuple[bool, bool]\n

A tuple of booleans that indicate if scrollbars are enabled.

Returns Type Description tuple[bool, bool]

A tuple of (, )"},{"location":"api/widget/#textual.widget.Widget.scrollbars_space","title":"scrollbars_space property","text":"

scrollbars_space: tuple[int, int]\n

The number of cells occupied by scrollbars for width and height

"},{"location":"api/widget/#textual.widget.Widget.show_horizontal_scrollbar","title":"show_horizontal_scrollbar instance-attribute class-attribute","text":"
show_horizontal_scrollbar: Reactive[bool] = Reactive(\nFalse, layout=True\n)\n

Show a horizontal scrollbar?

"},{"location":"api/widget/#textual.widget.Widget.show_vertical_scrollbar","title":"show_vertical_scrollbar instance-attribute class-attribute","text":"
show_vertical_scrollbar: Reactive[bool] = Reactive(\nFalse, layout=True\n)\n

Show a horizontal scrollbar?

"},{"location":"api/widget/#textual.widget.Widget.shrink","title":"shrink instance-attribute class-attribute","text":"
shrink: Reactive[bool] = Reactive(True)\n

Rich renderable may shrink below optimal size.

"},{"location":"api/widget/#textual.widget.Widget.siblings","title":"siblings property","text":"
siblings: list[Widget]\n

Get the widget's siblings (self is removed from the return list).

Returns Type Description list[Widget]

A list of siblings.

"},{"location":"api/widget/#textual.widget.Widget.size","title":"size property","text":"
size: Size\n

The size of the content area.

Returns Type Description Size

Content area size.

"},{"location":"api/widget/#textual.widget.Widget.tooltip","title":"tooltip writable property","text":"
tooltip: RenderableType | None\n

Tooltip for the widget, or None for no tooltip.

"},{"location":"api/widget/#textual.widget.Widget.vertical_scrollbar","title":"vertical_scrollbar property","text":"
vertical_scrollbar: ScrollBar\n

The vertical scrollbar (create if necessary).

Note

This will create a scrollbar if one doesn't exist.

Returns Type Description ScrollBar

ScrollBar Widget.

"},{"location":"api/widget/#textual.widget.Widget.virtual_region","title":"virtual_region property","text":"
virtual_region: Region\n

The widget region relative to it's container (which may not be visible, depending on scroll offset).

Returns Type Description Region

The virtual region.

"},{"location":"api/widget/#textual.widget.Widget.virtual_region_with_margin","title":"virtual_region_with_margin property","text":"
virtual_region_with_margin: Region\n

The widget region relative to its container (including margin), which may not be visible, depending on the scroll offset.

Returns Type Description Region

The virtual region of the Widget, inclusive of its margin.

"},{"location":"api/widget/#textual.widget.Widget.virtual_size","title":"virtual_size instance-attribute class-attribute","text":"
virtual_size: Reactive[Size] = Reactive(\nSize(0, 0), layout=True\n)\n

The virtual (scrollable) size of the widget.

"},{"location":"api/widget/#textual.widget.Widget.visible_siblings","title":"visible_siblings property","text":"
visible_siblings: list[Widget]\n

A list of siblings which will be shown.

Returns Type Description list[Widget]

List of siblings.

"},{"location":"api/widget/#textual.widget.Widget.window_region","title":"window_region property","text":"
window_region: Region\n

The region within the scrollable area that is currently visible.

Returns Type Description Region

New region.

"},{"location":"api/widget/#textual.widget.Widget.animate","title":"animate method","text":"
def animate(\nself,\nattribute,\nvalue,\n*,\nfinal_value=Ellipsis,\nduration=None,\nspeed=None,\ndelay=0.0,\neasing=DEFAULT_EASING,\non_complete=None\n):\n

Animate an attribute.

Parameters Name Type Description Default attribute str

Name of the attribute to animate.

required value float | Animatable

The value to animate to.

required final_value object

The final value of the animation. Defaults to value if not set.

Ellipsis duration float | None

The duration of the animate.

None speed float | None

The speed of the animation.

None delay float

A delay (in seconds) before the animation starts.

0.0 easing EasingFunction | str

An easing method.

DEFAULT_EASING on_complete CallbackType | None

A callable to invoke when the animation is finished.

None"},{"location":"api/widget/#textual.widget.Widget.begin_capture_print","title":"begin_capture_print method","text":"
def begin_capture_print(self, stdout=True, stderr=True):\n

Capture text from print statements (or writes to stdout / stderr).

If printing is captured, the widget will be sent an events.Print message.

Call end_capture_print to disable print capture.

Parameters Name Type Description Default stdout bool

Capture stdout.

True stderr bool

Capture stderr.

True"},{"location":"api/widget/#textual.widget.Widget.blur","title":"blur method","text":"
def blur(self):\n

Blur (un-focus) the widget.

Focus will be moved to the next available widget in the focus chain..

Returns Type Description Self

The Widget instance.

"},{"location":"api/widget/#textual.widget.Widget.can_view","title":"can_view method","text":"
def can_view(self, widget):\n

Check if a given widget is in the current view (scrollable area).

Note: This doesn't necessarily equate to a widget being visible. There are other reasons why a widget may not be visible.

Parameters Name Type Description Default widget Widget

A widget that is a descendant of self.

required Returns Type Description bool

True if the entire widget is in view, False if it is partially visible or not in view.

"},{"location":"api/widget/#textual.widget.Widget.capture_mouse","title":"capture_mouse method","text":"
def capture_mouse(self, capture=True):\n

Capture (or release) the mouse.

When captured, mouse events will go to this widget even when the pointer is not directly over the widget.

Parameters Name Type Description Default capture bool

True to capture or False to release.

True"},{"location":"api/widget/#textual.widget.Widget.check_message_enabled","title":"check_message_enabled method","text":"
def check_message_enabled(self, message):\n

Check if a given message is enabled (allowed to be sent).

Parameters Name Type Description Default message Message

A message object

required Returns Type Description bool

True if the message will be sent, or False if it is disabled.

"},{"location":"api/widget/#textual.widget.Widget.compose","title":"compose method","text":"
def compose(self):\n

Called by Textual to create child widgets.

Extend this to build a UI.

Example
def compose(self) -> ComposeResult:\nyield Header()\nyield Container(\nTree(), Viewer()\n)\nyield Footer()\n
"},{"location":"api/widget/#textual.widget.Widget.end_capture_print","title":"end_capture_print method","text":"
def end_capture_print(self):\n

End print capture (set with capture_print).

"},{"location":"api/widget/#textual.widget.Widget.focus","title":"focus method","text":"
def focus(self, scroll_visible=True):\n

Give focus to this widget.

Parameters Name Type Description Default scroll_visible bool

Scroll parent to make this widget visible.

True Returns Type Description Self

The Widget instance.

"},{"location":"api/widget/#textual.widget.Widget.get_child_by_id","title":"get_child_by_id method","text":"
def get_child_by_id(self, id, expect_type=None):\n

Return the first child (immediate descendent) of this node with the given ID.

Parameters Name Type Description Default id str

The ID of the child.

required expect_type type[ExpectType] | None

Require the object be of the supplied type, or None for any type.

None Returns Type Description ExpectType | Widget

The first child of this node with the ID.

Raises Type Description NoMatches

if no children could be found for this ID

WrongType

if the wrong type was found.

"},{"location":"api/widget/#textual.widget.Widget.get_child_by_type","title":"get_child_by_type method","text":"
def get_child_by_type(self, expect_type):\n

Get a child of a give type.

Parameters Name Type Description Default expect_type type[ExpectType]

The type of the expected child.

required Raises Type Description NoMatches

If no valid child is found.

Returns Type Description ExpectType

A widget.

"},{"location":"api/widget/#textual.widget.Widget.get_component_rich_style","title":"get_component_rich_style method","text":"
def get_component_rich_style(self, name, *, partial=False):\n

Get a Rich style for a component.

Parameters Name Type Description Default name str

Name of component.

required partial bool

Return a partial style (not combined with parent).

False Returns Type Description Style

A Rich style object.

"},{"location":"api/widget/#textual.widget.Widget.get_content_height","title":"get_content_height method","text":"
def get_content_height(self, container, viewport, width):\n

Called by Textual to get the height of the content area. May be overridden in a subclass.

Parameters Name Type Description Default container Size

Size of the container (immediate parent) widget.

required viewport Size

Size of the viewport.

required width int

Width of renderable.

required Returns Type Description int

The height of the content.

"},{"location":"api/widget/#textual.widget.Widget.get_content_width","title":"get_content_width method","text":"
def get_content_width(self, container, viewport):\n

Called by textual to get the width of the content area. May be overridden in a subclass.

Parameters Name Type Description Default container Size

Size of the container (immediate parent) widget.

required viewport Size

Size of the viewport.

required Returns Type Description int

The optimal width of the content.

"},{"location":"api/widget/#textual.widget.Widget.get_pseudo_class_state","title":"get_pseudo_class_state method","text":"
def get_pseudo_class_state(self):\n

Get an object describing whether each pseudo class is present on this object or not.

Returns Type Description PseudoClasses

A PseudoClasses object describing the pseudo classes that are present.

"},{"location":"api/widget/#textual.widget.Widget.get_pseudo_classes","title":"get_pseudo_classes method","text":"
def get_pseudo_classes(self):\n

Pseudo classes for a widget.

Returns Type Description Iterable[str]

Names of the pseudo classes.

"},{"location":"api/widget/#textual.widget.Widget.get_style_at","title":"get_style_at method","text":"
def get_style_at(self, x, y):\n

Get the Rich style in a widget at a given relative offset.

Parameters Name Type Description Default x int

X coordinate relative to the widget.

required y int

Y coordinate relative to the widget.

required Returns Type Description Style

A rich Style object.

"},{"location":"api/widget/#textual.widget.Widget.get_widget_by_id","title":"get_widget_by_id method","text":"
def get_widget_by_id(self, id, expect_type=None):\n

Return the first descendant widget with the given ID.

Performs a depth-first search rooted at this widget.

Parameters Name Type Description Default id str

The ID to search for in the subtree.

required expect_type type[ExpectType] | None

Require the object be of the supplied type, or None for any type.

None Returns Type Description ExpectType | Widget

The first descendant encountered with this ID.

Raises Type Description NoMatches

if no children could be found for this ID.

WrongType

if the wrong type was found.

"},{"location":"api/widget/#textual.widget.Widget.mount","title":"mount method","text":"
def mount(self, *widgets, before=None, after=None):\n

Mount widgets below this widget (making this widget a container).

Parameters Name Type Description Default *widgets Widget

The widget(s) to mount.

() before int | str | Widget | None

Optional location to mount before. An int is the index of the child to mount before, a str is a query_one query to find the widget to mount before.

None after int | str | Widget | None

Optional location to mount after. An int is the index of the child to mount after, a str is a query_one query to find the widget to mount after.

None Returns Type Description AwaitMount

An awaitable object that waits for widgets to be mounted.

Raises Type Description MountError

If there is a problem with the mount request.

Note

Only one of before or after can be provided. If both are provided a MountError will be raised.

"},{"location":"api/widget/#textual.widget.Widget.mount_all","title":"mount_all method","text":"
def mount_all(self, widgets, *, before=None, after=None):\n

Mount widgets from an iterable.

Parameters Name Type Description Default widgets Iterable[Widget]

An iterable of widgets.

required before int | str | Widget | None

Optional location to mount before. An int is the index of the child to mount before, a str is a query_one query to find the widget to mount before.

None after int | str | Widget | None

Optional location to mount after. An int is the index of the child to mount after, a str is a query_one query to find the widget to mount after.

None Returns Type Description AwaitMount

An awaitable object that waits for widgets to be mounted.

Raises Type Description MountError

If there is a problem with the mount request.

Note

Only one of before or after can be provided. If both are provided a MountError will be raised.

"},{"location":"api/widget/#textual.widget.Widget.move_child","title":"move_child method","text":"
def move_child(self, child, before=None, after=None):\n

Move a child widget within its parent's list of children.

Parameters Name Type Description Default child int | Widget

The child widget to move.

required before int | Widget | None

Child widget or location index to move before.

None after int | Widget | None

Child widget or location index to move after.

None Raises Type Description WidgetError

If there is a problem with the child or target.

Note

Only one of before or after can be provided. If neither or both are provided a WidgetError will be raised.

"},{"location":"api/widget/#textual.widget.Widget.notify","title":"notify method","text":"
def notify(\nself,\nmessage,\n*,\ntitle=\"\",\nseverity=\"information\",\ntimeout=Notification.timeout\n):\n

Create a notification.

Tip

This method is thread-safe.

Parameters Name Type Description Default message str

The message for the notification.

required title str

The title for the notification.

'' severity SeverityLevel

The severity of the notification.

'information' timeout float

The timeout for the notification.

Notification.timeout

See App.notify for the full documentation for this method.

"},{"location":"api/widget/#textual.widget.Widget.post_message","title":"post_message method","text":"
def post_message(self, message):\n

Post a message to this widget.

Parameters Name Type Description Default message Message

Message to post.

required Returns Type Description bool

True if the message was posted, False if this widget was closed / closing.

"},{"location":"api/widget/#textual.widget.Widget.post_render","title":"post_render method","text":"
def post_render(self, renderable):\n

Applies style attributes to the default renderable.

Returns Type Description ConsoleRenderable

A new renderable.

"},{"location":"api/widget/#textual.widget.Widget.refresh","title":"refresh method","text":"
def refresh(self, *regions, repaint=True, layout=False):\n

Initiate a refresh of the widget.

This method sets an internal flag to perform a refresh, which will be done on the next idle event. Only one refresh will be done even if this method is called multiple times.

By default this method will cause the content of the widget to refresh, but not change its size. You can also set layout=True to perform a layout.

Warning

It is rarely necessary to call this method explicitly. Updating styles or reactive attributes will do this automatically.

Parameters Name Type Description Default *regions Region

Additional screen regions to mark as dirty.

() repaint bool

Repaint the widget (will call render() again).

True layout bool

Also layout widgets in the view.

False Returns Type Description Self

The Widget instance.

"},{"location":"api/widget/#textual.widget.Widget.release_mouse","title":"release_mouse method","text":"
def release_mouse(self):\n

Release the mouse.

Mouse events will only be sent when the mouse is over the widget.

"},{"location":"api/widget/#textual.widget.Widget.remove","title":"remove method","text":"
def remove(self):\n

Remove the Widget from the DOM (effectively deleting it).

Returns Type Description AwaitRemove

An awaitable object that waits for the widget to be removed.

"},{"location":"api/widget/#textual.widget.Widget.remove_children","title":"remove_children method","text":"
def remove_children(self):\n

Remove all children of this Widget from the DOM.

Returns Type Description AwaitRemove

An awaitable object that waits for the children to be removed.

"},{"location":"api/widget/#textual.widget.Widget.render","title":"render method","text":"
def render(self):\n

Get renderable for widget.

Returns Type Description RenderableType

Any renderable.

"},{"location":"api/widget/#textual.widget.Widget.render_line","title":"render_line method","text":"
def render_line(self, y):\n

Render a line of content.

Parameters Name Type Description Default y int

Y Coordinate of line.

required Returns Type Description Strip

A rendered line.

"},{"location":"api/widget/#textual.widget.Widget.render_lines","title":"render_lines method","text":"
def render_lines(self, crop):\n

Render the widget in to lines.

Parameters Name Type Description Default crop Region

Region within visible area to render.

required Returns Type Description list[Strip]

A list of list of segments.

"},{"location":"api/widget/#textual.widget.Widget.render_str","title":"render_str method","text":"
def render_str(self, text_content):\n

Convert str in to a Text object.

If you pass in an existing Text object it will be returned unaltered.

Parameters Name Type Description Default text_content str | Text

Text or str.

required Returns Type Description Text

A text object.

"},{"location":"api/widget/#textual.widget.Widget.run_action","title":"run_action async","text":"
def run_action(self, action):\n

Perform a given action, with this widget as the default namespace.

Parameters Name Type Description Default action str

Action encoded as a string.

required"},{"location":"api/widget/#textual.widget.Widget.scroll_down","title":"scroll_down method","text":"
def scroll_down(\nself,\n*,\nanimate=True,\nspeed=None,\nduration=None,\neasing=None,\nforce=False,\non_complete=None\n):\n

Scroll one line down.

Parameters Name Type Description Default animate bool

Animate scroll.

True speed float | None

Speed of scroll if animate is True; or None to use duration.

None duration float | None

Duration of animation, if animate is True and speed is None.

None easing EasingFunction | str | None

An easing method for the scrolling animation.

None force bool

Force scrolling even when prohibited by overflow styling.

False on_complete CallbackType | None

A callable to invoke when the animation is finished.

None"},{"location":"api/widget/#textual.widget.Widget.scroll_end","title":"scroll_end method","text":"
def scroll_end(\nself,\n*,\nanimate=True,\nspeed=None,\nduration=None,\neasing=None,\nforce=False,\non_complete=None\n):\n

Scroll to the end of the container.

Parameters Name Type Description Default animate bool

Animate scroll.

True speed float | None

Speed of scroll if animate is True; or None to use duration.

None duration float | None

Duration of animation, if animate is True and speed is None.

None easing EasingFunction | str | None

An easing method for the scrolling animation.

None force bool

Force scrolling even when prohibited by overflow styling.

False on_complete CallbackType | None

A callable to invoke when the animation is finished.

None"},{"location":"api/widget/#textual.widget.Widget.scroll_home","title":"scroll_home method","text":"
def scroll_home(\nself,\n*,\nanimate=True,\nspeed=None,\nduration=None,\neasing=None,\nforce=False,\non_complete=None\n):\n

Scroll to home position.

Parameters Name Type Description Default animate bool

Animate scroll.

True speed float | None

Speed of scroll if animate is True; or None to use duration.

None duration float | None

Duration of animation, if animate is True and speed is None.

None easing EasingFunction | str | None

An easing method for the scrolling animation.

None force bool

Force scrolling even when prohibited by overflow styling.

False on_complete CallbackType | None

A callable to invoke when the animation is finished.

None"},{"location":"api/widget/#textual.widget.Widget.scroll_left","title":"scroll_left method","text":"
def scroll_left(\nself,\n*,\nanimate=True,\nspeed=None,\nduration=None,\neasing=None,\nforce=False,\non_complete=None\n):\n

Scroll one cell left.

Parameters Name Type Description Default animate bool

Animate scroll.

True speed float | None

Speed of scroll if animate is True; or None to use duration.

None duration float | None

Duration of animation, if animate is True and speed is None.

None easing EasingFunction | str | None

An easing method for the scrolling animation.

None force bool

Force scrolling even when prohibited by overflow styling.

False on_complete CallbackType | None

A callable to invoke when the animation is finished.

None"},{"location":"api/widget/#textual.widget.Widget.scroll_page_down","title":"scroll_page_down method","text":"
def scroll_page_down(\nself,\n*,\nanimate=True,\nspeed=None,\nduration=None,\neasing=None,\nforce=False,\non_complete=None\n):\n

Scroll one page down.

Parameters Name Type Description Default animate bool

Animate scroll.

True speed float | None

Speed of scroll if animate is True; or None to use duration.

None duration float | None

Duration of animation, if animate is True and speed is None.

None easing EasingFunction | str | None

An easing method for the scrolling animation.

None force bool

Force scrolling even when prohibited by overflow styling.

False on_complete CallbackType | None

A callable to invoke when the animation is finished.

None"},{"location":"api/widget/#textual.widget.Widget.scroll_page_left","title":"scroll_page_left method","text":"
def scroll_page_left(\nself,\n*,\nanimate=True,\nspeed=None,\nduration=None,\neasing=None,\nforce=False,\non_complete=None\n):\n

Scroll one page left.

Parameters Name Type Description Default animate bool

Animate scroll.

True speed float | None

Speed of scroll if animate is True; or None to use duration.

None duration float | None

Duration of animation, if animate is True and speed is None.

None easing EasingFunction | str | None

An easing method for the scrolling animation.

None force bool

Force scrolling even when prohibited by overflow styling.

False on_complete CallbackType | None

A callable to invoke when the animation is finished.

None"},{"location":"api/widget/#textual.widget.Widget.scroll_page_right","title":"scroll_page_right method","text":"
def scroll_page_right(\nself,\n*,\nanimate=True,\nspeed=None,\nduration=None,\neasing=None,\nforce=False,\non_complete=None\n):\n

Scroll one page right.

Parameters Name Type Description Default animate bool

Animate scroll.

True speed float | None

Speed of scroll if animate is True; or None to use duration.

None duration float | None

Duration of animation, if animate is True and speed is None.

None easing EasingFunction | str | None

An easing method for the scrolling animation.

None force bool

Force scrolling even when prohibited by overflow styling.

False on_complete CallbackType | None

A callable to invoke when the animation is finished.

None"},{"location":"api/widget/#textual.widget.Widget.scroll_page_up","title":"scroll_page_up method","text":"
def scroll_page_up(\nself,\n*,\nanimate=True,\nspeed=None,\nduration=None,\neasing=None,\nforce=False,\non_complete=None\n):\n

Scroll one page up.

Parameters Name Type Description Default animate bool

Animate scroll.

True speed float | None

Speed of scroll if animate is True; or None to use duration.

None duration float | None

Duration of animation, if animate is True and speed is None.

None easing EasingFunction | str | None

An easing method for the scrolling animation.

None force bool

Force scrolling even when prohibited by overflow styling.

False on_complete CallbackType | None

A callable to invoke when the animation is finished.

None"},{"location":"api/widget/#textual.widget.Widget.scroll_relative","title":"scroll_relative method","text":"
def scroll_relative(\nself,\nx=None,\ny=None,\n*,\nanimate=True,\nspeed=None,\nduration=None,\neasing=None,\nforce=False,\non_complete=None\n):\n

Scroll relative to current position.

Parameters Name Type Description Default x float | None

X distance (columns) to scroll, or None for no change.

None y float | None

Y distance (rows) to scroll, or None for no change.

None animate bool

Animate to new scroll position.

True speed float | None

Speed of scroll if animate is True. Or None to use duration.

None duration float | None

Duration of animation, if animate is True and speed is None.

None easing EasingFunction | str | None

An easing method for the scrolling animation.

None force bool

Force scrolling even when prohibited by overflow styling.

False on_complete CallbackType | None

A callable to invoke when the animation is finished.

None"},{"location":"api/widget/#textual.widget.Widget.scroll_right","title":"scroll_right method","text":"
def scroll_right(\nself,\n*,\nanimate=True,\nspeed=None,\nduration=None,\neasing=None,\nforce=False,\non_complete=None\n):\n

Scroll one cell right.

Parameters Name Type Description Default animate bool

Animate scroll.

True speed float | None

Speed of scroll if animate is True; or None to use duration.

None duration float | None

Duration of animation, if animate is True and speed is None.

None easing EasingFunction | str | None

An easing method for the scrolling animation.

None force bool

Force scrolling even when prohibited by overflow styling.

False on_complete CallbackType | None

A callable to invoke when the animation is finished.

None"},{"location":"api/widget/#textual.widget.Widget.scroll_to","title":"scroll_to method","text":"
def scroll_to(\nself,\nx=None,\ny=None,\n*,\nanimate=True,\nspeed=None,\nduration=None,\neasing=None,\nforce=False,\non_complete=None\n):\n

Scroll to a given (absolute) coordinate, optionally animating.

Parameters Name Type Description Default x float | None

X coordinate (column) to scroll to, or None for no change.

None y float | None

Y coordinate (row) to scroll to, or None for no change.

None animate bool

Animate to new scroll position.

True speed float | None

Speed of scroll if animate is True; or None to use duration.

None duration float | None

Duration of animation, if animate is True and speed is None.

None easing EasingFunction | str | None

An easing method for the scrolling animation.

None force bool

Force scrolling even when prohibited by overflow styling.

False on_complete CallbackType | None

A callable to invoke when the animation is finished.

None Note

The call to scroll is made after the next refresh.

"},{"location":"api/widget/#textual.widget.Widget.scroll_to_center","title":"scroll_to_center method","text":"
def scroll_to_center(\nself,\nwidget,\nanimate=True,\n*,\nspeed=None,\nduration=None,\neasing=None,\nforce=False,\norigin_visible=True,\non_complete=None\n):\n

Scroll this widget to the center of self.

The center of the widget will be scrolled to the center of the container.

Parameters Name Type Description Default widget Widget

The widget to scroll to the center of self.

required animate bool

Whether to animate the scroll.

True speed float | None

Speed of scroll if animate is True; or None to use duration.

None duration float | None

Duration of animation, if animate is True and speed is None.

None easing EasingFunction | str | None

An easing method for the scrolling animation.

None force bool

Force scrolling even when prohibited by overflow styling.

False origin_visible bool

Ensure that the top left corner of the widget remains visible after the scroll.

True on_complete CallbackType | None

A callable to invoke when the animation is finished.

None"},{"location":"api/widget/#textual.widget.Widget.scroll_to_region","title":"scroll_to_region method","text":"
def scroll_to_region(\nself,\nregion,\n*,\nspacing=None,\nanimate=True,\nspeed=None,\nduration=None,\neasing=None,\ncenter=False,\ntop=False,\norigin_visible=True,\nforce=False,\non_complete=None\n):\n

Scrolls a given region in to view, if required.

This method will scroll the least distance required to move region fully within the scrollable area.

Parameters Name Type Description Default region Region

A region that should be visible.

required spacing Spacing | None

Optional spacing around the region.

None animate bool

True to animate, or False to jump.

True speed float | None

Speed of scroll if animate is True; or None to use duration.

None duration float | None

Duration of animation, if animate is True and speed is None.

None easing EasingFunction | str | None

An easing method for the scrolling animation.

None top bool

Scroll region to top of container.

False origin_visible bool

Ensure that the top left of the widget is within the window.

True force bool

Force scrolling even when prohibited by overflow styling.

False on_complete CallbackType | None

A callable to invoke when the animation is finished.

None Returns Type Description Offset

The distance that was scrolled.

"},{"location":"api/widget/#textual.widget.Widget.scroll_to_widget","title":"scroll_to_widget method","text":"
def scroll_to_widget(\nself,\nwidget,\n*,\nanimate=True,\nspeed=None,\nduration=None,\neasing=None,\ncenter=False,\ntop=False,\norigin_visible=True,\nforce=False,\non_complete=None\n):\n

Scroll scrolling to bring a widget in to view.

Parameters Name Type Description Default widget Widget

A descendant widget.

required animate bool

True to animate, or False to jump.

True speed float | None

Speed of scroll if animate is True; or None to use duration.

None duration float | None

Duration of animation, if animate is True and speed is None.

None easing EasingFunction | str | None

An easing method for the scrolling animation.

None top bool

Scroll widget to top of container.

False origin_visible bool

Ensure that the top left of the widget is within the window.

True force bool

Force scrolling even when prohibited by overflow styling.

False on_complete CallbackType | None

A callable to invoke when the animation is finished.

None Returns Type Description bool

True if any scrolling has occurred in any descendant, otherwise False.

"},{"location":"api/widget/#textual.widget.Widget.scroll_up","title":"scroll_up method","text":"
def scroll_up(\nself,\n*,\nanimate=True,\nspeed=None,\nduration=None,\neasing=None,\nforce=False,\non_complete=None\n):\n

Scroll one line up.

Parameters Name Type Description Default animate bool

Animate scroll.

True speed float | None

Speed of scroll if animate is True; or None to use duration.

None duration float | None

Duration of animation, if animate is True and speed is None.

None easing EasingFunction | str | None

An easing method for the scrolling animation.

None force bool

Force scrolling even when prohibited by overflow styling.

False on_complete CallbackType | None

A callable to invoke when the animation is finished.

None"},{"location":"api/widget/#textual.widget.Widget.scroll_visible","title":"scroll_visible method","text":"
def scroll_visible(\nself,\nanimate=True,\n*,\nspeed=None,\nduration=None,\ntop=False,\neasing=None,\nforce=False,\non_complete=None\n):\n

Scroll the container to make this widget visible.

Parameters Name Type Description Default animate bool

Animate scroll.

True speed float | None

Speed of scroll if animate is True; or None to use duration.

None duration float | None

Duration of animation, if animate is True and speed is None.

None top bool

Scroll to top of container.

False easing EasingFunction | str | None

An easing method for the scrolling animation.

None force bool

Force scrolling even when prohibited by overflow styling.

False on_complete CallbackType | None

A callable to invoke when the animation is finished.

None"},{"location":"api/widget/#textual.widget.Widget.stop_animation","title":"stop_animation async","text":"
def stop_animation(self, attribute, complete=True):\n

Stop an animation on an attribute.

Parameters Name Type Description Default attribute str

Name of the attribute whose animation should be stopped.

required complete bool

Should the animation be set to its final value?

True Note

If there is no animation scheduled or running, this is a no-op.

"},{"location":"api/widget/#textual.widget.Widget.watch_disabled","title":"watch_disabled method","text":"
def watch_disabled(self):\n

Update the styles of the widget and its children when disabled is toggled.

"},{"location":"api/widget/#textual.widget.Widget.watch_has_focus","title":"watch_has_focus method","text":"
def watch_has_focus(self, value):\n

Update from CSS if has focus state changes.

"},{"location":"api/widget/#textual.widget.Widget.watch_mouse_over","title":"watch_mouse_over method","text":"
def watch_mouse_over(self, value):\n

Update from CSS if mouse over state changes.

"},{"location":"api/widget/#textual.widget.WidgetError","title":"WidgetError class","text":"

Bases: Exception

Base widget error.

"},{"location":"api/work/","title":"Work","text":"

A decorator used to create workers.

Parameters Name Type Description Default method Callable[FactoryParamSpec, ReturnType] | Callable[FactoryParamSpec, Coroutine[None, None, ReturnType]] | None

A function or coroutine.

None name str

A short string to identify the worker (in logs and debugging).

'' group str

A short string to identify a group of workers.

'default' exit_on_error bool

Exit the app if the worker raises an error. Set to False to suppress exceptions.

True exclusive bool

Cancel all workers in the same group.

False description str | None

Readable description of the worker for debugging purposes. By default, it uses a string representation of the decorated method and its arguments.

None thread bool

Mark the method as a thread worker.

False"},{"location":"api/worker/","title":"Worker","text":"

A class to manage concurrent work.

"},{"location":"api/worker/#textual.worker.WorkType","title":"WorkType module-attribute","text":"
WorkType: TypeAlias = Union[\nCallable[[], Coroutine[None, None, ResultType]],\nCallable[[], ResultType],\nAwaitable[ResultType],\n]\n

Type used for workers.

"},{"location":"api/worker/#textual.worker.active_worker","title":"active_worker module-attribute","text":"
active_worker: ContextVar[Worker] = ContextVar(\n\"active_worker\"\n)\n

Currently active worker context var.

"},{"location":"api/worker/#textual.worker.DeadlockError","title":"DeadlockError class","text":"

Bases: WorkerError

The operation would result in a deadlock.

"},{"location":"api/worker/#textual.worker.NoActiveWorker","title":"NoActiveWorker class","text":"

Bases: Exception

There is no active worker.

"},{"location":"api/worker/#textual.worker.Worker","title":"Worker class","text":"
def __init__(\nself,\nnode,\nwork=None,\n*,\nname=\"\",\ngroup=\"default\",\ndescription=\"\",\nexit_on_error=True,\nthread=False\n):\n

Bases: Generic[ResultType]

A class to manage concurrent work (either a task or a thread).

Parameters Name Type Description Default node DOMNode

The widget, screen, or App that initiated the work.

required work WorkType | None

A callable, coroutine, or other awaitable object to run in the worker.

None name str

Name of the worker (short string to help identify when debugging).

'' group str

The worker group.

'default' description str

Description of the worker (longer string with more details).

'' exit_on_error bool

Exit the app if the worker raises an error. Set to False to suppress exceptions.

True thread bool

Mark the worker as a thread worker.

False"},{"location":"api/worker/#textual.worker.Worker.completed_steps","title":"completed_steps property","text":"
completed_steps: int\n

The number of completed steps.

"},{"location":"api/worker/#textual.worker.Worker.error","title":"error property","text":"
error: BaseException | None\n

The exception raised by the worker, or None if there was no error.

"},{"location":"api/worker/#textual.worker.Worker.is_cancelled","title":"is_cancelled property","text":"
is_cancelled: bool\n

Has the work been cancelled?

Note that cancelled work may still be running.

"},{"location":"api/worker/#textual.worker.Worker.is_finished","title":"is_finished property","text":"
is_finished: bool\n

Has the task finished (cancelled, error, or success)?

"},{"location":"api/worker/#textual.worker.Worker.is_running","title":"is_running property","text":"
is_running: bool\n

Is the task running?

"},{"location":"api/worker/#textual.worker.Worker.node","title":"node property","text":"
node: DOMNode\n

The node where this worker was run from.

"},{"location":"api/worker/#textual.worker.Worker.progress","title":"progress property","text":"
progress: float\n

Progress as a percentage.

If the total steps is None, then this will return 0. The percentage will be clamped between 0 and 100.

"},{"location":"api/worker/#textual.worker.Worker.result","title":"result property","text":"
result: ResultType | None\n

The result of the worker, or None if there is no result.

"},{"location":"api/worker/#textual.worker.Worker.state","title":"state writable property","text":"
state: WorkerState\n

The current state of the worker.

"},{"location":"api/worker/#textual.worker.Worker.total_steps","title":"total_steps property","text":"
total_steps: int | None\n

The number of total steps, or None if indeterminate.

"},{"location":"api/worker/#textual.worker.Worker.StateChanged","title":"StateChanged class","text":"
def __init__(self, worker, state):\n

Bases: Message

The worker state changed.

Parameters Name Type Description Default worker Worker

The worker object.

required state WorkerState

New state.

required"},{"location":"api/worker/#textual.worker.Worker.advance","title":"advance method","text":"
def advance(self, steps=1):\n

Advance the number of completed steps.

Parameters Name Type Description Default steps int

Number of steps to advance.

1"},{"location":"api/worker/#textual.worker.Worker.cancel","title":"cancel method","text":"
def cancel(self):\n

Cancel the task.

"},{"location":"api/worker/#textual.worker.Worker.run","title":"run async","text":"
def run(self):\n

Run the work.

Implement this method in a subclass, or pass a callable to the constructor.

Returns Type Description ResultType

Return value of the work.

"},{"location":"api/worker/#textual.worker.Worker.update","title":"update method","text":"
def update(self, completed_steps=None, total_steps=-1):\n

Update the number of completed steps.

Parameters Name Type Description Default completed_steps int | None

The number of completed seps, or None to not change.

None total_steps int | None

The total number of steps, None for indeterminate, or -1 to leave unchanged.

-1"},{"location":"api/worker/#textual.worker.Worker.wait","title":"wait async","text":"
def wait(self):\n

Wait for the work to complete.

Raises Type Description WorkerFailed

If the Worker raised an exception.

WorkerCancelled

If the Worker was cancelled before it completed.

Returns Type Description ResultType

The return value of the work.

"},{"location":"api/worker/#textual.worker.WorkerCancelled","title":"WorkerCancelled class","text":"

Bases: WorkerError

The worker was cancelled and did not complete.

"},{"location":"api/worker/#textual.worker.WorkerError","title":"WorkerError class","text":"

Bases: Exception

A worker related error.

"},{"location":"api/worker/#textual.worker.WorkerFailed","title":"WorkerFailed class","text":"
def __init__(self, error):\n

Bases: WorkerError

The worker raised an exception and did not complete.

"},{"location":"api/worker/#textual.worker.WorkerState","title":"WorkerState class","text":"

Bases: enum.Enum

A description of the worker's current state.

"},{"location":"api/worker/#textual.worker.WorkerState.CANCELLED","title":"CANCELLED instance-attribute class-attribute","text":"
CANCELLED = 3\n

Worker is not running, and was cancelled.

"},{"location":"api/worker/#textual.worker.WorkerState.ERROR","title":"ERROR instance-attribute class-attribute","text":"
ERROR = 4\n

Worker is not running, and exited with an error.

"},{"location":"api/worker/#textual.worker.WorkerState.PENDING","title":"PENDING instance-attribute class-attribute","text":"
PENDING = 1\n

Worker is initialized, but not running.

"},{"location":"api/worker/#textual.worker.WorkerState.RUNNING","title":"RUNNING instance-attribute class-attribute","text":"
RUNNING = 2\n

Worker is running.

"},{"location":"api/worker/#textual.worker.WorkerState.SUCCESS","title":"SUCCESS instance-attribute class-attribute","text":"
SUCCESS = 5\n

Worker is not running, and completed successfully.

"},{"location":"api/worker/#textual.worker.get_current_worker","title":"get_current_worker function","text":"
def get_current_worker():\n

Get the currently active worker.

Raises Type Description NoActiveWorker

If there is no active worker.

Returns Type Description Worker

A Worker instance.

"},{"location":"api/worker_manager/","title":"Worker manager","text":"

A class to manage workers for an app.

You access this object via App.workers or Widget.workers.

"},{"location":"api/worker_manager/#textual._worker_manager.WorkerManager","title":"WorkerManager class","text":"
def __init__(self, app):\n

An object to manager a number of workers.

You will not have to construct this class manually, as widgets, screens, and apps have a worker manager accessibly via a workers attribute.

Parameters Name Type Description Default app App

An App instance.

required"},{"location":"api/worker_manager/#textual._worker_manager.WorkerManager.add_worker","title":"add_worker method","text":"
def add_worker(self, worker, start=True, exclusive=True):\n

Add a new worker.

Parameters Name Type Description Default worker Worker

A Worker instance.

required start bool

Start the worker if True, otherwise the worker must be started manually.

True exclusive bool

Cancel all workers in the same group as worker.

True"},{"location":"api/worker_manager/#textual._worker_manager.WorkerManager.cancel_all","title":"cancel_all method","text":"
def cancel_all(self):\n

Cancel all workers.

"},{"location":"api/worker_manager/#textual._worker_manager.WorkerManager.cancel_group","title":"cancel_group method","text":"
def cancel_group(self, node, group):\n

Cancel a single group.

Parameters Name Type Description Default node DOMNode

Worker DOM node.

required group str

A group name.

required Returns Type Description list[Worker]

A list of workers that were cancelled.

"},{"location":"api/worker_manager/#textual._worker_manager.WorkerManager.cancel_node","title":"cancel_node method","text":"
def cancel_node(self, node):\n

Cancel all workers associated with a given node

Parameters Name Type Description Default node DOMNode

A DOM node (widget, screen, or App).

required Returns Type Description list[Worker]

List of cancelled workers.

"},{"location":"api/worker_manager/#textual._worker_manager.WorkerManager.start_all","title":"start_all method","text":"
def start_all(self):\n

Start all the workers.

"},{"location":"api/worker_manager/#textual._worker_manager.WorkerManager.wait_for_complete","title":"wait_for_complete async","text":"
def wait_for_complete(self, workers=None):\n

Wait for workers to complete.

Parameters Name Type Description Default workers Iterable[Worker] | None

An iterable of workers or None to wait for all workers in the manager.

None"},{"location":"blog/","title":"Textual Blog","text":""},{"location":"blog/2023/03/15/no-async-async-with-python/","title":"No-async async with Python","text":"

A (reasonable) criticism of async is that it tends to proliferate in your code. In order to await something, your functions must be async all the way up the call-stack. This tends to result in you making things async just to support that one call that needs it or, worse, adding async just-in-case. Given that going from def to async def is a breaking change there is a strong incentive to go straight there.

Before you know it, you have adopted a policy of \"async all the things\".

Textual is an async framework, but doesn't require the app developer to use the async and await keywords (but you can if you need to). This post is about how Textual accomplishes this async-agnosticism.

Info

See this example from the docs for an async-less Textual app.

"},{"location":"blog/2023/03/15/no-async-async-with-python/#an-apology","title":"An apology","text":"

But first, an apology! In a previous post I said Textual \"doesn't do any IO of its own\". This is not accurate. Textual responds to keys and mouse events (Input) and writes content to the terminal (Output).

Although Textual clearly does do IO, it uses asyncio mainly for concurrency. It allows each widget to update its part of the screen independently from the rest of the app.

"},{"location":"blog/2023/03/15/no-async-async-with-python/#await-me-maybe","title":"Await me (maybe)","text":"

The first no-async async technique is the \"Await me maybe\" pattern, a term first coined by Simon Willison. This is particularly applicable to callbacks (or in Textual terms, message handlers).

The await_me_maybe function below can run a callback that is either a plain old function or a coroutine (async def). It does this by awaiting the result of the callback if it is awaitable, or simply returning the result if it is not.

import asyncio\nimport inspect\ndef plain_old_function():\nreturn \"Plain old function\"\nasync def async_function():\nreturn \"Async function\"\nasync def await_me_maybe(callback):\nresult = callback()\nif inspect.isawaitable(result):\nreturn await result\nreturn result\nasync def run_framework():\nprint(\nawait await_me_maybe(plain_old_function)\n)\nprint(\nawait await_me_maybe(async_function)\n)\nif __name__ == \"__main__\":\nasyncio.run(run_framework())\n
"},{"location":"blog/2023/03/15/no-async-async-with-python/#optionally-awaitable","title":"Optionally awaitable","text":"

The \"await me maybe\" pattern is great when an async framework calls the app's code. The app developer can choose to write async code or not. Things get a little more complicated when the app wants to call the framework's API. If the API has asynced all the things, then it would force the app to do the same.

Textual's API consists of regular methods for the most part, but there are a few methods which are optionally awaitable. These are not coroutines (which must be awaited to do anything).

In practice, this means that those API calls initiate something which will complete a short time later. If you discard the return value then it won't prevent it from working. You only need to await if you want to know when it has finished.

The mount method is one such method. Calling it will add a widget to the screen:

def on_key(self):\n# Add MyWidget to the screen\nself.mount(MyWidget(\"Hello, World!\"))\n

In this example we don't care that the widget hasn't been mounted immediately, only that it will be soon.

Note

Textual awaits the result of mount after the message handler, so even if you don't explicitly await it, it will have been completed by the time the next message handler runs.

We might care if we want to mount a widget then make some changes to it. By making the handler async and awaiting the result of mount, we can be sure that the widget has been initialized before we update it:

async def on_key(self):\n# Add MyWidget to the screen\nawait self.mount(MyWidget(\"Hello, World!\"))\n# add a border\nself.query_one(MyWidget).styles.border = (\"heavy\", \"red\")\n

Incidentally, I found there were very few examples of writing awaitable objects in Python. So here is the code for AwaitMount which is returned by the mount method:

class AwaitMount:\n\"\"\"An awaitable returned by mount() and mount_all().\"\"\"\ndef __init__(self, parent: Widget, widgets: Sequence[Widget]) -> None:\nself._parent = parent\nself._widgets = widgets\nasync def __call__(self) -> None:\n\"\"\"Allows awaiting via a call operation.\"\"\"\nawait self\ndef __await__(self) -> Generator[None, None, None]:\nasync def await_mount() -> None:\nif self._widgets:\naws = [\ncreate_task(widget._mounted_event.wait(), name=\"await mount\")\nfor widget in self._widgets\n]\nif aws:\nawait wait(aws)\nself._parent.refresh(layout=True)\nreturn await_mount().__await__()\n
"},{"location":"blog/2023/03/15/no-async-async-with-python/#summing-up","title":"Summing up","text":"

Textual did initially \"async all the things\", which you might see if you find some old Textual code. Now async is optional.

This is not because I dislike async. I'm a fan! But it does place a small burden on the developer (more to type and think about). With the current API you generally don't need to write coroutines, or remember to await things. But async is there if you need it.

We're finding that Textual is increasingly becoming a UI to things which are naturally concurrent, so async was a good move. Concurrency can be a tricky subject, so we're planning some API magic to take the pain out of running tasks, threads, and processes. Stay tuned!

Join us on our Discord server if you want to talk about these things with the Textualize developers.

"},{"location":"blog/2022/12/08/be-the-keymaster/","title":"Be the Keymaster!","text":""},{"location":"blog/2022/12/08/be-the-keymaster/#that-didnt-go-to-plan","title":"That didn't go to plan","text":"

So... yeah... the blog. When I wrote my previous (and first) post I had wanted to try and do a post towards the end of each week, highlighting what I'd done on the \"dogfooding\" front. Life kinda had other plans. Not in a terrible way, but it turns out that getting both flu and Covid jabs (AKA \"jags\" as they tend to say in my adopted home) on the same day doesn't really agree with me too well.

I have been working, but there's been some odd moments in the past week and a bit and, last week, once I got to the end, I was glad for it to end. So no blog post happened.

Anyway...

"},{"location":"blog/2022/12/08/be-the-keymaster/#what-have-i-been-up-to","title":"What have I been up to?","text":"

While mostly sat feeling sorry for myself on my sofa, I have been coding. Rather than list all the different things here in detail, I'll quickly mention them with links to where to find them and play with them if you want:

"},{"location":"blog/2022/12/08/be-the-keymaster/#fivepyfive","title":"FivePyFive","text":"

While my Textual 5x5 puzzle is one of the examples in the Textual repo, I wanted to make it more widely available so people can download it with pip or pipx. See over on PyPi and see if you can solve it. ;-)

"},{"location":"blog/2022/12/08/be-the-keymaster/#textual-qrcode","title":"textual-qrcode","text":"

I wanted to put together a very small example of how someone may put together a third party widget library, and in doing so selected what I thought was going to be a mostly-useless example: a wrapper around a text-based QR code generator website. Weirdly I've had a couple of people express a need for QR codes in the terminal since publishing that!

"},{"location":"blog/2022/12/08/be-the-keymaster/#pispy","title":"PISpy","text":"

PISpy is a very simple terminal-based client for the PyPi API. Mostly it provides a hypertext interface to Python package details, letting you look up a package and then follow its dependency links. It's very simple at the moment, but I think more fun things can be done with this.

"},{"location":"blog/2022/12/08/be-the-keymaster/#oidia","title":"OIDIA","text":"

I'm a big fan of the use of streak-tracking in one form or another. Personally I use a streak-tracking app for keeping tabs of all sorts of good (and bad) habits, and as a heavy user of all things Apple I make a lot of use of the Fitness rings, etc. So I got to thinking it might be fun to do a really simple, no shaming, no counting, just recording, steak app for the Terminal. OIDIA is the result.

As of the time of writing I only finished the first version of this yesterday evening, so there are plenty of rough edges; but having got it to a point where it performed the basic tasks I wanted from it, that seemed like a good time to publish.

Expect to see this getting more updates and polish.

"},{"location":"blog/2022/12/08/be-the-keymaster/#wait-what-about-this-keymaster-thing","title":"Wait, what about this Keymaster thing?","text":"

Ahh, yes, about that... So one of the handy things I'm finding about Textual is its key binding system. The more I build Textual apps, the more I appreciate the bindings, how they can be associated with specific widgets, the use of actions (which can be used from other places too), etc.

But... (there's always a \"but\" right -- I mean, there'd be no blog post to be had here otherwise).

The terminal doesn't have access to all the key combinations you may want to use, and also, because some keys can't necessarily be \"typed\", at least not easily (think about it: there's no F1 character, you have to type F1), many keys and key combinations need to be bound with specific names.

So there's two problems here: how do I discover what keys even turn up in my application, and when they do, what should I call them when I pass them to Binding?

That felt like a \"well Dave just build an app for it!\" problem. So I did:

If you're building apps with Textual and you want to discover what keys turn up from your terminal and are available to your application, you can:

$ pipx install textual-keys\n

and then just run textual-keys and start mashing the keyboard to find out.

There's a good chance that this app, or at least a version of it, will make it into Textual itself (very likely as one of the devtools). But for now it's just an easy install away.

I think there's a call to be made here too: have you built anything to help speed up how you work with Textual, or just make the development experience \"just so\"? If so, do let us know, and come yell about it on the #show-and-tell channel in our Discord server.

"},{"location":"blog/2022/12/30/a-better-asyncio-sleep-for-windows-to-fix-animation/","title":"A better asyncio sleep for Windows to fix animation","text":"

I spent some time optimizing Textual on Windows recently, and discovered something which may be of interest to anyone working with async code on that platform.

Animation, scrolling, and fading had always been unsatisfactory on Windows. Textual was usable, but the lag when scrolling made apps feel far less snappy that other platforms. On macOS and Linux, scrolling is fast enough that it feels close to a native app, not something running in a terminal. Yet the Windows experience never improved, even as Textual got faster with each release.

I had chalked this up to Windows Terminal being slow to render updates. After all, the classic Windows terminal was (and still is) glacially slow. Perhaps Microsoft just weren't focusing on performance.

In retrospect, that was highly improbable. Like all modern terminals, Windows Terminal uses the GPU to render updates. Even without focussing on performance, it should be fast.

I figured I'd give it one last attempt to speed up Textual on Windows. If I failed, Windows would forever be a third-class platform for Textual apps.

It turned out that it was nothing to do with performance, per se. The issue was with a single asyncio function: asyncio.sleep.

Textual has a Timer class which creates events at regular intervals. It powers the JS-like set_interval and set_timer functions. It is also used internally to do animation (such as smooth scrolling). This Timer class calls asyncio.sleep to wait the time between one event and the next.

On macOS and Linux, calling asynco.sleep is fairly accurate. If you call sleep(3.14), it will return within 1% of 3.14 seconds. This is not the case for Windows, which for historical reasons uses a timer with a granularity of 15 milliseconds. The upshot is that sleep times will be rounded up to the nearest multiple of 15 milliseconds.

This limit appears to hold true for all async primitives on Windows. If you wait for something with a timeout, it will return on a multiple of 15 milliseconds. Fortunately there is work in the CPython pipeline to make this more accurate. Thanks to Steve Dower for pointing this out.

This lack of accuracy in the timer meant that timer events were created at a far slower rate than intended. Animation was slower because Textual was waiting too long between updates.

Once I had figured that out, I needed an alternative to asyncio.sleep for Textual's Timer class. And I found one. The following version of sleep is accurate to well within 1%:

from time import sleep as time_sleep\nfrom asyncio import get_running_loop\nasync def sleep(sleep_for: float) -> None:\n\"\"\"An asyncio sleep.\n    On Windows this achieves a better granularity than asyncio.sleep\n    Args:\n        sleep_for (float): Seconds to sleep for.\n    \"\"\"    \nawait get_running_loop().run_in_executor(None, time_sleep, sleep_for)\n

That is a drop-in replacement for sleep on Windows. With it, Textual runs a lot smoother. Easily on par with macOS and Linux.

It's not quite perfect. There is a little tearing during full \"screen\" updates, but performance is decent all round. I suspect when this bug is fixed (big thanks to Paul Moore for looking in to that), and Microsoft implements this protocol then Textual on Windows will be A+.

This Windows improvement will be in v0.9.0 of Textual, which will be released in a few days.

"},{"location":"blog/2023/02/11/the-heisenbug-lurking-in-your-async-code/","title":"The Heisenbug lurking in your async code","text":"

I'm taking a brief break from blogging about Textual to bring you this brief PSA for Python developers who work with async code. I wanted to expand a little on this tweet.

If you have ever used asyncio.create_task you may have created a bug for yourself that is challenging (read almost impossible) to reproduce. If it occurs, your code will likely fail in unpredictable ways.

The root cause of this Heisenbug is that if you don't hold a reference to the task object returned by create_task then the task may disappear without warning when Python runs garbage collection. In other words, the code in your task will stop running with no obvious indication why.

This behavior is well documented, as you can see from this excerpt (emphasis mine):

But who reads all the docs? And who has perfect recall if they do? A search on GitHub indicates that there are a lot of projects where this bug is waiting for just the right moment to ruin somebody's day.

I suspect the reason this mistake is so common is that tasks are a lot like threads (conceptually at least). With threads you can just launch them and forget. Unless you mark them as \"daemon\" threads they will exist for the lifetime of your app. Not so with Tasks.

The solution recommended in the docs is to keep a reference to the task for as long as you need it to live. On modern Python you could use TaskGroups which will keep references to your tasks. As long as all the tasks you spin up are in TaskGroups, you should be fine.

"},{"location":"blog/2023/03/08/overhead-of-python-asyncio-tasks/","title":"Overhead of Python Asyncio tasks","text":"

Every widget in Textual, be it a button, tree view, or a text input, runs an asyncio task. There is even a task for scrollbar corners (the little space formed when horizontal and vertical scrollbars meet).

Info

It may be IO that gives AsyncIO its name, but Textual doesn't do any IO of its own. Those tasks are used to power message queues, so that widgets (UI components) can do whatever they do at their own pace.

Its fair to say that Textual apps launch a lot of tasks. Which is why when I was trying to optimize startup (for apps with 1000s of widgets) I suspected it was task related.

I needed to know how much of an overhead it was to launch tasks. Tasks are lighter weight than threads, but how much lighter? The only way to know for certain was to profile.

The following code launches a load of do nothing tasks, then waits for them to shut down. This would give me an idea of how performant create_task is, and also a baseline for optimizations. I would know the absolute limit of any optimizations I make.

from asyncio import create_task, wait, run\nfrom time import process_time as time\nasync def time_tasks(count=100) -> float:\n\"\"\"Time creating and destroying tasks.\"\"\"\nasync def nop_task() -> None:\n\"\"\"Do nothing task.\"\"\"\npass\nstart = time()\ntasks = [create_task(nop_task()) for _ in range(count)]\nawait wait(tasks)\nelapsed = time() - start\nreturn elapsed\nfor count in range(100_000, 1000_000 + 1, 100_000):\ncreate_time = run(time_tasks(count))\ncreate_per_second = 1 / (create_time / count)\nprint(f\"{count:,} tasks \\t {create_per_second:0,.0f} tasks per/s\")\n

And here is the output:

100,000 tasks    280,003 tasks per/s\n200,000 tasks    255,275 tasks per/s\n300,000 tasks    248,713 tasks per/s\n400,000 tasks    248,383 tasks per/s\n500,000 tasks    241,624 tasks per/s\n600,000 tasks    260,660 tasks per/s\n700,000 tasks    244,510 tasks per/s\n800,000 tasks    247,455 tasks per/s\n900,000 tasks    242,744 tasks per/s\n1,000,000 tasks          259,715 tasks per/s\n

Info

Running on an M1 MacBook Pro.

This tells me I can create, run, and shutdown 260K tasks per second.

That's fast.

Clearly create_task is as close as you get to free in the Python world, and I would need to look elsewhere for optimizations. Turns out Textual spends far more time processing CSS rules than creating tasks (obvious in retrospect). I've noticed some big wins there, so the next version of Textual will be faster to start apps with a metric tonne of widgets.

But I still need to know what to do with those scrollbar corners. A task for two characters. I don't even...

"},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/","title":"A year of building for the terminal","text":"

I joined Textualize back in January 2022, and since then have been hard at work with the team on both Rich and Textual. Over the course of the year, I\u2019ve been able to work on a lot of really cool things. In this post, I\u2019ll review a subset of the more interesting and visual stuff I\u2019ve built. If you\u2019re into terminals and command line tooling, you\u2019ll hopefully see at least one thing of interest!

"},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/#a-file-manager-powered-by-textual","title":"A file manager powered by Textual","text":"

I\u2019ve been slowly developing a file manager as a \u201cdogfooding\u201d project for Textual. It takes inspiration from tools such as Ranger and Midnight Commander.

As of December 2022, it lets you browse your file system, filtering, multi-selection, creating and deleting files/directories, opening files in your $EDITOR and more.

I\u2019m happy with how far this project has come \u2014 I think it\u2019s a good example of the type of powerful application that can be built with Textual with relatively little code. I\u2019ve been able to focus on features, instead of worrying about terminal emulator implementation details.

The project is available on GitHub.

"},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/#better-diffs-in-the-terminal","title":"Better diffs in the terminal","text":"

Diffs in the terminal are often difficult to read at a glance. I wanted to see how close I could get to achieving a diff display of a quality similar to that found in the GitHub UI.

To attempt this, I built a tool called Dunk. It\u2019s a command line program which you can pipe your git diff output into, and it\u2019ll convert it into something which I find much more readable.

Although I\u2019m not particularly proud of the code - there are a lot of \u201chacks\u201d going on, but I\u2019m proud of the result. If anything, it shows what can be achieved for tools like this.

For many diffs, the difference between running git diff and git diff | dunk | less -R is night and day.

It\u2019d be interesting to revisit this at some point. It has its issues, but I\u2019d love to see how it can be used alongside Textual to build a terminal-based diff/merge tool. Perhaps it could be combined with\u2026

"},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/#code-editor-floating-gutter","title":"Code editor floating gutter","text":"

This is a common feature in text editors and IDEs: when you scroll to the right, you should still be able to see what line you\u2019re on. Out of interest, I tried to recreate the effect in the terminal using Textual.

Textual CSS offers a dock property which allows you to attach a widget to an edge of its parent. By creating a widget that contains a vertical list of numbers and setting the dock property to left, we can create a floating gutter effect. Then, we just need to keep the scroll_y in sync between the gutter and the content to ensure the line numbers stay aligned.

"},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/#dropdown-autocompletion-menu","title":"Dropdown autocompletion menu","text":"

While working on Shira (a proof-of-concept, terminal-based Python object explorer), I wrote some autocompleting dropdown functionality.

Textual forgoes the z-index concept from browser CSS and instead uses a \u201cnamed layer\u201d system. Using the layers property you can defined an ordered list of named layers, and using the layer property, you can assign a descendant widget to one of those layers.

By creating a new layer above all others and assigning a widget to that layer, we can ensure that widget is painted above everything else.

In order to determine where to place the dropdown, we can track the current value in the dropdown by watching the reactive input \u201cvalue\u201d inside the Input widget. This method will be called every time the value of the Input changes, and we can use this hook to amend the position of our dropdown position to accommodate for the length of the input value.

I\u2019ve now extracted this into a separate library called textual-autocomplete.

"},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/#tabs-with-animated-underline","title":"Tabs with animated underline","text":"

The aim here was to create a tab widget with underlines that animates smoothly as another tab is selected.

The difficulty with implementing something like this is that we don\u2019t have pixel-perfect resolution when animating - a terminal window is just a big grid of fixed-width character cells.

However, when animating things in a terminal, we can often achieve better granularity using Unicode related tricks. In this case, instead of shifting the bar along one whole cell, we adjust the endings of the bar to be a character which takes up half of a cell.

The exact characters that form the bar are \"\u257a\", \"\u2501\" and \"\u2578\". When the bar sits perfectly within cell boundaries, every character is \u201c\u2501\u201d. As it travels over a cell boundary, the left and right ends of the bar are updated to \"\u257a\" and \"\u2578\" respectively.

"},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/#snapshot-testing-for-terminal-apps","title":"Snapshot testing for terminal apps","text":"

One of the great features we added to Rich this year was the ability to export console contents to an SVG. This feature was later exposed to Textual, allowing users to capture screenshots of their running Textual apps. Ultimately, I ended up creating a tool for snapshot testing in the Textual codebase.

Snapshot testing is used to ensure that Textual output doesn\u2019t unexpectedly change. On disk, we store what we expect the output to look like. Then, when we run our unit tests, we get immediately alerted if the output has changed.

This essentially automates the process of manually spinning up several apps and inspecting them for unexpected visual changes. It\u2019s great for catching subtle regressions!

In Textual, each CSS property has its own canonical example and an associated snapshot test. If we accidentally break a property in a way that affects the visual output, the chances of it sneaking into a release are greatly reduced, because the corresponding snapshot test will fail.

As part of this work, I built a web interface for comparing snapshots with test output. There\u2019s even a little toggle which highlights the differences, since they\u2019re sometimes rather subtle.

Since the terminal output shown in the video above is just an SVG image, I was able to add the \"Show difference\" functionality by overlaying the two images and applying a single CSS property: mix-blend-mode: difference;.

The snapshot testing functionality itself is implemented as a pytest plugin, and it builds on top of a snapshot testing framework called syrupy.

It's quite likely that this will eventually be exposed to end-users of Textual.

"},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/#demonstrating-animation","title":"Demonstrating animation","text":"

I built an example app to demonstrate how to animate in Textual and the available easing functions.

The smoothness here is achieved using tricks similar to those used in the tabs I discussed earlier. In fact, the bar that animates in the video above is the same Rich renderable that is used by Textual's scrollbars.

You can play with this app by running textual easing. Please use animation sparingly.

"},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/#developer-console","title":"Developer console","text":"

When developing terminal based applications, performing simple debugging using print can be difficult, since the terminal is in application mode.

A project I worked on earlier in the year to improve the situation was the Textual developer console, which you can launch with textual console.

On the right, Dave's 5x5 Textual app. On the left, the Textual console.

Then, by running a Textual application with the --dev flag, all standard output will be redirected to it. This means you can use the builtin print function and still immediately see the output. Textual itself also writes information to this console, giving insight into the messages that are flowing through an application.

"},{"location":"blog/2022/12/20/a-year-of-building-for-the-terminal/#pixel-art","title":"Pixel art","text":"

Cells in the terminal are roughly two times taller than they are wide. This means, that two horizontally adjacent cells form an approximate square.

Using this fact, I wrote a simple library based on Rich and PIL which can convert an image file into terminal output. You can find the library, rich-pixels, on GitHub.

It\u2019s particularly good for displaying simple pixel art images. The SVG image below is also a good example of the SVG export functionality I touched on earlier.

Rich

Since the library generates an object which is renderable using Rich, these can easily be embedded inside Textual applications.

Here's an example of that in a scrapped \"Pok\u00e9dex\" app I threw together:

This is a rather naive approach to the problem... but I did it for fun!

Other methods for displaying images in the terminal include:

  • A more advanced library like chafa, which uses a range of Unicode characters to achieve a more accurate representation of the image.
  • One of the available terminal image protocols, such as Sixel, Kitty\u2019s Terminal Graphics Protocol, and iTerm Inline Images Protocol.

That was a whirlwind tour of just some of the projects I tackled in 2022. If you found it interesting, be sure to follow me on Twitter. I don't post often, but when I do, it's usually about things similar to those I've discussed here.

"},{"location":"blog/2022/11/06/new-blog/","title":"New Blog","text":"

Welcome to the first post on the Textual blog.

I plan on using this as a place to make announcements regarding new releases of Textual, and any other relevant news.

The first piece of news is that we've reorganized this site a little. The Events, Styles, and Widgets references are now under \"Reference\", and what used to be under \"Reference\" is now \"API\" which contains API-level documentation. I hope that's a little clearer than it used to be!

"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/","title":"So you're looking for a wee bit of Textual help...","text":""},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#introduction","title":"Introduction","text":"

Quote

Patience, Highlander. You have done well. But it'll take time. You are generations being born and dying. You are at one with all living things. Each man's thoughts and dreams are yours to know. You have power beyond imagination. Use it well, my friend. Don't lose your head.

Juan S\u00e1nchez Villalobos Ram\u00edrez, Chief metallurgist to King Charles V of Spain

As of the time of writing, I'm a couple or so days off having been with Textualize for 3 months. It's been fun, and educational, and every bit as engaging as I'd hoped, and more. One thing I hadn't quite prepared for though, but which I really love, is how so many other people are learning Textual along with me.

Even in those three months the library has changed and expanded quite a lot, and it continues to do so. Meanwhile, more people are turning up and using the framework; you can see this online in social media, blogs and of course in the ever-growing list of projects on GitHub which depend on Textual.

This inevitably means there's a lot of people getting to grips with a new tool, and one that is still a bit of a moving target. This in turn means lots of people are coming to us to get help.

As I've watched this happen I've noticed a few patterns emerging. Some of these good or neutral, some... let's just say not really beneficial to those seeking the help, or to those trying to provide the help. So I wanted to write a little bit about the different ways you can get help with Textual and your Textual-based projects, and to also try and encourage people to take the most helpful and positive approach to getting that help.

Now, before I go on, I want to make something very clear: I'm writing this as an individual. This is my own personal view, and my own advice from me to anyone who wishes to take it. It's not Textual (the project) or Textualize (the company) policy, rules or guidelines. This is just some ageing hacker's take on how best to go about asking for help, informed by years of asking for and also providing help in email, on Usenet, on forums, etc.

Or, put another way: if what you read in here seems sensible to you, I figure we'll likely have already hit it off over on GitHub or in the Discord server. ;-)

"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#where-to-go-for-help","title":"Where to go for help","text":"

At this point this is almost a bit of an FAQ itself, so I thought I'd address it here: where's the best place to ask for help about Textual, and what's the difference between GitHub Issues, Discussions and our Discord server?

I'd suggest thinking of them like this:

"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#discord","title":"Discord","text":"

You have a question, or need help with something, and perhaps you could do with a reply as soon as possible. But, and this is the really important part, it doesn't matter if you don't get a response. If you're in this situation then the Discord server is possibly a good place to start. If you're lucky someone will be hanging about who can help out.

I can't speak for anyone else, but keep this in mind: when I look in on Discord I tend not to go scrolling back much to see if anything has been missed. If something catches my eye, I'll try and reply, but if it doesn't... well, it's mostly an instant chat thing so I don't dive too deeply back in time.

Going from Discord to a GitHub issue

As a slight aside here: sometimes people will pop up in Discord, ask a question about something that turns out looking like a bug, and that's the last we hear of it. Please, please, please, if this happens, the most helpful thing you can do is go raise an issue for us. It'll help us to keep track of problems, it'll help get your problem fixed, it'll mean everyone benefits.

My own advice would be to treat Discord as an ephemeral resource. It happens in the moment but fades away pretty quickly. It's like knocking on a friend's door to see if they're in. If they're not in, you might leave them a note, which is sort of like going to...

"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#github","title":"GitHub","text":"

On the other hand, if you have a question or need some help or something where you want to stand a good chance of the Textual developers (amongst others) seeing it and responding, I'd recommend that GitHub is the place to go. Dropping something into the discussions there, or leaving an issue, ensures it'll get seen. It won't get lost.

As for which you should use -- a discussion or an issue -- I'd suggest this: if you need help with something, or you want to check your understanding of something, or you just want to be sure something is a problem before taking it further, a discussion might be the best thing. On the other hand, if you've got a clear bug or feature request on your hands, an issue makes a lot of sense.

Don't worry if you're not sure which camp your question or whatever falls into though; go with what you think is right. There's no harm done either way (I may move an issue to a discussion first before replying, if it's really just a request for help -- but that's mostly so everyone can benefit from finding it in the right place later on down the line).

"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#the-dos-and-donts-of-getting-help","title":"The dos and don'ts of getting help","text":"

Now on to the fun part. This is where I get a bit preachy. Ish. Kinda. A little bit. Again, please remember, this isn't a set of rules, this isn't a set of official guidelines, this is just a bunch of \"if you want my advice, and I know you didn't ask but you've read this far so you actually sort of did don't say I didn't warn you!\" waffle.

This isn't going to be an exhaustive collection, far from it. But I feel these are some important highlights.

"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#do","title":"Do...","text":"

When looking for help, in any of the locations mentioned above, I'd totally encourage:

"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#be-clear-and-detailed","title":"Be clear and detailed","text":"

Too much detail is almost always way better than not enough. \"My program didn't run\", often even with some of the code supplied, is so much harder to help than \"I ran this code I'm posting here, and I expected this particular outcome, and I expected it because I'd read this particular thing in the docs and had comprehended it to mean this, but instead the outcome was this exception here, and I'm a bit stuck -- can someone offer some pointers?\"

The former approach means there often ends up having to be a back and forth which can last a long time, and which can sometimes be frustrating for the person asking. Manage frustration: be clear, tell us everything you can.

"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#say-what-resources-youve-used-already","title":"Say what resources you've used already","text":"

If you've read the potions of the documentation that relate to what you're trying to do, it's going to be really helpful if you say so. If you don't, it might be assumed you haven't and you may end up being pointed at them.

So, please, if you've checked the documentation, looked in the FAQ, done a search of past issues or discussions or perhaps even done a search on the Discord server... please say so.

"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#be-polite","title":"Be polite","text":"

This one can go a long way when looking for help. Look, I get it, programming is bloody frustrating at times. We've all rage-quit some code at some point, I'm sure. It's likely going to be your moment of greatest frustration when you go looking for help. But if you turn up looking for help acting all grumpy and stuff it's not going to come over well. Folk are less likely to be motivated to lend a hand to someone who seems rather annoyed.

If you throw in a please and thank-you here and there that makes it all the better.

"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#fully-consider-the-replies","title":"Fully consider the replies","text":"

You could find yourself getting a reply that you're sure won't help at all. That's fair. But be sure to fully consider it first. Perhaps you missed the obvious along the way and this is 100% the course correction you'd unknowingly come looking for in the first place. Sure, the person replying might have totally misunderstood what was being asked, or might be giving a wrong answer (it me! I've totally done that and will again!), but even then a reply along the lines of \"I'm not sure that's what I'm looking for, because...\" gets everyone to the solution faster than \"lol nah\".

"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#entertain-what-might-seem-like-odd-questions","title":"Entertain what might seem like odd questions","text":"

Aye, I get it, being asked questions when you're looking for an answer can be a bit frustrating. But if you find yourself on the receiving end of a small series of questions about your question, keep this in mind: Textual is still rather new and still developing and it's possible that what you're trying to do isn't the correct way to do that thing. To the person looking to help you it may seem to them you have an XY problem.

Entertaining those questions might just get you to the real solution to your problem.

"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#allow-for-language-differences","title":"Allow for language differences","text":"

You don't need me to tell you that a project such as Textual has a global audience. With that rather obvious fact comes the other fact that we don't all share the same first language. So, please, as much as possible, try and allow for that. If someone is trying to help you out, and they make it clear they're struggling to follow you, keep this in mind.

"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#acknowledge-the-answer","title":"Acknowledge the answer","text":"

I suppose this is a variation on \"be polite\" (really, a thanks can go a long way), but there's more to this than a friendly acknowledgement. If someone has gone to the trouble of offering some help, it's helpful to everyone who comes after you to acknowledge if it worked or not. That way a future help-seeker will know if the answer they're reading stands a chance of being the right one.

"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#accept-that-textual-is-zero-point-software-right-now","title":"Accept that Textual is zero-point software (right now)","text":"

Of course the aim is to have every release of Textual be stable and useful, but things will break. So, please, do keep in mind things like:

  • Textual likely doesn't have your feature of choice just yet.
  • We might accidentally break something (perhaps pinning Textual and testing each release is a good plan here?).
  • We might deliberately break something because we've decided to take a particular feature or way of doing things in a better direction.

Of course it can be a bit frustrating a times, but overall the aim is to have the best framework possible in the long run.

"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#dont","title":"Don't...","text":"

Okay, now for a bit of old-hacker finger-wagging. Here's a few things I'd personally discourage:

"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#lack-patience","title":"Lack patience","text":"

Sure, it can be annoying. You're in your flow, you've got a neat idea for a thing you want to build, you're stuck on one particular thing and you really need help right now! Thing is, that's unlikely to happen. Badgering individuals, or a whole resource, to reply right now, or complaining that it's been $TIME_PERIOD since you asked and nobody has replied... that's just going to make people less likely to reply.

"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#unnecessarily-tag-individuals","title":"Unnecessarily tag individuals","text":"

This one often goes hand in hand with the \"lack patience\" thing: Be it asking on Discord, or in GitHub issues, discussions or even PRs, unnecessarily tagging individuals is a bit rude. Speaking for myself and only myself: I love helping folk with Textual. If I could help everyone all the time the moment they have a problem, I would. But it doesn't work like that. There's any number of reasons I might not be responding to a particular request, including but not limited to (here I'm talking personally because I don't want to speak for anyone else, but I'm sure I'm not alone here):

  • I have a job. Sure, my job is (in part) Textual, but there's more to it than that particular issue. I might be doing other stuff.
  • I have my own projects to work on too. I like coding for fun as well (or writing preaching old dude blog posts like this I guess, but you get the idea).
  • I actually have other interests outside of work hours so I might actually be out doing a 10k in the local glen, or battling headcrabs in VR, or something.
  • Housework. :-/

You get the idea though. So while I'm off having a well-rounded life, it's not good to get unnecessarily intrusive alerts to something that either a) doesn't actually directly involve me or b) could wait.

"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#seek-personal-support","title":"Seek personal support","text":"

Again, I'm going to speak totally for myself here, but I also feel the general case is polite for all: there's a lot of good support resources available already; sending DMs on Discord or Twitter or in the Fediverse, looking for direct personal support, isn't really the best way to get help. Using the public/collective resources is absolutely the best way to get that help. Why's it a bad idea to dive into DMs? Here's some reasons I think it's not a good idea:

  • It's a variation on \"unnecessarily tagging individuals\".
  • You're short-changing yourself when it comes to getting help. If you ask somewhere more public you're asking a much bigger audience, who collectively have more time, more knowledge and more experience than a single individual.
  • Following on from that, any answers can be (politely) fact-checked or enhanced by that audience, resulting in a better chance of getting the best help possible.
  • The next seeker-of-help gets to miss out on your question and the answer. If asked and answered in public, it's a record that can help someone else in the future.
"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#doubt-your-ability-or-skill-level","title":"Doubt your ability or skill level","text":"

I suppose this should really be phrased as a do rather than a don't, as here I want to encourage something positive. A few times I've helped people out who have been very apologetic about their questions being \"noob\" questions, or about how they're fairly new to Python, or programming in general. Really, please, don't feel the need to apologise and don't be ashamed of where you're at.

If you've asked something that's obviously answered in the documentation, that's not a problem; you'll likely get pointed at the docs and it's what happens next that's the key bit. If the attitude is \"oh, cool, that's exactly what I needed to be reading, thanks!\" that's a really positive thing. The only time it's a problem is when there's a real reluctance to use the available resources. We've all seen that person somewhere at some point, right? ;-)

Not knowing things is totally cool.

"},{"location":"blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/#conclusion","title":"Conclusion","text":"

So, that's my waffle over. As I said at the start: this is my own personal thoughts on how to get help with Textual, both as someone whose job it is to work on Textual and help people with Textual, and also as a FOSS advocate and supporter who can normally be found helping Textual users when he's not \"on the clock\" too.

What I've written here isn't exhaustive. Neither is it novel. Plenty has been written on the general subject in the past, and I'm sure more will be written on the subject in the future. I do, however, feel that these are the most common things I notice. I'd say those dos and don'ts cover 90% of \"can I get some help?\" interactions; perhaps closer to 99%.

Finally, and I think this is the most important thing to remember, the next time you are battling some issue while working with Textual: don't lose your head!

"},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/","title":"On dog food, the (original) Metaverse, and (not) being bored","text":""},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#introduction","title":"Introduction","text":"

Quote

Cutler, armed with a schedule, was urging the team to \"eat its own dog food\". Part macho stunt and part common sense, the \"dog food diet\" was the cornerstone of Cutler\u2019s philosophy.

G. Pascal Zachary \u2014 Show-Stopper!

I can't remember exactly when it was -- it was likely late in 1994 or some time in 1995 -- when I first came across the concept of, or rather the name for the concept of, \"eating your own dog food\". The idea and the name played a huge part in the book Show-Stopper! by G. Pascal Zachary. The idea wasn't new to me of course; I'd been writing code for over a decade by then and plenty of times I'd built things and then used those things to do things, but it was fascinating to a mostly-self-taught 20-something me to be reading this (excellent -- go read it if you care about the history of your craft) book and to see the idea written down and named.

While Textualize isn't (thankfully -- really, I do recommend reading the book) anything like working on the team building Windows NT, the idea of taking a little time out from working on Textual, and instead work with Textual, makes a lot of sense. It's far too easy to get focused on adding things and improving things and tweaking things while losing sight of the fact that people will want to build with your product.

So you can imagine how pleased I was when Will announced that he wanted all of us to spend a couple or so weeks building something with Textual. I had, of course, already written one small application with the library, and had plans for another (in part it's how I ended up working here), but I'd yet to really dive in and try and build something more involved.

Giving it some thought: I wasn't entirely sure what I wanted to build though. I do want to use Textual to build a brand new terminal-based Norton Guide reader (not my first, not by a long way) but I felt that was possibly a bit too niche, and actually could take a bit too long anyway. Maybe not, it remains to be seen.

Eventually I decided on this approach: try and do a quick prototype of some daft idea each day or each couple of days, do that for a week or so, and then finally try and settle down on something less trivial. This approach should work well in that it'll help introduce me to more of Textual, help try out a few different parts of the library, and also hopefully discover some real pain-points with working with it and highlight a list of issues we should address -- as seen from the perspective of a developer working with the library.

So, here I am, at the end of week one. What I want to try and do is briefly (yes yes, I know, this introduction is the antithesis of brief) talk about what I built and perhaps try and highlight some lessons learnt, highlight some patterns I think are useful, and generally do an end-of-week version of a TIL. TWIL?

Yeah. I guess this is a TWIL.

"},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#gridinfo","title":"gridinfo","text":"

I started the week by digging out a quick hack I'd done a couple of weeks earlier, with a view to cleaning it up. It started out as a fun attempt to do something with Rich Pixels while also making a terminal-based take on slstats.el. I'm actually pleased with the result and how quickly it came together.

The point of the application itself is to show some general information about the current state of the Second Life grid (hello to any fellow residents of the original Metaverse!), and to also provide a simple region lookup screen that, using Rich Pixels, will display the object map (albeit in pretty low resolution -- but that's the fun of this!).

So the opening screen looks like this:

and a lookup of a region looks like this:

Here's a wee video of the whole thing in action:

"},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#worth-a-highlight","title":"Worth a highlight","text":"

Here's a couple of things from the code that I think are worth a highlight, as things to consider when building Textual apps:

"},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#dont-use-the-default-screen","title":"Don't use the default screen","text":"

Use of the default Screen that's provided by the App is handy enough, but I feel any non-trivial application should really put as much code as possible in screens that relate to key \"work\". Here's the entirety of my application code:

class GridInfo( App[ None ] ):\n\"\"\"TUI app for showing information about the Second Life grid.\"\"\"\nCSS_PATH = \"gridinfo.css\"\n\"\"\"The name of the CSS file for the app.\"\"\"\nTITLE = \"Grid Information\"\n\"\"\"str: The title of the application.\"\"\"\nSCREENS = {\n\"main\": Main,\n\"region\": RegionInfo\n}\n\"\"\"The collection of application screens.\"\"\"\ndef on_mount( self ) -> None:\n\"\"\"Set up the application on startup.\"\"\"\nself.push_screen( \"main\" )\n

You'll notice there's no work done in the app, other than to declare the screens, and to set the main screen running when the app is mounted.

"},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#dont-work-hard-on_mount","title":"Don't work hard on_mount","text":"

My initial version of the application had it loading up the data from the Second Life and GridSurvey APIs in Main.on_mount. This obviously wasn't a great idea as it made the startup appear slow. That's when I realised just how handy call_after_refresh is. This meant I could show some placeholder information and then fire off the requests (3 of them: one to get the main grid information, one to get the grid concurrency data, and one to get the grid size data), keeping the application looking active and updating the display when the replies came in.

"},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#pain-points","title":"Pain points","text":"

While building this app I think there was only really the one pain-point, and I suspect it's mostly more on me than on Textual itself: getting a good layout and playing whack-a-mole with CSS. I suspect this is going to be down to getting more and more familiar with CSS and the terminal (which is different from laying things out for the web), while also practising with various layout schemes -- which is where the revamped Placeholder class is going to be really useful.

"},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#unbored","title":"unbored","text":"

The next application was initially going to be a very quick hack, but actually turned into a less-trivial build than I'd initially envisaged; not in a negative way though. The more I played with it the more I explored and I feel that this ended up being my first really good exploration of some useful (personal -- your kilometerage may vary) patterns and approaches when working with Textual.

The application itself is a terminal client for the Bored-API. I had initially intended to roll my own code for working with the API, but I noticed that someone had done a nice library for it and it seemed silly to not build on that. Not needing to faff with that, I could concentrate on the application itself.

At first I was just going to let the user click away at a button that showed a random activity, but this quickly morphed into a \"why don't I make this into a sort of TODO list builder app, where you can add things to do when you are bored, and delete things you don't care for or have done\" approach.

Here's a view of the main screen:

and here's a view of the filter pop-over:

"},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#worth-a-highlight_1","title":"Worth a highlight","text":""},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#dont-put-all-your-bindings-in-one-place","title":"Don't put all your BINDINGS in one place","text":"

This came about from me overloading the use of the escape key. I wanted it to work more or less like this:

  • If you're inside an activity, move focus up to the activity type selection buttons.
  • If the filter pop-over is visible, close that.
  • Otherwise exit the application.

It was easy enough to do, and I had an action in the Main screen that escape was bound to (again, in the Main screen) that did all this logic with some if/elif work but it didn't feel elegant. Moreover, it meant that the Footer always displayed the same description for the key.

That's when I realised that it made way more sense to have a Binding for escape in every widget that was the actual context for escape's use. So I went from one top-level binding to...

...\nclass Activity( Widget ):\n\"\"\"A widget that holds and displays a suggested activity.\"\"\"\nBINDINGS = [\n...\nBinding( \"escape\", \"deselect\", \"Switch to Types\" )\n]\n...\nclass Filters( Vertical ):\n\"\"\"Filtering sidebar.\"\"\"\nBINDINGS = [\nBinding( \"escape\", \"close\", \"Close Filters\" )\n]\n...\nclass Main( Screen ):\n\"\"\"The main application screen.\"\"\"\nBINDINGS = [\nBinding( \"escape\", \"quit\", \"Close\" )\n]\n\"\"\"The bindings for the main screen.\"\"\"\n

This was so much cleaner and I got better Footer descriptions too. I'm going to be leaning hard on this approach from now on.

"},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#messages-are-awesome","title":"Messages are awesome","text":"

Until I wrote this application I hadn't really had a need to define or use my own Messages. During work on this I realised how handy they really are. In the code I have an Activity widget which takes care of the job of moving itself amongst its siblings if the user asks to move an activity up or down. When this happens I also want the Main screen to save the activities to the filesystem as things have changed.

Thing is: I don't want the screen to know what an Activity is capable of and I don't want an Activity to know what the screen is capable of; especially the latter as I really don't want a child of a screen to know what the screen can do (in this case \"save stuff\").

This is where messages come in. Using a message I could just set things up so that the Activity could shout out \"HEY I JUST DID A THING THAT CHANGES ME\" and not care who is listening and not care what they do with that information.

So, thanks to this bit of code in my Activity widget...

    class Moved( Message ):\n\"\"\"A message to indicate that an activity has moved.\"\"\"\ndef action_move_up( self ) -> None:\n\"\"\"Move this activity up one place in the list.\"\"\"\nif self.parent is not None and not self.is_first:\nparent = cast( Widget, self.parent )\nparent.move_child(\nself, before=parent.children.index( self ) - 1\n)\nself.emit_no_wait( self.Moved( self ) )\nself.scroll_visible( top=True )\n

...the Main screen can do this:

    def on_activity_moved( self, _: Activity.Moved ) -> None:\n\"\"\"React to an activity being moved.\"\"\"\nself.save_activity_list()\n

Warning

The code above used emit_no_wait. Since this blog post was first published that method has been removed from Textual. You should use post_message_no_wait or post_message instead now.

"},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#pain-points_1","title":"Pain points","text":"

On top of the issues of getting to know terminal-based-CSS that I mentioned earlier:

  • Textual currently lacks any sort of selection list or radio-set widget. This meant that I couldn't quite do the activity type picking how I would have wanted. Of course I could have rolled my own widgets for this, but I think I'd sooner wait until such things are in Textual itself.
  • Similar to that, I could have used some validating Input widgets. They too are on the roadmap but I managed to cobble together fairly good working versions for my purposes. In doing so though I did further highlight that the reactive attribute facility needs a wee bit more attention as I ran into some (already-known) bugs. Thankfully in my case it was a very easy workaround.
  • Scrolling in general seems a wee bit off when it comes to widgets that are more than one line tall. While there's nothing really obvious I can point my finger at, I'm finding that scrolling containers sometimes get confused about what should be in view. This becomes very obvious when forcing things to scroll from code. I feel this deserves a dedicated test application to explore this more.
"},{"location":"blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/#conclusion","title":"Conclusion","text":"

The first week of \"dogfooding\" has been fun and I'm more convinced than ever that it's an excellent exercise for Textualize to engage in. I didn't quite manage my plan of \"one silly trivial prototype per day\", which means I've ended up with two (well technically one and a half I guess given that gridinfo already existed as a prototype) applications rather than four. I'm okay with that. I got a lot of utility out of this.

Now to look at the list of ideas I have going and think about what I'll kick next week off with...

"},{"location":"blog/2022/11/22/what-i-learned-from-my-first-non-trivial-pr/","title":"What I learned from my first non-trivial PR","text":"PlaceholderApp Placeholder\u00a0p2\u00a0here! This\u00a0is\u00a0a\u00a0custom\u00a0label\u00a0for\u00a0p1. #p4 #p3#p5Placeholde r Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0 26\u00a0x\u00a06amet,\u00a0consectetur\u00a027\u00a0x\u00a06 adipiscing\u00a0elit.\u00a0Etiam\u00a0 feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0 Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0 consectetur\u00a0adipiscing\u00a0elit.\u00a0Etiam\u00a040\u00a0x\u00a06 feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0accumsan.\u00a0 Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis gravida.\u00a0Phasellus\u00a0id\u00a0eleifend\u00a0 ligula.\u00a0Nullam\u00a0imperdiet\u00a0sem\u00a0tellus, sed\u00a0vehicula\u00a0nisl\u00a0faucibus\u00a0sit\u00a0amet.Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0 Praesent\u00a0iaculis\u00a0tempor\u00a0ultricies.\u00a0\u2586\u2586consectetur\u00a0adipiscing\u00a0elit.\u00a0Etiam\u00a0\u2586\u2586 Sed\u00a0lacinia,\u00a0tellus\u00a0id\u00a0rutrum\u00a0feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0accumsan.\u00a0 lacinia,\u00a0sapien\u00a0sapien\u00a0congue\u00a0Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis

It's 8:59 am and, by my Portuguese standards, it is freezing cold outside: 5 or 6 degrees Celsius. It is my second day at Textualize and I just got into the office. I undress my many layers of clothing to protect me from the Scottish cold and I sit down in my improvised corner of the Textualize office. As I sit down, I turn myself in my chair to face my boss and colleagues to ask \u201cSo, what should I do today?\u201d. I was not expecting Will's answer, but the challenge excited me:

\u201cI thought I'll just throw you in the deep end and have you write some code.\u201d

What happened next was that I spent two days working on PR #1229 to add a new widget to the Textual code base. At the time of writing, the pull request has not been merged yet. Well, to be honest with you, it hasn't even been reviewed by anyone... But that won't stop me from blogging about some of the things I learned while creating this PR.

"},{"location":"blog/2022/11/22/what-i-learned-from-my-first-non-trivial-pr/#the-placeholder-widget","title":"The placeholder widget","text":"

This PR adds a widget called Placeholder to Textual. As per the documentation, this widget \u201cis meant to have no complex functionality. Use the placeholder widget when studying the layout of your app before having to develop your custom widgets.\u201d

The point of the placeholder widget is that you can focus on building the layout of your app without having to have all of your (custom) widgets ready. The placeholder widget also displays a couple of useful pieces of information to help you work out the layout of your app, namely the ID of the widget itself (or a custom label, if you provide one) and the width and height of the widget.

As an example of usage of the placeholder widget, you can refer to the screenshot at the top of this blog post, which I included below so you don't have to scroll up:

PlaceholderApp Placeholder\u00a0p2\u00a0here! This\u00a0is\u00a0a\u00a0custom\u00a0label\u00a0for\u00a0p1. #p4 #p3#p5Placeholde r Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0 26\u00a0x\u00a06amet,\u00a0consectetur\u00a027\u00a0x\u00a06 adipiscing\u00a0elit.\u00a0Etiam\u00a0 feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0 Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0 consectetur\u00a0adipiscing\u00a0elit.\u00a0Etiam\u00a040\u00a0x\u00a06 feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0accumsan.\u00a0 Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis gravida.\u00a0Phasellus\u00a0id\u00a0eleifend\u00a0 ligula.\u00a0Nullam\u00a0imperdiet\u00a0sem\u00a0tellus, sed\u00a0vehicula\u00a0nisl\u00a0faucibus\u00a0sit\u00a0amet.Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0 Praesent\u00a0iaculis\u00a0tempor\u00a0ultricies.\u00a0\u2586\u2586consectetur\u00a0adipiscing\u00a0elit.\u00a0Etiam\u00a0\u2586\u2586 Sed\u00a0lacinia,\u00a0tellus\u00a0id\u00a0rutrum\u00a0feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0accumsan.\u00a0 lacinia,\u00a0sapien\u00a0sapien\u00a0congue\u00a0Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis

The top left and top right widgets have custom labels. Immediately under the top right placeholder, you can see some placeholders identified as #p3, #p4, and #p5. Those are the IDs of the respective placeholders. Then, rows 2 and 3 contain some placeholders that show their respective size and some placeholders that just contain some text.

"},{"location":"blog/2022/11/22/what-i-learned-from-my-first-non-trivial-pr/#bootstrapping-the-code-for-the-widget","title":"Bootstrapping the code for the widget","text":"

So, how does a code monkey start working on a non-trivial PR within 24 hours of joining a company? The answer is simple: just copy and paste code! But instead of copying and pasting from Stack Overflow, I decided to copy and paste from the internal code base.

My task was to create a new widget, so I thought it would be a good idea to take a look at the implementation of other Textual widgets. For some reason I cannot seem to recall, I decided to take a look at the implementation of the button widget that you can find in _button.py. By looking at how the button widget is implemented, I could immediately learn a few useful things about what I needed to do and some other things about how Textual works.

For example, a widget can have a class attribute called DEFAULT_CSS that specifies the default CSS for that widget. I learned this just from staring at the code for the button widget.

Studying the code base will also reveal the standards that are in place. For example, I learned that for a widget with variants (like the button with its \u201csuccess\u201d and \u201cerror\u201d variants), the widget gets a CSS class with the name of the variant prefixed by a dash. You can learn this by looking at the method Button.watch_variant:

class Button(Static, can_focus=True):\n# ...\ndef watch_variant(self, old_variant: str, variant: str):\nself.remove_class(f\"-{old_variant}\")\nself.add_class(f\"-{variant}\")\n

In short, looking at code and files that are related to the things you need to do is a great way to get information about things you didn't even know you needed.

"},{"location":"blog/2022/11/22/what-i-learned-from-my-first-non-trivial-pr/#handling-the-placeholder-variant","title":"Handling the placeholder variant","text":"

A button widget can have a different variant, which is mostly used by Textual to determine the CSS that should apply to the given button. For the placeholder widget, we want the variant to determine what information the placeholder shows. The original GitHub issue mentions 5 variants for the placeholder:

  • a variant that just shows a label or the placeholder ID;
  • a variant that shows the size and location of the placeholder;
  • a variant that shows the state of the placeholder (does it have focus? is the mouse over it?);
  • a variant that shows the CSS that is applied to the placeholder itself; and
  • a variant that shows some text inside the placeholder.

The variant can be assigned when the placeholder is first instantiated, for example, Placeholder(\"css\") would create a placeholder that shows its own CSS. However, we also want to have an on_click handler that cycles through all the possible variants. I was getting ready to reinvent the wheel when I remembered that the standard module itertools has a lovely tool that does exactly what I needed! Thus, all I needed to do was create a new cycle through the variants each time a placeholder is created and then grab the next variant whenever the placeholder is clicked:

class Placeholder(Static):\ndef __init__(\nself,\nvariant: PlaceholderVariant = \"default\",\n*,\nlabel: str | None = None,\nname: str | None = None,\nid: str | None = None,\nclasses: str | None = None,\n) -> None:\n# ...\nself.variant = self.validate_variant(variant)\n# Set a cycle through the variants with the correct starting point.\nself._variants_cycle = cycle(_VALID_PLACEHOLDER_VARIANTS_ORDERED)\nwhile next(self._variants_cycle) != self.variant:\npass\ndef on_click(self) -> None:\n\"\"\"Click handler to cycle through the placeholder variants.\"\"\"\nself.cycle_variant()\ndef cycle_variant(self) -> None:\n\"\"\"Get the next variant in the cycle.\"\"\"\nself.variant = next(self._variants_cycle)\n

I am just happy that I had the insight to add this little while loop when a placeholder is instantiated:

from itertools import cycle\n# ...\nclass Placeholder(Static):\n# ...\ndef __init__(...):\n# ...\nself._variants_cycle = cycle(_VALID_PLACEHOLDER_VARIANTS_ORDERED)\nwhile next(self._variants_cycle) != self.variant:\npass\n

Can you see what would be wrong if this loop wasn't there?

"},{"location":"blog/2022/11/22/what-i-learned-from-my-first-non-trivial-pr/#updating-the-render-of-the-placeholder-on-variant-change","title":"Updating the render of the placeholder on variant change","text":"

If the variant of the placeholder is supposed to determine what information the placeholder shows, then that information must be updated every time the variant of the placeholder changes. Thankfully, Textual has reactive attributes and watcher methods, so all I needed to do was... Defer the problem to another method:

class Placeholder(Static):\n# ...\nvariant = reactive(\"default\")\n# ...\ndef watch_variant(\nself, old_variant: PlaceholderVariant, variant: PlaceholderVariant\n) -> None:\nself.validate_variant(variant)\nself.remove_class(f\"-{old_variant}\")\nself.add_class(f\"-{variant}\")\nself.call_variant_update()  # <-- let this method do the heavy lifting!\n

Doing this properly required some thinking. Not that the current proposed solution is the best possible, but I did think of worse alternatives while I was thinking how to tackle this. I wasn't entirely sure how I would manage the variant-dependant rendering because I am not a fan of huge conditional statements that look like switch statements:

if variant == \"default\":\n# render the default placeholder\nelif variant == \"size\":\n# render the placeholder with its size\nelif variant == \"state\":\n# render the state of the placeholder\nelif variant == \"css\":\n# render the placeholder with its CSS rules\nelif variant == \"text\":\n# render the placeholder with some text inside\n

However, I am a fan of using the built-in getattr and I thought of creating a rendering method for each different variant. Then, all I needed to do was make sure the variant is part of the name of the method so that I can programmatically determine the name of the method that I need to call. This means that the method Placeholder.call_variant_update is just this:

class Placeholder(Static):\n# ...\ndef call_variant_update(self) -> None:\n\"\"\"Calls the appropriate method to update the render of the placeholder.\"\"\"\nupdate_variant_method = getattr(self, f\"_update_{self.variant}_variant\")\nupdate_variant_method()\n

If self.variant is, say, \"size\", then update_variant_method refers to _update_size_variant:

class Placeholder(Static):\n# ...\ndef _update_size_variant(self) -> None:\n\"\"\"Update the placeholder with the size of the placeholder.\"\"\"\nwidth, height = self.size\nself._placeholder_label.update(f\"[b]{width} x {height}[/b]\")\n

This variant \"size\" also interacts with resizing events, so we have to watch out for those:

class Placeholder(Static):\n# ...\ndef on_resize(self, event: events.Resize) -> None:\n\"\"\"Update the placeholder \"size\" variant with the new placeholder size.\"\"\"\nif self.variant == \"size\":\nself._update_size_variant()\n
"},{"location":"blog/2022/11/22/what-i-learned-from-my-first-non-trivial-pr/#deleting-code-is-a-hurtful-blessing","title":"Deleting code is a (hurtful) blessing","text":"

To conclude this blog post, let me muse about the fact that the original issue mentioned five placeholder variants and that my PR only includes two and a half.

After careful consideration and after coming up with the getattr mechanism to update the display of the placeholder according to the active variant, I started showing the \u201cfinal\u201d product to Will and my other colleagues. Eventually, we ended up getting rid of the variant for CSS and the variant that shows the placeholder state. This means that I had to delete part of my code even before it saw the light of day.

On the one hand, deleting those chunks of code made me a bit sad. After all, I had spent quite some time thinking about how to best implement that functionality! But then, it was time to write documentation and tests, and I verified that the best code is the code that you don't even write! The code you don't write is guaranteed to have zero bugs and it also does not need any documentation whatsoever!

So, it was a shame that some lines of code I poured my heart and keyboard into did not get merged into the Textual code base. On the other hand, I am quite grateful that I won't have to fix the bugs that will certainly reveal themselves in a couple of weeks or months from now. Heck, the code hasn't been merged yet and just by writing this blog post I noticed a couple of tweaks that were missing!

"},{"location":"blog/2023/07/29/pull-requests-are-cake-or-puppies/","title":"Pull Requests are cake or puppies","text":"

Broadly speaking, there are two types of contributions you can make to an Open Source project.

The first type is typically a bug fix, but could also be a documentation update, linting fix, or other change which doesn't impact core functionality. Such a contribution is like cake. It's a simple, delicious, gift to the project.

The second type of contribution often comes in the form of a new feature. This contribution likely represents a greater investment of time and effort than a bug fix. It is still a gift to the project, but this contribution is not cake.

A feature PR has far more in common with a puppy. The maintainer(s) may really like the feature but hesitate to merge all the same. They may even reject the contribution entirely. This is because a feature PR requires an ongoing burden to maintain. In the same way that a puppy needs food and walkies, a new feature will require updates and fixes long after the original contribution. Even if it is an amazing feature, the maintainer may not want to commit to that ongoing work.

The chances of a feature being merged can depend on the maturity of the project. At the beginning of a project, a maintainer may be delighted with a new feature contribution. After all, having others join you to build something is the joy of Open Source. And yet when a project gets more mature there may be a growing resistance to adding new features, and a greater risk that a feature PR is rejected or sits unappreciated in the PR queue.

So how should a contributor avoid this? If there is any doubt, it's best to propose the feature to the maintainers before undertaking the work. In all likelihood they will be happy for your contribution, just be prepared for them to say \"thanks but no thanks\". Don't take it as a rejection of your gift: it's just that the maintainer can't commit to taking on a puppy.

There are other ways to contribute code to a project that don't require the code to be merged in to the core. You could publish your change as a third party library. Take it from me: maintainers love it when their project spawns an ecosystem. You could also blog about how you solved your problem without an update to the core project. Having a resource that can be googled for, or a maintainer can direct people to, can be a huge help.

What prompted me to think about this is that my two main projects, Rich and Textual, are at quite different stages in their lifetime. Rich is relatively mature, and I'm unlikely to accept a puppy. If you can achieve what you need without adding to the core library, I am probably going to decline a new feature. Textual is younger and still accepting puppies \u2014 in addition to stick insects, gerbils, capybaras and giraffes.

Tip

If you are maintainer, and you do have to close a feature PR, feel free to link to this post.

Join us on the Discord Server if you want to discuss puppies and other creatures.

"},{"location":"blog/2023/02/15/textual-0110-adds-a-beautiful-markdown-widget/","title":"Textual 0.11.0 adds a beautiful Markdown widget","text":"

We released Textual 0.10.0 25 days ago, which is a little longer than our usual release cycle. What have we been up to?

The headline feature of this release is the enhanced Markdown support. Here's a screenshot of an example:

MarkdownApp \u258bHeader\u00a0level\u00a06\u00a0content. \u25bc\u00a0\u2160\u00a0Textual\u00a0Markdown\u00a0Browser\u00a0-\u00a0Demo\u258b \u251c\u2500\u2500\u00a0\u25bc\u00a0\u2161\u00a0Headers\u258b\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2502\u00a0\u00a0\u00a0\u2514\u2500\u2500\u00a0\u25bc\u00a0\u2162\u00a0This\u00a0is\u00a0H3\u258b\u258e\u258b \u2502\u00a0\u00a0\u00a0\u2514\u2500\u2500\u00a0\u25bc\u00a0\u2163\u00a0This\u00a0is\u00a0H4\u258b\u258eTypography\u258b \u2502\u00a0\u00a0\u00a0\u2514\u2500\u2500\u00a0\u25bc\u00a0\u2164\u00a0This\u00a0is\u00a0H5\u258b\u258e\u258b \u2502\u00a0\u00a0\u00a0\u2514\u2500\u2500\u00a0\u2165\u00a0This\u00a0is\u00a0H6\u258b\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u251c\u2500\u2500\u00a0\u25bc\u00a0\u2161\u00a0Typography\u258bThe\u00a0usual\u00a0Markdown\u00a0typography\u00a0is\u00a0supported.\u00a0The\u00a0exact\u00a0output\u00a0depends\u00a0on\u00a0 \u2502\u00a0\u00a0\u00a0\u2523\u2501\u2501\u00a0\u2162\u00a0Emphasis\u258byour\u00a0terminal,\u00a0although\u00a0most\u00a0are\u00a0fairly\u00a0consistent.\u2581\u2581 \u2502\u00a0\u00a0\u00a0\u2523\u2501\u2501\u00a0\u2162\u00a0Strong\u258b \u2502\u00a0\u00a0\u00a0\u2523\u2501\u2501\u00a0\u2162\u00a0Strikethrough\u258bEmphasis \u2502\u00a0\u00a0\u00a0\u2517\u2501\u2501\u00a0\u2162\u00a0Inline\u00a0code\u258b\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u251c\u2500\u2500\u00a0\u2161\u00a0Fences\u258bEmphasis\u00a0is\u00a0rendered\u00a0with\u00a0*asterisks*,\u00a0and\u00a0looks\u00a0like\u00a0this; \u251c\u2500\u2500\u00a0\u2161\u00a0Quote\u258b \u2514\u2500\u2500\u00a0\u2161\u00a0Tables\u258bStrong \u258b\u2594\u2594\u2594\u2594\u2594\u2594 \u258bUse\u00a0two\u00a0asterisks\u00a0to\u00a0indicate\u00a0strong\u00a0which\u00a0renders\u00a0in\u00a0bold,\u00a0e.g.\u00a0 \u258b**strong**\u00a0render\u00a0strong. \u258b \u258bStrikethrough \u258b\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258bTwo\u00a0tildes\u00a0indicates\u00a0strikethrough,\u00a0e.g.\u00a0~~cross\u00a0out~~\u00a0render\u00a0cross\u00a0out. \u258b\u2582\u2582 \u258bInline\u00a0code \u258b\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258bInline\u00a0code\u00a0is\u00a0indicated\u00a0by\u00a0backticks.\u00a0e.g.\u00a0import\u00a0this. \u258b \u258b\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258b\u258e\u258b \u258b\u258eFences\u258b \u258b\u258e\u258b \u258b\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258bFenced\u00a0code\u00a0blocks\u00a0are\u00a0introduced\u00a0with\u00a0three\u00a0back-ticks\u00a0and\u00a0the\u00a0optional\u00a0 \u258bparser.\u00a0Here\u00a0we\u00a0are\u00a0rendering\u00a0the\u00a0code\u00a0in\u00a0a\u00a0sub-widget\u00a0with\u00a0syntax\u00a0 \u258bhighlighting\u00a0and\u00a0indent\u00a0guides. \u258b \u258bIn\u00a0the\u00a0future\u00a0I\u00a0think\u00a0we\u00a0could\u00a0add\u00a0controls\u00a0to\u00a0export\u00a0the\u00a0code,\u00a0copy\u00a0to\u00a0 \u258bthe\u00a0clipboard.\u00a0Heck,\u00a0even\u00a0run\u00a0it\u00a0and\u00a0show\u00a0the\u00a0output? \u258b \u258b \u258b@lru_cache(maxsize=1024) \u258bdefsplit(self,cut_x:int,cut_y:int)->tuple[Region,Region,Regi \u258b\u2502\u00a0\u00a0\u00a0\"\"\"Split\u00a0a\u00a0region\u00a0in\u00a0to\u00a04\u00a0from\u00a0given\u00a0x\u00a0and\u00a0y\u00a0offsets\u00a0(cuts). \u00a0T\u00a0\u00a0TOC\u00a0\u00a0B\u00a0\u00a0Back\u00a0\u00a0F\u00a0\u00a0Forward\u00a0

Tip

You can generate these SVG screenshots for your app with textual run my_app.py --screenshot 5 which will export a screenshot after 5 seconds.

There are actually 2 new widgets: Markdown for a simple Markdown document, and MarkdownViewer which adds browser-like navigation and a table of contents.

Textual has had support for Markdown since day one by embedding a Rich Markdown object -- which still gives decent results! This new widget adds dynamic controls such as scrollable code fences and tables, in addition to working links.

In future releases we plan on adding more Markdown extensions, and the ability to easily embed custom widgets within the document. I'm sure there are plenty of interesting applications that could be powered by dynamically generated Markdown documents.

"},{"location":"blog/2023/02/15/textual-0110-adds-a-beautiful-markdown-widget/#datatable-improvements","title":"DataTable improvements","text":"

There has been a lot of work on the DataTable API. We've added the ability to sort the data, which required that we introduce the concept of row and column keys. You can now reference rows / columns / cells by their coordinate or by row / column key.

Additionally there are new update_cell and update_cell_at methods to update cells after the data has been populated. Future releases will have more methods to manipulate table data, which will make it a very general purpose (and powerful) widget.

"},{"location":"blog/2023/02/15/textual-0110-adds-a-beautiful-markdown-widget/#tree-control","title":"Tree control","text":"

The Tree widget has grown a few methods to programmatically expand, collapse and toggle tree nodes.

"},{"location":"blog/2023/02/15/textual-0110-adds-a-beautiful-markdown-widget/#breaking-changes","title":"Breaking changes","text":"

There are a few breaking changes in this release. These are mostly naming and import related, which should be easy to fix if you are affected. Here's a few notable examples:

  • Checkbox has been renamed to Switch. This is because we plan to introduce complimentary Checkbox and RadioButton widgets in a future release, but we loved the look of Switches too much to drop them.
  • We've dropped the emit and emit_no_wait methods. These methods posted message to the parent widget, but we found that made it problematic to subclass widgets. In almost all situations you want to replace these with self.post_message (or self.post_message_no_wait).

Be sure to check the CHANGELOG for the full details on potential breaking changes.

"},{"location":"blog/2023/02/15/textual-0110-adds-a-beautiful-markdown-widget/#join-us","title":"Join us!","text":"

We're having fun on our Discord server. Join us there to talk to Textualize developers and share ideas.

"},{"location":"blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/","title":"Textual 0.12.0 adds syntactical sugar and batch updates","text":"

It's been just 9 days since the previous release, but we have a few interesting enhancements to the Textual API to talk about.

"},{"location":"blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/#better-compose","title":"Better compose","text":"

We've added a little syntactical sugar to Textual's compose methods, which aids both readability and editability (that might not be a word).

First, let's look at the old way of building compose methods. This snippet is taken from the textual colors command.

for color_name in ColorSystem.COLOR_NAMES:\nitems: list[Widget] = [ColorLabel(f'\"{color_name}\"')]\nfor level in LEVELS:\ncolor = f\"{color_name}-{level}\" if level else color_name\nitem = ColorItem(\nColorBar(f\"${color}\", classes=\"text label\"),\nColorBar(\"$text-muted\", classes=\"muted\"),\nColorBar(\"$text-disabled\", classes=\"disabled\"),\nclasses=color,\n)\nitems.append(item)\nyield ColorGroup(*items, id=f\"group-{color_name}\")\n

This code composes the following color swatches:

ColorsApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 primary \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b\u2581\u2581 secondary\u258e\"primary\"\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b background\u258e$primary-darken-3$text-muted$text-disabled\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b primary-background\u258e$primary-darken-2$text-muted$text-disabled\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b secondary-background\u258e$primary-darken-1$text-muted$text-disabled\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b surface\u258e$primary$text-muted$text-disabled\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b panel\u258e$primary-lighten-1$text-muted$text-disabled\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b boost\u258e$primary-lighten-2$text-muted$text-disabled\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b warning\u258e$primary-lighten-3$text-muted$text-disabled\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b error\u258e\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 success \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258b accent\u258e\"secondary\"\u258b \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258b \u258e\u258b \u00a0D\u00a0\u00a0Toggle\u00a0dark\u00a0mode\u00a0

Tip

You can see this by running textual colors from the command line.

The old way was not all that bad, but it did make it hard to see the structure of your app at-a-glance, and editing compose methods always felt a little laborious.

Here's the new syntax, which uses context managers to add children to containers:

for color_name in ColorSystem.COLOR_NAMES:\nwith ColorGroup(id=f\"group-{color_name}\"):\nyield Label(f'\"{color_name}\"')\nfor level in LEVELS:\ncolor = f\"{color_name}-{level}\" if level else color_name\nwith ColorItem(classes=color):\nyield ColorBar(f\"${color}\", classes=\"text label\")\nyield ColorBar(\"$text-muted\", classes=\"muted\")\nyield ColorBar(\"$text-disabled\", classes=\"disabled\")\n

The context manager approach generally results in fewer lines of code, and presents attributes on the same line as containers themselves. Additionally, adding widgets to a container can be as simple is indenting them.

You can still construct widgets and containers with positional arguments, but this new syntax is preferred. It's not documented yet, but you can start using it now. We will be updating our examples in the next few weeks.

"},{"location":"blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/#batch-updates","title":"Batch updates","text":"

Textual is smart about performing updates to the screen. When you make a change that might repaint the screen, those changes don't happen immediately. Textual makes a note of them, and repaints the screen a short time later (around a 1/60th of a second). Multiple updates are combined so that Textual does less work overall, and there is none of the flicker you might get with multiple repaints.

Although this works very well, it is possible to introduce a little flicker if you make changes across multiple widgets. And especially if you add or remove many widgets at once. To combat this we have added a batch_update context manager which tells Textual to disable screen updates until the end of the with block.

The new Markdown widget uses this context manager when it updates its content. Here's the code:

with self.app.batch_update():\nawait self.query(\"MarkdownBlock\").remove()\nawait self.mount_all(output)\n

Without the batch update there are a few frames where the old markdown blocks are removed and the new blocks are added (which would be perceived as a brief flicker). With the update, the update appears instant.

"},{"location":"blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/#disabled-widgets","title":"Disabled widgets","text":"

A few widgets (such as Button) had a disabled attribute which would fade the widget a little and make it unselectable. We've extended this to all widgets. Although it is particularly applicable to input controls, anything may be disabled. Disabling a container makes its children disabled, so you could use this for disabling a form, for example.

Tip

Disabled widgets may be styled with the :disabled CSS pseudo-selector.

"},{"location":"blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/#preventing-messages","title":"Preventing messages","text":"

Also in this release is another context manager, which will disable specified Message types. This doesn't come up as a requirement very often, but it can be very useful when it does. This one is documented, see Preventing events for details.

"},{"location":"blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/#full-changelog","title":"Full changelog","text":"

As always see the release page for additional changes and bug fixes.

"},{"location":"blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/#join-us","title":"Join us!","text":"

We're having fun on our Discord server. Join us there to talk to Textualize developers and share ideas.

"},{"location":"blog/2023/03/09/textual-0140-shakes-up-posting-messages/","title":"Textual 0.14.0 shakes up posting messages","text":"

Textual version 0.14.0 has landed just a week after 0.13.0.

Note

We like fast releases for Textual. Fast releases means quicker feedback, which means better code.

What's new?

We did a little shake-up of posting messages which will simplify building widgets. But this does mean a few breaking changes.

There are two methods in Textual to post messages: post_message and post_message_no_wait. The former was asynchronous (you needed to await it), and the latter was a regular method call. These two methods have been replaced with a single post_message method.

To upgrade your project to Textual 0.14.0, you will need to do the following:

  • Remove await keywords from any calls to post_message.
  • Replace any calls to post_message_no_wait with post_message.

Additionally, we've simplified constructing messages classes. Previously all messages required a sender argument, which had to be manually set. This was a clear violation of our \"no boilerplate\" policy, and has been dropped. There is still a sender property on messages / events, but it is set automatically.

So prior to 0.14.0 you might have posted messages like the following:

await self.post_message(self.Changed(self, item=self.item))\n

You can now replace it with this simpler function call:

self.post_message(self.Change(item=self.item))\n

This also means that you will need to drop the sender from any custom messages you have created.

If this was code pre-0.14.0:

class MyWidget(Widget):\nclass Changed(Message):\n\"\"\"My widget change event.\"\"\"\ndef __init__(self, sender:MessageTarget, item_index:int) -> None:\nself.item_index = item_index\nsuper().__init__(sender)\n

You would need to make the following change (dropping sender).

class MyWidget(Widget):\nclass Changed(Message):\n\"\"\"My widget change event.\"\"\"\ndef __init__(self, item_index:int) -> None:\nself.item_index = item_index\nsuper().__init__()\n

If you have any problems upgrading, join our Discord server, we would be happy to help.

See the release notes for the full details on this update.

"},{"location":"blog/2023/03/13/textual-0150-adds-a-tabs-widget/","title":"Textual 0.15.0 adds a tabs widget","text":"

We've just pushed Textual 0.15.0, only 4 days after the previous version. That's a little faster than our typical release cadence of 1 to 2 weeks.

What's new in this release?

The highlight of this release is a new Tabs widget to display tabs which can be navigated much like tabs in a browser. Here's a screenshot:

TabsApp Paul\u00a0AtreidiesDuke\u00a0Leto\u00a0AtreidesLady\u00a0JessicaGurney\u00a0Halleck \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258aLady\u00a0Jessica\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u00a0A\u00a0\u00a0Add\u00a0tab\u00a0\u00a0R\u00a0\u00a0Remove\u00a0active\u00a0tab\u00a0\u00a0C\u00a0\u00a0Clear\u00a0tabs\u00a0

In a future release, this will be combined with the ContentSwitcher widget to create a traditional tabbed dialog. Although Tabs is still useful as a standalone widgets.

Tip

I like to tweet progress with widgets on Twitter. See the #textualtabs hashtag which documents progress on this widget.

Also in this release is a new LoadingIndicator widget to display a simple animation while waiting for data. Here's a screenshot:

LoadingApp \u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf

As always, see the release notes for the full details on this update.

If you want to talk about these widgets, or anything else Textual related, join us on our Discord server.

"},{"location":"blog/2023/03/22/textual-0160-adds-tabbedcontent-and-border-titles/","title":"Textual 0.16.0 adds TabbedContent and border titles","text":"

Textual 0.16.0 lands 9 days after the previous release. We have some new features to show you.

There are two highlights in this release. In no particular order, the first is TabbedContent which uses a row of tabs to navigate content. You will have likely encountered this UI in the desktop and web. I think in Windows they are known as \"Tabbed Dialogs\".

This widget combines existing Tabs and ContentSwitcher widgets and adds an expressive interface for composing. Here's a trivial example to use content tabs to navigate a set of three markdown documents:

def compose(self) -> ComposeResult:\nwith TabbedContent(\"Leto\", \"Jessica\", \"Paul\"):\nyield Markdown(LETO)\nyield Markdown(JESSICA)\nyield Markdown(PAUL)\n

Here's an example of the UI you can create with this widget (note the nesting)!

TabbedApp LetoJessicaPaul \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eLady\u00a0Jessica\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Bene\u00a0Gesserit\u00a0and\u00a0concubine\u00a0of\u00a0Leto,\u00a0and\u00a0mother\u00a0of\u00a0Paul\u00a0and\u00a0Alia. PaulAlia \u2501\u2578\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 First\u00a0child \u00a0L\u00a0\u00a0Leto\u00a0\u00a0J\u00a0\u00a0Jessica\u00a0\u00a0P\u00a0\u00a0Paul\u00a0

"},{"location":"blog/2023/03/22/textual-0160-adds-tabbedcontent-and-border-titles/#border-titles","title":"Border titles","text":"

The second highlight is a frequently requested feature (FRF?). Widgets now have the two new string properties, border_title and border_subtitle, which will be displayed within the widget's border.

You can set the alignment of these titles via border-title-align and border-subtitle-align. Titles may contain Console Markup, so you can add additional color and style to the labels.

Here's an example of a widget with a title:

BorderApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 ascii \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 none \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550double\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557 hidden\u2551\u2551 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2551\u2551 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2551I\u00a0must\u00a0not\u00a0fear.\u2551 blank\u2551Fear\u00a0is\u00a0the\u00a0mind-killer.\u2551 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2551Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2551 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2551I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2551 round\u2551I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2551 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2551And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0\u2551 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2551path.\u2551 solid\u2586\u2586\u2551Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0\u2551 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2551remain.\u2551 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2551\u2551 double\u2551\u2551 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 dashed \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 heavy \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

BTW the above is a command you can run to see the various border styles you can apply to widgets.

textual borders\n
"},{"location":"blog/2023/03/22/textual-0160-adds-tabbedcontent-and-border-titles/#container-changes","title":"Container changes","text":"

Breaking change

If you have an app that uses any container classes, you should read this section.

We've made a change to containers in this release. Previously all containers had auto scrollbars, which means that any container would scroll if its children didn't fit. With nested layouts, it could be tricky to understand exactly which containers were scrolling. In 0.16.0 we split containers in to scrolling and non-scrolling versions. So Horizontal will now not scroll by default, but HorizontalScroll will have automatic scrollbars.

"},{"location":"blog/2023/03/22/textual-0160-adds-tabbedcontent-and-border-titles/#what-else","title":"What else?","text":"

As always, see the release notes for the full details on this update.

If you want to talk about this update or anything else Textual related, join us on our Discord server.

"},{"location":"blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/","title":"Textual 0.17.0 adds translucent screens and Option List","text":"

This is a surprisingly large release, given it has been just 7 days since the last version (and we were down a developer for most of that time).

What's new in this release?

There are two new notable features I want to cover. The first is a compositor effect.

"},{"location":"blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/#translucent-screens","title":"Translucent screens","text":"

Textual has a concept of \"screens\" which you can think of as independent UI modes, each with their own user interface and logic. The App class keeps a stack of these screens so you can switch to a new screen and later return to the previous screen.

Screens

See the guide to learn more about the screens API.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN1cXOtT20hcdTAwMTL/nr+C4r7sVcWz0z3vrbq6XHUwMDAyXHUwMDEyXHUwMDEyQnhsyObB3VZK2MLW4ddaMsZs5X+/XHUwMDFlhSDJQorBxnHiXHUwMDBmXHUwMDE4a+RRa+bX3b9+yH8/2djYTKbDcPO3jc3wqlx1MDAxOXSj1iiYbD71xy/DUVx1MDAxY1xy+jSE6ed4MFx1MDAxZTXTMztJMox/+/XXXjC6XGKTYTdohuwyisdBN07GrWjAmoPer1FcdTAwMTL24n/7v4dBL/zXcNBrJSOWXaRcdTAwMTG2omQw+nKtsFx1MDAxYvbCflx1MDAxMtPs/6HPXHUwMDFiXHUwMDFif6d/c9K1oqA36LfS09OBnHhazFx1MDAxZT1cdTAwMWP0U1FBXHUwMDBiLZW28vaEKH5GXHUwMDE3S8JcdTAwMTaNnpPAYTbiXHUwMDBmbcrRNNFcdTAwMDae897Fx119cmhcdTAwMGZOxyfZVc+jbvckmXZTmeJcdTAwMDHdSjZcdTAwMTYno8FF+D5qJVx1MDAxZH/pmeNV31x1MDAxYVxyxu1OP4zjwndcdTAwMDbDoFx1MDAxOSVTOqb47cGg306nyI5cXNGnXHUwMDA2cs6M0Vx1MDAxNqTiXHUwMDEy6G7V7fiXXHRcdTAwMDQz1lx1MDAxOFx1MDAwNUJcdTAwMWEhXHUwMDA1qFx1MDAxOcl2XHUwMDA2XdpcdTAwMDeS7Fx1MDAxZjx9ZbKdXHUwMDA1zYs2XHTYb2XngFxugrPz7JzJzf1Kp5i0Uphs+k5cdTAwMTi1O4nfIauZNcBdfjRcdTAwMGXTTXCgpJNaZlvkrzjca6Vg+HN2XHUwMDE1O8FoeLNam7H/kJPWXHUwMDBi+nxcdTAwMTZJeTTl9lm8grPdXHUwMDEwYKe1v/3X85NcdTAwMDP5+2CrfztXXHUwMDAxesFoNJhs3o58vvkvXHUwMDEzbTxsXHUwMDA1X1x1MDAxMFx1MDAwNVpLa43TXHUwMDEyTVx1MDAwNspu1L+gwf64282OXHKaXHUwMDE3XHUwMDE5XGLTo5+f3lx1MDAxYvp0mSroo+OO0KD03NBcdTAwMGbHU3ux39vnfHz+ctLeiyb6hfue0Fx1MDAwN/5N7IPTTFx1MDAxOSNRc1x1MDAwZVx1MDAwMoyyXHUwMDA17EuBXGalQYKeddo4vlx1MDAxOPbPgzPO1Vx1MDAxMrGPQipwlq9cdTAwMTb7vd45n2zx5NlhNFxmwz9eXHUwMDFlbb86iJeEfVx1MDAwYlxccG6Whf0kvEruXHUwMDAyvkVdXHUwMDA1fFx1MDAxMNZx5NLh3Mh/d941l1fDy5fT3taHwfjj8PiF2F1v5CMqprRBXHUwMDA0dEZ6XHUwMDBiWlx1MDAwML7lwMhcdTAwMDRJclxi1iHkrMBDcG+c4udYxj1wW1x1MDAwNryBWZhrgdL7pp/IxDtcdTAwMGJK2PvAPEPToJ+cRNepjbaFo7tBL+pOXHUwMDBikEjxT1x1MDAwMp40R2HY34D/9n/pRK1W2P9nfsfikK7vJ9TFb251o7bXls1ueF5UoyRcIlx1MDAxZXY7nFxmcmvcJElcdTAwMDKabrTXmr2jwShqR/2g+7Zaqlpt/rLMd6gzUVx1MDAxM5w9nNNnIHojxPz6XFy/8/fQZ5zF5uPps3HMSFx1MDAwMGm4pXdbVGfjJFx1MDAwM81cdTAwMWRcdTAwMWGU6EjjXHUwMDFmRZ1cdTAwMWQyrogwS2MsR9TuXHUwMDBl5XZMIzk6KVFcdTAwMDHXOlx1MDAwM/BXlyaFv4VcdTAwMDeoeipkjao/ijLGSTBKtqN+K+q3aTCzXCJfQ5K9OVx1MDAxY0Sqvs2xl5IzhVxcS8FpI5UgL1x1MDAwNLmT2sHQLyFcdTAwMDMgTqLJZKNVTtibXHUwMDEzPt9cblx1MDAxNfZb31x1MDAxNqk+UMmJ1OBcZml5nPV7pjhqilx1MDAxM0pCSaZcdTAwMDVIXHUwMDA3nITiTlmnSlJ1gzjZXHUwMDE59HpRQmt/PIj6yexcdTAwMWGni7nldbxcdTAwMTNcdTAwMDYl40F3lVx1MDAxZps1XHUwMDA2Qz9j0aZn/21k2pJ+uP3/z6d3nt2ohHI6WkJxNt+T/PtcdTAwMDNcdTAwMTi5sJWGXGYtqVx1MDAwNlx1MDAxMZdM8b/JyM9G2NpcdTAwMGKuw/1nW1x1MDAwN89f9LWNIVhvXlwinGOOTFx1MDAxNVxiNN5y68xcdTAwMTJ8XHRGLeNcdTAwMDLAOeK+ZCaEmJHsIcGo1suj5ESVkKJcYs5cdTAwMWaBrNRYMECFuIqIkYLtSkeLZLhcdTAwMDVwNb+jPdzpXHUwMDFjXGZfXHUwMDFjvdtcdTAwMWRcdTAwMWNfj9xhcjQ+XHUwMDFmivVcdTAwMDao5IJcdJ9cdFGWdFWqYrJEXG7NuOPGXHUwMDEyQqVcdTAwMDZcXFxmnsuOXHUwMDE3SVx1MDAxZaE4mTS7fHDWMenXZ8rufYyvOsPx+MNWdPomuVx1MDAxOMllXHUwMDA1jIQ3yHG7x4O+XHUwMDE1qlxu+k6AUkS65jfN76eHb9o9t3+0r/76NH32abtzKI+WivxWXHUwMDEwd8IlQ98xolx1MDAxZYJcdTAwMTONXHUwMDA0YpGuXHUwMDAwfaGQXHUwMDExKZFcdTAwMDY5cDBcXC9GMi02XHUwMDFkhGqZ6CdcdTAwMTNpSTS+4nSJXHLDl+PX19fds1P96VxcJJGYnvL50P+0bt5Y7U+v4+3jyejgdO/Vofj0x9SeLWHe8+H7ydvG5Ni+v+7Fp1x1MDAxN83wI3ZcdTAwMGaWMC9cdTAwMWab//VO3mLyprl9XHUwMDE1NT/uvlx1MDAxMc1oWfE0J+S5pTnAqrSRrktcdTAwMWKRWjiKvOzcNuAsnuKOONpcdTAwMDXV3u6Mmq33h+r6dL3DTGlcdTAwMWSzSkvNOZleNZMuXHUwMDA1QaNOcrKERJ3JXHUwMDE3zlxudj9cdTAwMTNA6npcdTAwMTbewc1ErqZxq/myrO9CWqIrhj+Ct6tBolx1MDAwMan1fZCYbXiW2Vx1MDAxMVx1MDAxNOWlgYfnwDZcdTAwMTdJXHUwMDE38jzZVb7meYLhkFxyXHUwMDA3w09xmln55e4sj9CF7z12lqckU63qVeZ4dHVkROxXXHUwMDEz71Zyft2rN57L0L1H8L8gmERUXHUwMDE0ZiOZOzFTq7CGIVx1MDAxN9pcYqPJXHUwMDE3w2Ip2yrd40RvXHUwMDAxkVgwXHUwMDAx06E1QpZ1XHUwMDExuPGCmpRvXHUwMDAyXHUwMDE5XHUwMDA0V9ZNQjUpTC7JvopEXHUwMDBmcFqcx0z01NO6jUJWxVEsXHUwMDBmjmyoRE2rmUsx3CRVXHUwMDE0U1x1MDAxNFx1MDAwMyuOdFx1MDAwMtpvZnqKt/EjZVtqQJWOl/GUTfkk/35vo2JyRYVZo0JwXHUwMDAx8iD3yFx1MDAxYtczp/U0Ko5LZi1ZXGZDsaySZtaoXGJmXHUwMDA0V4pWnlxmXHUwMDBizEZcdTAwMWLLMipoOWghfSmfLpIrRuVsimWSglx1MDAwZetcdTAwMWNcdTAwMTiCXG6WykSgjZXOc5PV2lx1MDAxNKCYXCJbte9oU4DRXHUwMDBl0PIopykqJlx1MDAxZVJOXHUwMDFlXHUwMDAzZ8QmrNTkQoThXFyan9SmVEPKv1x1MDAxYWU0LcuiXHUwMDE08l+zXHUwMDE5XFyurZS+nDm3SalPnaynSdHKMCO55lx1MDAxMiVIk+tk8d/XXHUwMDAyXHUwMDE4+X3iMd6x4YItXHUwMDE1VSaFdMFcdTAwMTkjyWhJWnXSiez+b02KU1xmpXbKcsFccqrcrtxYXHUwMDE0dPRdhfpcdTAwMDFcdTAwMDHEQiSFK5fJ8nCDMqt8P4FaN6r3NVx1MDAxZC5t6T3Vuq5XSldTXHUwMDA1Tv5cdTAwMTM06vmpgn6m8FX0cnLZfvfhQrevT49+j79rn+C31ZrIMzBcbj6QTCatr5lpXHUwMDE5UUAkTSluXHUwMDFkUSaXL8ivR2VGeTOvVptcZlhdJ59cdTAwMTbVXlx1MDAwN4Rwvk1s/ui4cbU/tn+1wsvOycdLXHUwMDExTl87PNhZe3Rq5uNcdTAwMDdB2DMobLFwSG6XkS9cIuRcbq2EQbdcdTAwMTA6l16YcUIgUTfzgHB4kdR0I57s7CB2I9lcdTAwMTZcdTAwMWZa4z+O48nFslKyVlpuga9cdTAwMDD7NuegS1xy3IAkjJyfcMX93dFe7+L5azHF8L1otTvDg9Z6Q79cdTAwMDHWMrCKmKXTzmlcdTAwMGVcdTAwMDXoXHUwMDBiKYlcZiuL3KLv6l1cYvlfyjLLLElcdTAwMTJHXHUwMDA0XHUwMDEy7CcpypxtPT9/XHUwMDFmbPHD9rv9t1x1MDAwN83rQZDE46X1xqK0uDSNqlxmYWpcbp2gfHVbajN/W3h92Wd9I1x1MDAxOFxupYVcdTAwMDRSXHUwMDFiXHUwMDAxekafiOiQWfHxi6b9WKzOWVx1MDAxZMBcdTAwMThFXHUwMDExlKPolYiLJZ9VVizincxnZZRcIq/mfLmjpF5cdTAwMTTqy2Jss5pcdTAwMTCG1P1BNZBl50Q448AlxXBKXHUwMDE5jeA4N3d2r/neNk3RKsWlZJBuTvjZklwijWpQfVx1MDAxOS7hKZvxSf79vnVTqWD26C071cjR3ec5k6vXoVx1MDAxY530XHUwMDFiz/u6od1+6/CV6thcboPSXHSanfEoXFxcdTAwMDNcdTAwMWZN+GJcdTAwMDZ89yTB0T/jUEy0XHUwMDFhJ1x1MDAxOHF0X1x1MDAwZVx1MDAxMMpavlD1Jlx1MDAxOVx1MDAwNf14XHUwMDE4jEhd7jAsucxpTdO9XHUwMDE0qJw1YsWM9DGfLfHZXHUwMDAy41bedI9r2XSPizfdc1fdXHJBtsOSf7zHk5P1O38vtV5dP0SDXHUwMDAySlx1MDAwNspYIynYXHUwMDE3zuqZxnuLjLyhptPQmVxccWXpWlxyilx0XHUwMDAwKYiekW1x7q5cdTAwMTJcblx1MDAwMpM+XHLCLYVMoPJcdTAwMWRaN3TBu1x1MDAwN4fwkCzJXCJ0gTyzclx1MDAwZtHLOelCvcvYKDa7k+8z5CONTOvo5bIscEaLJITl3JdR9Ndi5D1cdTAwMWLw61x1MDAxZpcsUFx1MDAxOFx1MDAxMCCNpZ3zXHUwMDE5XHUwMDAyJY0uyWRcdTAwMTjSgCR648jGXHUwMDE5xJJMP1x1MDAxMk+pXHUwMDA2s381yjheXHUwMDE2TVx1MDAxMZWJXHUwMDA0XHUwMDA0n2+mUHV+nlwiPlxcyNa1fHn54tnhm9Z04sK+qirdrFx1MDAwZk9cdTAwMDHBmSFoI7FD32ePRYOGqH1cdTAwMDXRKCAuY6VSXHUwMDBipVx1MDAxM75BVO5o8ypcdTAwMTNcdTAwMTWyXHUwMDFjUlx0IdWPw1Se1s37mFx1MDAxOVx1MDAwNLK0Wq+eXHUwMDAxXHTiXHUwMDFhl1FcdTAwMWOddcN1okBcdTAwMDWxXHUwMDFlxoFcdTAwMTRWXHUwMDA2NuAsoZK4/vxN4fVbv65cdTAwMTTIP/IglFTeLKBRYqYtXHUwMDFjkFx0Ylx1MDAxNVx1MDAxNoUherTYXHUwMDAzO7X2XHUwMDAyXHUwMDFkOUokj01Ow3IpsyvdWlx1MDAwZu3du/TJXHUwMDAwXHS0MVjOl/jSXHUwMDAxRaSr7iF5sFrOSYDqfVGRXHUwMDAwgdCeXHUwMDA3Ulx1MDAxNFxuXHUwMDE2yeHlclx1MDAwNF9cdTAwMTmQZNJvJaBcdTAwMTJcdTAwMWOMeegziPW59qJUXFxcbmPJISljtNEu94NcdTAwMWa3YllmNHFcdTAwMDPaWue4MkKWpPqRSFAlnP2rXHUwMDA05CUxIFGdqCFhUEtcdTAwMGbXue3ZyfXlKzWNj6ZHL+LB5OPWZft479O6MyCK1ohcdTAwMDGh4MSCjK9ZXHUwMDE0XHUwMDEzNUJcdTAwMThGXHUwMDExgnRElFxibvlfUPlOmVx1MDAxYSBcdTAwMWHmIFx1MDAxN1x1MDAxNfzotUPHXHUwMDA1t8LlbmiFmZo15Cm4OE8xrlqvwVx1MDAxN1xyrTXzN6/Ub/2a8lx1MDAxNFxuKsH/mFx1MDAwZjk3w41cdTAwMTC5eCFtXHUwMDEwXHUwMDAwzdAnckxcdTAwMWHai8VcdTAwMWXdrNVr7dtotPDP9/tcdTAwMDdcdTAwMDfhXHUwMDBlLdeGOeu7XHUwMDE2NddWlVx1MDAxYdP8U2zKoV3l7yQspJVz0pR6h7FRKOs4RS9OXHUwMDFiXHUwMDA1WphcXGfyRpZcdTAwMTORXGLCao1cdTAwMWH8XHUwMDEzYuWfJJiLpNT3wszIRFxcSCvyyEJq8lx1MDAxZqIkXHUwMDEzkmfx7Wv+4XY0d7X0/0hcdTAwMTSlXHUwMDEyyOlgXHUwMDExwlVcdTAwMDTlyc3s/jGhk4TwdrtcdTAwMTVcdTAwMDTpqHVjyrNb3LyMwsn2XT056cvbx3QxvVx1MDAxNVxu/Y3+/fnJ5/9cdTAwMDPV4pXXIn0= Screen 1(hidden)app.pop_screen()Screen 2(hidden)Screen 3(visible)Screen 2(visible)

Screens can be used to build modal dialogs by pushing a screen with controls / buttons, and popping the screen when the user has finished with it. The problem with this approach is that there was nothing to indicate to the user that the original screen was still there, and could be returned to.

In this release we have added alpha support to the Screen's background color which allows the screen underneath to show through, typically blended with a little color. Applying this to a screen makes it clear than the user can return to the previous screen when they have finished interacting with the modal.

Here's how you can enable this effect with CSS:

DialogScreen {\nalign: center middle;\nbackground: $primary 30%;\n}\n

Setting the background to $primary will make the background blue (with the default theme). The addition of 30% sets the alpha so that it will be blended with the background. Here's the kind of effect this creates:

DialogApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u2588\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2588 Fear\u00a0is\u00a0the\u00a0mind-killer\u2588\u2588 Fear\u00a0is\u00a0the\u00a0little-deat\u2588\u2588 I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2588\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2588 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pas\u2588\u2588 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0pa\u2588Good\u00a0for\u00a0natural\u00a0breaks\u00a0in\u00a0the\u00a0content,\u00a0that\u00a0don't\u00a0require\u00a0another\u2588 Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u2588header.\u2588 Fear\u00a0is\u00a0the\u00a0mind-killer\u2588\u2588\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2588 Fear\u00a0is\u00a0the\u00a0little-deat\u2588\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2588\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2588 I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2588\u258e\u258b\u2588\u258b\u2588 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pas\u2588\u258eLists\u258b\u2588\u258b\u2588 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0pa\u2588\u258e\u258b\u2582\u2582\u2588\u258b\u2588 Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u2588\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2588\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2582\u2582\u2588 Fear\u00a0is\u00a0the\u00a0mind-killer\u2588\u00a01.\u00a0Lists\u00a0can\u00a0be\u00a0ordered\u2588down\u00a0widgets.\u2588 Fear\u00a0is\u00a0the\u00a0little-deat\u2588\u00a02.\u00a0Lists\u00a0can\u00a0be\u00a0unordered\u2588\u2588 I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2588\u25cf\u00a0I\u00a0must\u00a0not\u00a0fear.\u2588\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2588 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pas\u2588\u25aa\u00a0Fear\u00a0is\u00a0the\u00a0mind-killer.\u2584\u2584\u2588\u258b\u2588 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0pa\u2588\u2023\u00a0Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2588\u258b\u2588\u2580\u2580\u2580\u2580\u2588 Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u2588\u2022\u00a0I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2588\u258b\u2588\u2588 Fear\u00a0is\u00a0the\u00a0mind-killer\u2588\u2b51\u00a0I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2588\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2588\u2588 Fear\u00a0is\u00a0the\u00a0little-deat\u2588\u25aa\u00a0And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0\u2588\u2588\u2588 I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2588see\u00a0its\u00a0path.\u2588\u2588\u2588 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pas\u2588\u25cf\u00a0Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0\u2588\u2588\u2582\u2582\u2588 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0pa\u2588remain.\u2588\u2588\u2588 Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u2588\u2588\u2588\u2588 Fear\u00a0is\u00a0the\u00a0mind-killer\u2588Longer\u00a0list\u2588\u2588\u2588 Fear\u00a0is\u00a0the\u00a0little-deat\u2588\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2588\u2588\u2588 I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2588\u00a0\u00a01.\u00a0Duke\u00a0Leto\u00a0I\u00a0Atreides,\u00a0head\u00a0of\u00a0House\u00a0Atreides\u2588\u2588\u2588 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pas\u2588\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2588\u00a0headings.\u2588\u2588 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u2588\u2588\u2588 Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0O\u2588This\u00a0is\u00a0H5\u2588\u2588 Fear\u00a0is\u00a0the\u00a0mind-killer.\u2588\u2588\u2588 Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0oblit\u2588Header\u00a0level\u00a05\u00a0content.\u2588\u2588 I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2588\u2588\u2588 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2588This\u00a0is\u00a0H6\u2588\u2588 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u2588\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2588\u2588 Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u2588This\u00a0is\u00a0H4\u2588 Fear\u00a0is\u00a0the\u00a0mind-killer.\u2588\u2588 Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliterati\u2588Header\u00a0level\u00a04\u00a0content.\u00a0Drilling\u00a0down\u00a0in\u00a0to\u00a0finer\u00a0headings.\u2588 I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2588\u2588 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2588This\u00a0is\u00a0H5\u2588 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0\u2588\u2588 Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u2588Header\u00a0level\u00a05\u00a0content.\u2588 Fear\u00a0is\u00a0the\u00a0mind-killer.\u2588\u2588 Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliterati\u2588This\u00a0is\u00a0H6\u2588 I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2588\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2588 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer.

There are 4 screens in the above screenshot, one for the base screen and one for each of the three dialogs. Note how each screen modifies the color of the screen below, but leaves everything visible.

See the docs on screen opacity if you want to add this to your apps.

"},{"location":"blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/#option-list","title":"Option list","text":"

Textual has had a ListView widget for a while, which is an excellent way of navigating a list of items (actually other widgets). In this release we've added an OptionList which is similar in appearance, but uses the line api under the hood. The Line API makes it more efficient when you approach thousands of items.

OptionListApp \u2b58OptionListApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aAerilon\u258e \u258aAquaria\u258e \u258aCanceron\u258e \u258aCaprica\u258e \u258aGemenon\u258e \u258aLeonis\u258e \u258aLibran\u258e \u258aPicon\u258e \u258aSagittaron\u258e \u258aScorpia\u258e \u258aTauron\u258e \u258aVirgon\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

The Options List accepts Rich renderable, which means that anything Rich can render may be displayed in a list. Here's an Option List of tables:

OptionListApp \u2b58OptionListApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Data\u00a0for\u00a0Aerilon\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258e \u258a\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u258e \u258a\u2503Patron\u00a0God\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Population\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Capital\u00a0City\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503\u258e \u258a\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\u258e \u258a\u2502Demeter\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u25021.2\u00a0Billion\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502Gaoth\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u258e \u258a\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u258e \u258a\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Data\u00a0for\u00a0Aquaria\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2582\u2582\u258e \u258a\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u258e \u258a\u2503Patron\u00a0God\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Population\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Capital\u00a0City\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503\u258e \u258a\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\u258e \u258a\u2502Hermes\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250275,000\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502None\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u258e \u258a\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u258e \u258a\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Data\u00a0for\u00a0Canceron\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258e \u258a\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u258e \u258a\u2503Patron\u00a0God\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Population\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Capital\u00a0City\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503\u258e \u258a\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\u258e \u258a\u2502Hephaestus\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u25026.7\u00a0Billion\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502Hades\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u258e \u258a\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u258e \u258a\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Data\u00a0for\u00a0Caprica\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258e \u258a\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u258e \u258a\u2503Patron\u00a0God\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Population\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Capital\u00a0City\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503\u258e \u258a\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

We plan to build on the OptionList widget to implement drop-downs, menus, check lists, etc. But it is still very useful as it is, and you can add it to apps now.

"},{"location":"blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/#what-else","title":"What else?","text":"

There are a number of fixes regarding refreshing in this release. If you had issues with parts of the screen not updating, the new version should resolve it.

There's also a new logging handler, and a \"thick\" border type.

See release notes for the full details.

"},{"location":"blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/#next-week","title":"Next week","text":"

Next week we plan to take a break from building Textual to building apps with Textual. We do this now and again to give us an opportunity to step back and understand things from the perspective of a developer using Textual. We will hopefully have something interesting to show from the exercise, and new Open Source apps to share.

"},{"location":"blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/#join-us","title":"Join us","text":"

If you want to talk about this update or anything else Textual related, join us on our Discord server.

"},{"location":"blog/2023/04/04/textual-0180-adds-api-for-managing-concurrent-workers/","title":"Textual 0.18.0 adds API for managing concurrent workers","text":"

Less than a week since the last release, and we have a new API to show you.

This release adds a new Worker API designed to manage concurrency, both asyncio tasks and threads.

An API to manage concurrency may seem like a strange addition to a library for building user interfaces, but on reflection it makes a lot of sense. People are building Textual apps to interface with REST APIs, websockets, and processes; and they are running into predictable issues. These aren't specifically Textual problems, but rather general problems related to async tasks and threads. It's not enough for us to point users at the asyncio docs, we needed a better answer.

The new run_worker method provides an easy way of launching \"Workers\" (a wrapper over async tasks and threads) which also manages their lifetime.

One of the challenges I've found with tasks and threads is ensuring that they are shut down in an orderly manner. Interestingly enough, Textual already implemented an orderly shutdown procedure to close the tasks that power widgets: children are shut down before parents, all the way up to the App (the root node). The new API piggybacks on to that existing mechanism to ensure that worker tasks are also shut down in the same order.

Tip

You won't need to worry about this gnarly issue with the new Worker API.

I'm particularly pleased with the new @work decorator which can turn a coroutine OR a regular function into a Textual Worker object, by scheduling it as either an asyncio task or a thread. I suspect this will solve 90% of the concurrency issues we see with Textual apps.

See the Worker API for the details.

"},{"location":"blog/2023/04/04/textual-0180-adds-api-for-managing-concurrent-workers/#join-us","title":"Join us","text":"

If you want to talk about this update or anything else Textual related, join us on our Discord server.

"},{"location":"blog/2023/05/03/textual-0230-improves-message-handling/","title":"Textual 0.23.0 improves message handling","text":"

It's been a busy couple of weeks at Textualize. We've been building apps with Textual, as part of our dog-fooding week. The first app, Frogmouth, was released at the weekend and already has 1K GitHub stars! Expect two more such apps this month.

Frogmouth /Users/willmcgugan/projects/textual/FAQ.md ContentsLocalBookmarksHistory\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u258e\u258a \u258eHow\u00a0do\u00a0I\u00a0pass\u00a0arguments\u00a0to\u00a0an\u00a0app?\u258a \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u258e\u258a \u2503\u25bc\u00a0\u2160\u00a0Frequently\u00a0Asked\u00a0Questions\u2503\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0Does\u00a0Textual\u00a0support\u00a0images?\u2503When\u00a0creating\u00a0your\u00a0App\u00a0class,\u00a0override\u00a0__init__\u00a0as\u00a0you\u00a0would\u00a0wheninheriting\u00a0normally.\u00a0For\u00a0example: \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0can\u00a0I\u00a0fix\u00a0ImportError\u00a0cannot\u00a0i\u2503 \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0can\u00a0I\u00a0select\u00a0and\u00a0copy\u00a0text\u00a0in\u00a0\u2503 \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0can\u00a0I\u00a0set\u00a0a\u00a0translucent\u00a0app\u00a0ba\u2503fromtextual.appimportApp,ComposeResult \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0do\u00a0I\u00a0center\u00a0a\u00a0widget\u00a0in\u00a0a\u00a0scre\u2503fromtextual.widgetsimportStatic \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0do\u00a0I\u00a0pass\u00a0arguments\u00a0to\u00a0an\u00a0app?\u2503 \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0Why\u00a0do\u00a0some\u00a0key\u00a0combinations\u00a0never\u2503classGreetings(App[None]): \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0Why\u00a0doesn't\u00a0Textual\u00a0look\u00a0good\u00a0on\u00a0m\u2503\u2502\u00a0\u00a0\u00a0 \u2503\u2514\u2500\u2500\u00a0\u2161\u00a0Why\u00a0doesn't\u00a0Textual\u00a0support\u00a0ANSI\u00a0t\u2503\u2502\u00a0\u00a0\u00a0def__init__(self,greeting:str=\"Hello\",to_greet:str=\"World\")->None: \u2503\u2503\u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0self.greeting=greeting \u2503\u2503\u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0self.to_greet=to_greet \u2503\u2503\u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0super().__init__() \u2503\u2503\u2502\u00a0\u00a0\u00a0 \u2503\u2503\u2502\u00a0\u00a0\u00a0defcompose(self)->ComposeResult: \u2503\u2503\u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0yieldStatic(f\"{self.greeting},\u00a0{self.to_greet}\") \u2503\u2503 \u2503\u2503 \u2503\u2503Then\u00a0the\u00a0app\u00a0can\u00a0be\u00a0run,\u00a0passing\u00a0in\u00a0various\u00a0arguments;\u00a0for\u00a0example: \u2503\u2503\u2585\u2585 \u2503\u2503 \u2503\u2503#\u00a0Running\u00a0with\u00a0default\u00a0arguments. \u2503\u2503Greetings().run() \u2503\u2503 \u2503\u2503#\u00a0Running\u00a0with\u00a0a\u00a0keyword\u00a0arguyment. \u2503\u2503Greetings(to_greet=\"davep\").run()\u2585\u2585 \u2503\u2503 \u2503\u2503#\u00a0Running\u00a0with\u00a0both\u00a0positional\u00a0arguments. \u2503\u2503Greetings(\"Well\u00a0hello\",\"there\").run() \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2503\u2589\u2503\u258e\u258a \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u00a0F1\u00a0\u00a0Help\u00a0\u00a0F2\u00a0\u00a0About\u00a0\u00a0CTRL+N\u00a0\u00a0Navigation\u00a0\u00a0CTRL+Q\u00a0\u00a0Quit\u00a0

Tip

Join our mailing list if you would like to be the first to hear about our apps.

We haven't stopped developing Textual in that time. Today we released version 0.23.0 which has a really interesting API update I'd like to introduce.

Textual widgets can send messages to each other. To respond to those messages, you implement a message handler with a naming convention. For instance, the Button widget sends a Pressed event. To handle that event, you implement a method called on_button_pressed.

Simple enough, but handler methods are called to handle pressed events from all Buttons. To manage multiple buttons you typically had to write a large if statement to wire up each button to the code it should run. It didn't take many Buttons before the handler became hard to follow.

"},{"location":"blog/2023/05/03/textual-0230-improves-message-handling/#on-decorator","title":"On decorator","text":"

Version 0.23.0 introduces the @on decorator which allows you to dispatch events based on the widget that initiated them.

This is probably best explained in code. The following two listings respond to buttons being pressed. The first uses a single message handler, the second uses the decorator approach:

on_decorator01.pyon_decorator02.pyOutput on_decorator01.py
from textual import on\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Button\nclass OnDecoratorApp(App):\nCSS_PATH = \"on_decorator.tcss\"\ndef compose(self) -> ComposeResult:\n\"\"\"Three buttons.\"\"\"\nyield Button(\"Bell\", id=\"bell\")\nyield Button(\"Toggle dark\", classes=\"toggle dark\")\nyield Button(\"Quit\", id=\"quit\")\ndef on_button_pressed(self, event: Button.Pressed) -> None:  # (1)!\n\"\"\"Handle all button pressed events.\"\"\"\nif event.button.id == \"bell\":\nself.bell()\nelif event.button.has_class(\"toggle\", \"dark\"):\nself.dark = not self.dark\nelif event.button.id == \"quit\":\nself.exit()\nif __name__ == \"__main__\":\napp = OnDecoratorApp()\napp.run()\n
  1. The message handler is called when any button is pressed
on_decorator02.py
from textual import on\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Button\nclass OnDecoratorApp(App):\nCSS_PATH = \"on_decorator.tcss\"\ndef compose(self) -> ComposeResult:\n\"\"\"Three buttons.\"\"\"\nyield Button(\"Bell\", id=\"bell\")\nyield Button(\"Toggle dark\", classes=\"toggle dark\")\nyield Button(\"Quit\", id=\"quit\")\n@on(Button.Pressed, \"#bell\")  # (1)!\ndef play_bell(self):\n\"\"\"Called when the bell button is pressed.\"\"\"\nself.bell()\n@on(Button.Pressed, \".toggle.dark\")  # (2)!\ndef toggle_dark(self):\n\"\"\"Called when the 'toggle dark' button is pressed.\"\"\"\nself.dark = not self.dark\n@on(Button.Pressed, \"#quit\")  # (3)!\ndef quit(self):\n\"\"\"Called when the quit button is pressed.\"\"\"\nself.exit()\nif __name__ == \"__main__\":\napp = OnDecoratorApp()\napp.run()\n
  1. Matches the button with an id of \"bell\" (note the # to match the id)
  2. Matches the button with class names \"toggle\" and \"dark\"
  3. Matches the button with an id of \"quit\"

OnDecoratorApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Bell\u00a0\u00a0Toggle\u00a0dark\u00a0\u00a0Quit\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

The decorator dispatches events based on a CSS selector. This means that you could have a handler per button, or a handler for buttons with a shared class, or parent.

We think this is a very flexible mechanism that will help keep code readable and maintainable.

"},{"location":"blog/2023/05/03/textual-0230-improves-message-handling/#why-didnt-we-do-this-earlier","title":"Why didn't we do this earlier?","text":"

It's a reasonable question to ask: why didn't we implement this in an earlier version? We were certainly aware there was a deficiency in the API.

The truth is simply that we didn't have an elegant solution in mind until recently. The @on decorator is, I believe, an elegant and powerful mechanism for dispatching handlers. It might seem obvious in hindsight, but it took many iterations and brainstorming in the office to come up with it!

"},{"location":"blog/2023/05/03/textual-0230-improves-message-handling/#join-us","title":"Join us","text":"

If you want to talk about this update or anything else Textual related, join us on our Discord server.

"},{"location":"blog/2023/05/08/textual-0240-adds-a-select-control/","title":"Textual 0.24.0 adds a Select control","text":"

Coming just 5 days after the last release, we have version 0.24.0 which we are crowning the King of Textual releases. At least until it is deposed by version 0.25.0.

The highlight of this release is the new Select widget: a very familiar control from the web and desktop worlds. Here's a screenshot and code:

Output (expanded)select_widget.pyselect.css

SelectApp \u2b58SelectApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aSelect\u25b2\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aSelect\u258e \u258aI\u00a0must\u00a0not\u00a0fear.\u258e \u258aFear\u00a0is\u00a0the\u00a0mind-killer.\u258e \u258aFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0\u258e \u258aobliteration.\u258e \u258aI\u00a0will\u00a0face\u00a0my\u00a0fear.\u258e \u258aI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

from textual import on\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Header, Select\nLINES = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\"\"\".splitlines()\nclass SelectApp(App):\nCSS_PATH = \"select.tcss\"\ndef compose(self) -> ComposeResult:\nyield Header()\nyield Select((line, line) for line in LINES)\n@on(Select.Changed)\ndef select_changed(self, event: Select.Changed) -> None:\nself.title = str(event.value)\nif __name__ == \"__main__\":\napp = SelectApp()\napp.run()\n
\n
"},{"location":"blog/2023/05/08/textual-0240-adds-a-select-control/#new-styles","title":"New styles","text":"

This one required new functionality in Textual itself. The \"pull-down\" overlay with options presented a difficulty with the previous API. The overlay needed to appear over any content below it. This is possible (using layers), but there was no simple way of positioning it directly under the parent widget.

We solved this with a new \"overlay\" concept, which can considered a special layer for user interactions like this Select, but also pop-up menus, tooltips, etc. Widgets styled to use the overlay appear in their natural place in the \"document\", but on top of everything else.

A second problem we tackled was ensuring that an overlay widget was never clipped. This was also solved with a new rule called \"constrain\". Applying constrain to a widget will keep the widget within the bounds of the screen. In the case of Select, if you expand the options while at the bottom of the screen, then the overlay will be moved up so that you can see all the options.

These new rules are currently undocumented as they are still subject to change, but you can see them in the Select source if you are interested.

In a future release these will be finalized and you can confidently use them in your own projects.

"},{"location":"blog/2023/05/08/textual-0240-adds-a-select-control/#fixes-for-the-on-decorator","title":"Fixes for the @on decorator","text":"

The new @on decorator is proving popular. To recap, it is a more declarative and finely grained way of dispatching messages. Here's a snippet from the calculator example which uses @on:

    @on(Button.Pressed, \"#plus,#minus,#divide,#multiply\")\ndef pressed_op(self, event: Button.Pressed) -> None:\n\"\"\"Pressed one of the arithmetic operations.\"\"\"\nself.right = Decimal(self.value or \"0\")\nself._do_math()\nassert event.button.id is not None\nself.operator = event.button.id\n

The decorator arranges for the method to be called when any of the four math operation buttons are pressed.

In 0.24.0 we've fixed some missing attributes which prevented the decorator from working with some messages. We've also extended the decorator to use keywords arguments, so it will match attributes other than control.

"},{"location":"blog/2023/05/08/textual-0240-adds-a-select-control/#other-fixes","title":"Other fixes","text":"

There is a surprising number of fixes in this release for just 5 days. See CHANGELOG.md for details.

"},{"location":"blog/2023/05/08/textual-0240-adds-a-select-control/#join-us","title":"Join us","text":"

If you want to talk about this update or anything else Textual related, join us on our Discord server.

"},{"location":"blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/","title":"Textual adds Sparklines, Selection list, Input validation, and tool tips","text":"

It's been 12 days since the last Textual release, which is longer than our usual release cycle of a week.

We've been a little distracted with our \"dogfood\" projects: Frogmouth and Trogon. Both of which hit 1000 Github stars in 24 hours. We will be maintaining / updating those, but it is business as usual for this Textual release (and it's a big one). We have such sights to show you.

"},{"location":"blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/#sparkline-widget","title":"Sparkline widget","text":"

A Sparkline is essentially a mini-plot. Just detailed enough to keep an eye on time-series data.

SparklineColorsApp \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582 \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582 \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582 \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582 \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582 \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582 \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582 \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582 \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582 \u2581\u2582\u2582\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2583\u2583\u2582\u2581\u2582\u2582\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2582

Colors are configurable, and all it takes is a call to set_interval to make it animate.

"},{"location":"blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/#selection-list","title":"Selection list","text":"

Next up is the SelectionList widget. Essentially a scrolling list of checkboxes. Lots of use cases for this one.

SelectionListApp \u2b58SelectionListApp \u250c\u2500\u00a0Shall\u00a0we\u00a0play\u00a0some\u00a0games?\u00a0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502 \u2502\u2590X\u258cFalken's\u00a0Maze\u2502 \u2502\u2590X\u258cBlack\u00a0Jack\u2502 \u2502\u2590X\u258cGin\u00a0Rummy\u2502 \u2502\u2590X\u258cHearts\u2502 \u2502\u2590X\u258cBridge\u2502 \u2502\u2590X\u258cCheckers\u2502 \u2502\u2590X\u258cChess\u2502 \u2502\u2590X\u258cPoker\u2502 \u2502\u2590X\u258cFighter\u00a0Combat\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"},{"location":"blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/#tooltips","title":"Tooltips","text":"

We've added tooltips to Textual widgets.

The API couldn't be simpler: simply assign a string to the tooltip property on any widget. This string will be displayed after 300ms when you hover over the widget.

TooltipApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Click\u00a0me \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear.

As always, you can configure how the tooltips will be displayed with CSS.

"},{"location":"blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/#input-updates","title":"Input updates","text":"

We have some quality of life improvements for the Input widget.

You can now use a simple declarative API to validating input.

InputApp Enter\u00a0an\u00a0even\u00a0number\u00a0between\u00a01\u00a0and\u00a0100\u00a0that\u00a0is\u00a0also\u00a0a\u00a0palindrome. \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258afoo\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e ['Must\u00a0be\u00a0a\u00a0valid\u00a0number.',\u00a0'Value\u00a0is\u00a0not\u00a0even.',\u00a0\"That's\u00a0not\u00a0a\u00a0palindrome\u00a0:/\"]

Also in this release is a suggestion API, which will suggest auto completions as you type. Hit right to accept the suggestion.

Here's a screenshot:

FruitsApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258astrawberry\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

You could use this API to offer suggestions from a fixed list, or even pull the data from a network request.

"},{"location":"blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/#join-us","title":"Join us","text":"

Development on Textual is fast. We're very responsive to issues and feature requests.

If you have any suggestions, jump on our Discord server and you may see your feature in the next release!

"},{"location":"blog/2023/07/03/textual-0290-refactors-dev-tools/","title":"Textual 0.29.0 refactors dev tools","text":"

It's been a slow week or two at Textualize, with Textual devs taking well-earned annual leave, but we still managed to get a new version out.

Version 0.29.0 has shipped with a number of fixes (see the release notes for details), but I'd like to use this post to explain a change we made to how Textual developer tools are distributed.

Previously if you installed textual[dev] you would get the Textual dev tools plus the library itself. If you were distributing Textual apps and didn't need the developer tools you could drop the [dev].

We did this because the less dependencies a package has, the fewer installation issues you can expect to get in the future. And Textual is surprisingly lean if you only need to run apps, and not build them.

Alas, this wasn't quite as elegant solution as we hoped. The dependencies defined in extras wouldn't install commands, so textual was bundled with the core library. This meant that if you installed the Textual package without the [dev] you would still get the textual command on your path but it wouldn't run.

We solved this by creating two packages: textual contains the core library (with minimal dependencies) and textual-dev contains the developer tools. If you are building Textual apps, you should install both as follows:

pip install textual textual-dev\n

That's the only difference. If you run in to any issues feel free to ask on the Discord server!

"},{"location":"blog/2023/07/17/textual-0300-adds-desktop-style-notifications/","title":"Textual 0.30.0 adds desktop-style notifications","text":"

We have a new release of Textual to talk about, but before that I'd like to cover a little Textual news.

By sheer coincidence we reached 20,000 stars on GitHub today. Now stars don't mean all that much (at least until we can spend them on coffee), but its nice to know that twenty thousand developers thought Textual was interesting enough to hit the \u2605 button. Thank you!

In other news: we moved office. We are now a stone's throw away from Edinburgh Castle. The office is around three times as big as the old place, which means we have room for wide standup desks and dual monitors. But more importantly we have room for new employees. Don't send your CVs just yet, but we hope to grow the team before the end of the year.

Exciting times.

"},{"location":"blog/2023/07/17/textual-0300-adds-desktop-style-notifications/#new-release","title":"New Release","text":"

And now, for the main feature. Version 0.30 adds a new notification system. Similar to desktop notifications, it displays a small window with a title and message (called a toast) for a pre-defined number of seconds.

Notifications are great for short timely messages to add supplementary information for the user. Here it is in action:

The API is super simple. To display a notification, call notify() with a message and an optional title.

def on_mount(self) -> None:\nself.notify(\"Hello, from Textual!\", title=\"Welcome\")\n
"},{"location":"blog/2023/07/17/textual-0300-adds-desktop-style-notifications/#textualize-video-channel","title":"Textualize Video Channel","text":"

In case you missed it; Textualize now has a YouTube channel. Our very own Rodrigo has recorded a video tutorial series on how to build Textual apps. Check it out!

We will be adding more videos in the near future, covering anything from beginner to advanced topics.

Don't worry if you prefer reading to watching videos. We will be adding plenty more content to the Textual docs in the near future. Watch this space.

As always, if you want to discuss anything with the Textual developers, join us on the Discord server.

"},{"location":"blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/","title":"Textual 0.38.0 adds a syntax aware TextArea","text":"

This is the second big feature release this month after last week's command palette.

The TextArea has finally landed. I know a lot of folk have been waiting for this one. Textual's TextArea is a fully-featured widget for editing code, with syntax highlighting and line numbers. It is highly configurable, and looks great.

Darren Burns (the author of this widget) has penned a terrific write-up on the TextArea. See Things I learned while building Textual's TextArea for some of the challenges he faced.

"},{"location":"blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/#scoped-css","title":"Scoped CSS","text":"

Another notable feature added in 0.38.0 is scoped CSS. A common gotcha in building Textual widgets is that you could write CSS that impacted styles outside of that widget.

Consider the following widget:

class MyWidget(Widget):\nDEFAULT_CSS = \"\"\"\n    MyWidget {\n        height: auto;\n        border: magenta;\n    }\n    Label {\n        border: solid green;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Label(\"foo\")\nyield Label(\"bar\")\n

The author has intended to style the labels in that widget by adding a green border. This does work for the widget in question, but (prior to 0.38.0) the Label rule would style all Labels (including any outside of the widget) \u2014 which was probably not intended.

With version 0.38.0, the CSS is scoped so that only the widget's labels will be styled. This is almost always what you want, which is why it is enabled by default. If you do want to style something outside of the widget you can set SCOPED_CSS=False (as a classvar).

"},{"location":"blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/#light-and-dark-pseudo-selectors","title":"Light and Dark pseudo selectors","text":"

We've also made a slight quality of life improvement to the CSS, by adding :light and :dark pseudo selectors. This allows you to change styles depending on whether you have dark mode enabled or not.

This was possible before, just a little verbose. Here's how you would do it in 0.37.0:

App.-dark-mode MyWidget Label {\n...\n}\n

In 0.38.0 it's a little more concise and readable:

MyWidget:dark Label {\n...\n}\n
"},{"location":"blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/#testing-guide","title":"Testing guide","text":"

Not strictly part of the release, but we've added a guide on testing Textual apps.

As you may know, we are on a mission to make TUIs a serious proposition for critical apps, which makes testing essential. We've extracted and documented our internal testing tools, including our snapshot tests pytest plugin pytest-textual-snapshot.

This gives devs powerful tools to ensure the quality of their apps. Let us know your thoughts on that!

"},{"location":"blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/#release-notes","title":"Release notes","text":"

See the release page for the full details on this release.

"},{"location":"blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/#whats-next","title":"What's next?","text":"

There's lots of features planned over the next few months. One feature I am particularly excited by is a widget to generate plots by wrapping the awesome Plotext library. Check out some early work on this feature:

"},{"location":"blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/#join-us","title":"Join us","text":"

Join our Discord server if you want to discuss Textual with the Textualize devs, or the community.

"},{"location":"blog/2022/11/08/version-040/","title":"Version 0.4.0","text":"

We've released version 0.4.0 of Textual.

As this is the first post tagged with release let me first explain where the blog fits in with releases. We plan on doing a post for every note-worthy release. Which likely means all but the most trivial updates (typos just aren't that interesting). Blog posts will be supplementary to release notes which you will find on the Textual repository.

Blog posts will give a little more background for the highlights in a release, and a rationale for changes and new additions. We embrace building in public, which means that we would like you to be as up-to-date with new developments as if you were sitting in our office. It's a small office, and you might not be a fan of the Scottish weather (it's dreich), but you can at least be here virtually.

Release 0.4.0 follows 0.3.0, released on October 31st. Here are the highlights of the update.

"},{"location":"blog/2022/11/08/version-040/#updated-mount-method","title":"Updated Mount Method","text":"

The mount method has seen some work. We've dropped the ability to assign an id via keyword attributes, which wasn't terribly useful. Now, an id must be assigned via the constructor.

The mount method has also grown before and after parameters which tell Textual where to add a new Widget (the default was to add it to the end). Here are a few examples:

# Mount at the start\nself.mount(Button(id=\"Buy Coffee\"), before=0)\n# Mount after a selector\nself.mount(Static(\"Password is incorrect\"), after=\"Dialog Input.-error\")\n# Mount after a specific widget\ntweet = self.query_one(\"Tweet\")\nself.mount(Static(\"Consider switching to Mastodon\"), after=tweet)\n

Textual needs much of the same kind of operations as the JS API exposed by the browser. But we are determined to make this way more intuitive. The new mount method is a step towards that.

"},{"location":"blog/2022/11/08/version-040/#faster-updates","title":"Faster Updates","text":"

Textual now writes to stdout in a thread. The upshot of this is that Textual can work on the next update before the terminal has displayed the previous frame.

This means smoother updates all round! You may notice this when scrolling and animating, but even if you don't, you will have more CPU cycles to play with in your Textual app.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN2ca3Oa3Fx1MDAxNsff91NkPG8r3fdLZ86cybW5t401SXPmmVx1MDAwZUFUXCKK4WIunX73Z0FsXHUwMDAwI2pcdTAwMTKDTPLCKlx1MDAxYvZebNaP/1prQ39/WFurhXdDu/Z5rWbfWqbrtHzzpvYx3j6y/cDxXHUwMDA20ESS34FcdTAwMTf5VrJnN1xmh8HnT5/6pt+zw6FrWrYxcoLIdIMwajmeYXn9T05o94P/xZ/HZt/+79Drt0LfSFx1MDAwN6nbLSf0/IexbNfu24MwgN7/XHUwMDBmv9fWfiefXHUwMDE563zbXG7NQce1k1x1MDAwM5Km1ECK1eTWY2+QXHUwMDE4SzDDVFx1MDAxMk7Q41x1MDAxZU6wXHUwMDA144V2XHUwMDBimttgs522xJtq55fW8dVgOzo971xuWd857UaDzct02Lbjuo3wzn2YXG7T6kZ+xqgg9L2efea0wi6044ntj8dcdTAwMDVcdTAwMWXMQnqU70Wd7sBcdTAwMGWC3DHe0LSc8C7ehlLzXHUwMDFmZuHzWrrlXHUwMDE2filuKClcdTAwMTVcdTAwMTVCYc6kXHUwMDE0j63J8ZxcdTAwMWGEU41cdTAwMDThXGL+Ju3a9FxcuFx1MDAxNGDXf6hgbUumll2aVq9cdTAwMDPmXHJa6T6EqEtbKJXudfP3fJkyiJSUUclcdTAwMWZcdTAwMWK7ttPphtDKkKGlXCJgI5KYXHUwMDExRVMr7ORqxC2USMZcdTAwMWVcdTAwMWLioYd7rcQx/pmczK7pXHUwMDBmx5NWXHUwMDBi4lx1MDAxZlx1MDAxObNji7cnvSrrWZlcdTAwMGJ+2T5qXHUwMDFjOMdcciFcdTAwMWPinp9uXHUwMDFmXHUwMDA218HPx75yblx1MDAxONq3Ye2x4c/H6d3m9v646IBpt+Nv6Vx0R8OW+eCwWFxiqWGKJaY09Vx1MDAwMNdcdTAwMTn0oHFcdTAwMTC5brrNs3qpj3/I2PtMuDQthotKXHUwMDA03pa5YPPgut9ccr5aR0PyvVlf92+PLbp/9l1UXHUwMDFkrjp4q0GRXHUwMDE2XHUwMDA0cy01opzk8FwiXGJcdTAwMWKEUnBuJqTChFx1MDAxN+LF27Rlsdl42YJphabhRVxiM4RmkjMmgFx1MDAxZq5ewFx1MDAxONxcdTAwMGaVwHBcdTAwMDaqXFzIml/I/p55MrpAw9b1tT/sbl37J29cdNn0XHUwMDAxn1x1MDAwM1x1MDAxOUwkUrxcdTAwMTTIXHUwMDE4xkWQYSm4RIhxsTBkw1x1MDAxZt++7l1cdTAwMWW1aGRcdTAwMWTgId9wej/vulWHjFx0XHUwMDBljGFcdTAwMDJcdTAwMTOviKSgXCJcdTAwMTNcdTAwMTImXGbMlVx1MDAxNFx1MDAxOCeuzYohW6mGYaQg2lx1MDAxMFx1MDAxNJNy+dq17qXd22t2b2lvyLqX9uHoXHUwMDFifku+plx1MDAwZlhRXHUwMDExY1pcdTAwMTbxJZhcIkhgtLiG3e2qKPq1dVx1MDAxZlx1MDAxYzXNkfn91t05cdpVx4tQaVx1MDAwMFVcdTAwMTju/pJzzSfo0spA0FwiXHUwMDE555RLnLnZVE/CXGLinCnKsChcdTAwMTexg63v6Ng+6nxcdTAwMGbCbvO62d7mqH/2lohNXHUwMDFmcHWI5ezMJoikXGIuTFx1MDAxOIeQnujFxWt2oFBRuupCXHUwMDE4TIBzM1xuai2QyONFkDZcYokjyPiCKKqXXHUwMDExIT6FKyOZjzRlg9ExPlx1MDAwMJVcdTAwMTBYv4VCLTOaSi+5N1xiXHUwMDFizn2ScaDc1lx1MDAxZLPvuHe5q5b4aOxGyfjZeVxmbFx1MDAxODNxSpXbe911OrFcdTAwMTfXLDhcdTAwMGLbzzl46Fim+7hD32m1svpigVx0JvTp7y2S23i+03FcdTAwMDam+yNv4YuIK6x3MKmpQGRxMZud/FZcdTAwMTQ3TJmBsODgUFxcQFx1MDAwMqry+Vx1MDAxOFx1MDAxNpCPMVxu07BENXuKWyZcdTAwMGKcgVx1MDAxYqaEXHUwMDBirbF8XHUwMDAztVrmnf91uJ35Tsm0zSnTTdL2YOCLYCvOzTjHXHUwMDFhJluphXGbXHUwMDFkQ1RcdTAwMTQ3iqnBXHUwMDE1Y5prXHUwMDA0mZGarH5cYlx1MDAwM0muYFx1MDAxZYA1ls18ViFuXHUwMDEwNCrMdbniVjJtK1x1MDAxMLc5SU8p4iYgZ6BU4cWDydlZcUVx44BcdTAwMWKjWsPpclx1MDAxOVdCJsSNXHUwMDE5XHUwMDE4JE/qh1xmSqo3wW1BcVx1MDAwMyvB0pJDyXevbXNcbngv0LbZ1ZHMZE4gJ1x0QpqpZ1x1MDAwNJT7I3SAdjZa/kE/XHUwMDFhnUdHfp1wXXXkMKKGXHUwMDAy5WJcYkI1hjjOIcewNFx1MDAwNChcdTAwMWZCY5ErJG7FpUfQZ1x1MDAwMVx1MDAwMW/JdVx1MDAxMeF87TaO2DpB9+bJSe8wwvhm9Jq6yFx1MDAxYnU7r9wyfcC02/G3alQ0OS1cXJbjRGhCsmvAc2Wy8+XQPJDfnOZF65c2zZsrc++u6szWMUlcdTAwMTa9OTi8oFwiXHUwMDBlTfPUXG6QUak4pH/JqncxtasvaWKCQEhFdpGnXHUwMDE0dCN2u3s9ijbcXHUwMDAzT9zwweHVcc/uvVx1MDAxZd2ldztcdTAwMGbd6Vx1MDAwM1ZcdTAwMTVdrYvQJZpoXHIh7uJye9A7adCGa91ftK9+7Gy2T5v3vdOqo0spMTAnXHUwMDAwLlx1MDAxMVxmXCJZzfLkYmFcYlx1MDAxZcdcdTAwMWRcdTAwMGbAZG51lVx1MDAxMlxcolC85l82tPvWyW5k4u0uP6xcdTAwMWaMblx1MDAwNlx1MDAwM1Cr6PXQLr3bedBOXHUwMDFmsKLQSsSLoMVcdTAwMWHFSal+XHUwMDA2tdH2VXR4tyt/ycPG9lx1MDAxNW9IsnfxperUYsRcZsqFwpJSXHUwMDExL2boPLWgt5CfYyrwPGorILiSY1x1MDAxNa9Olczu1sXXy0NN7ky457Gj4O6871xmL1/P7tK7ncfu9Fx1MDAwMVfHbmHtVlx1MDAxND9YoyA4XHUwMDA0aJ+R286OayqKbZ0jXHUwMDAzUYRcdTAwMTVcdTAwMDI448WSiYdDmcRcdTAwMDZcdTAwMTNSUPqQ3L7Rasli9du4sqWIxO+7orSC+u2cXHUwMDE0b5n1W4xcdTAwMGLjWy21ZOCKiyM3u1xuUFHkMMeGjlx1MDAxN/0gxGVxOWmCOEhcXLlcdTAwMDYh1VgnXG62yvXJeMVcdTAwMDRcdKrouyau/Fx1MDAxYe6cOuhcdTAwMTLXJ8lcZt5cdTAwMTSDm756xuM3syOJqvKmkEFcdTAwMTDDXHUwMDFh4jnIx8hEOlx0XHUwMDAyJyghT+PF8vVNQ85cdTAwMGKRqX7XXHUwMDBmXHUwMDAzrEDe5iRUS5U3XZxcdFIpiFr8VaPZKXdFYaMxbCBtTGGqp1x1MDAxNG/i7IwhJMRY2+Sb4Lbg+iRcdTAwMDfkJc247jvErXxtm1N0XFyitlx1MDAxNT5cdTAwMGKAXHUwMDA1XCKCaJEpy8yj7eJLdK+Q02tcXHX3XHUwMDFhO1dnW0PvdLfqtGFFXGZcdTAwMTRnZVx1MDAwNCdcdTAwMGZcdTAwMDPofCippVx1MDAwMUmdYip+qlx1MDAxM9LZV7FWXFwoXHUwMDE1U2osT2mjXHUwMDA0bn/wUX4kSXRptG3Ybc9/XHUwMDFlbq7dXHUwMDBlZ8BcdTAwMTZ6w1wi0nJnMYnV2JJcdTAwMTdxVfg6XHUwMDA0ZkzHIUqmejePK9usb1x1MDAwNH3xrd+MzlG9j++3tjevK8+VIIZcdTAwMDBqXG64ojxeoVx1MDAxMFxuKVx1MDAxND9BOut1o9eRlcn9ZpAlKKVSKVa+jpVI1no7J0qrXHUwMDAz68GQmVxcmb7v3UxNxlDhujxcdTAwMDQjSHKK9OJoscb53k7n+HJ9py72/P3dU7y19cKHaSafynzDV424QWAvTjWjkIzhiXSMXCJlcFx1MDAwMOLvXHUwMDAzbK9Lx4rJgsFccqD872NymE5J0LChMFx1MDAwNKtiSuSIOFFcdTAwMWGJXHUwMDE3vIueWFg2cUFo+uGGM2g5g87kIfagVdDimkG46fX7TlxiZnzznEE4uUfS73rs7V3bfEJcdTAwMGX0nG2rhb4zsYI2jDvNL4uk39ZSz0l+PH7/5+PUvVx0llx1MDAwNlI4juhxPreI/+rI0EhcdTAwMDGAmPB5PVx1MDAxNTtH0lPqXHUwMDE3aUdcdTAwMWay/z5Xb0XxS/SIKU3gfNLLPrdGc/Szuf+r88VcdTAwMTXO6c9r+/asqe2rqt9cdTAwMTXqXFxcdTAwMTlcdTAwMTBcdTAwMWRcblx1MDAxZf+PXHUwMDFjQiOevytAKG9gzcZcdTAwMDXRV69CzLgtLCS4mFx0grR8529t/HD6lYhkXHUwMDEzO1x1MDAxZbj6MEa2Zlx1MDAwZYeNMC7SfFx1MDAxZVNcdTAwMDZcdTAwMTfAaY1PMe2tNnLsm40pXHUwMDFl0E7+4l5cdTAwMTNWYyrsePp///nw51+TJY25In0= UpdateWriteUpdateWriteUpdateWriteUpdateWriteBeforeAfterTime"},{"location":"blog/2022/11/08/version-040/#multiple-css-paths","title":"Multiple CSS Paths","text":"

Up to version 0.3.0, Textual would only read a single CSS file set in the CSS_PATH class variable. You can now supply a list of paths if you have more than one CSS file.

This change was prompted by tuilwindcss which brings a TailwindCSS like approach to building Textual Widgets. Also check out calmcode.io by the same author, which is an amazing resource.

"},{"location":"blog/2022/12/11/version-060/","title":"Textual 0.6.0 adds a treemendous new widget","text":"

A new release of Textual lands 3 weeks after the previous release -- and it's a big one.

Information

If you're new here, Textual is TUI framework for Python.

"},{"location":"blog/2022/12/11/version-060/#tree-control","title":"Tree Control","text":"

The headline feature of version 0.6.0 is a new tree control built from the ground-up. The previous Tree control suffered from an overly complex API and wasn't scalable (scrolling slowed down with 1000s of nodes).

This new version has a simpler API and is highly scalable (no slowdown with larger trees). There are also a number of visual enhancements in this version.

Here's a very simple example:

Outputtree.py

TreeApp \u25bc\u00a0Dune \u2517\u2501\u2501\u00a0\u25bc\u00a0Characters \u2523\u2501\u2501\u00a0Paul \u2523\u2501\u2501\u00a0Jessica \u2517\u2501\u2501\u00a0Chani

from textual.app import App, ComposeResult\nfrom textual.widgets import Tree\nclass TreeApp(App):\ndef compose(self) -> ComposeResult:\ntree: Tree[dict] = Tree(\"Dune\")\ntree.root.expand()\ncharacters = tree.root.add(\"Characters\", expand=True)\ncharacters.add_leaf(\"Paul\")\ncharacters.add_leaf(\"Jessica\")\ncharacters.add_leaf(\"Chani\")\nyield tree\nif __name__ == \"__main__\":\napp = TreeApp()\napp.run()\n

Here's the tree control being used to navigate some JSON (json_tree.py in the examples directory).

I'm biased of course, but I think this terminal based tree control is more usable (and even prettier) than just about anything I've seen on the web or desktop. So much of computing tends to organize itself in to a tree that I think this widget will find a lot of uses.

The Tree control forms the foundation of the DirectoryTree widget, which has also been updated. Here it is used in the code_browser.py example:

"},{"location":"blog/2022/12/11/version-060/#list-view","title":"List View","text":"

We have a new ListView control to navigate and select items in a list. Items can be widgets themselves, which makes this a great platform for building more sophisticated controls.

Outputlist_view.pylist_view.css

ListViewExample One Two Three

from textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Label, ListItem, ListView\nclass ListViewExample(App):\nCSS_PATH = \"list_view.tcss\"\ndef compose(self) -> ComposeResult:\nyield ListView(\nListItem(Label(\"One\")),\nListItem(Label(\"Two\")),\nListItem(Label(\"Three\")),\n)\nyield Footer()\nif __name__ == \"__main__\":\napp = ListViewExample()\napp.run()\n
\n
"},{"location":"blog/2022/12/11/version-060/#placeholder","title":"Placeholder","text":"

The Placeholder widget was broken since the big CSS update. We've brought it back and given it a bit of a polish.

Use this widget in place of custom widgets you have yet to build when designing your UI. The colors are automatically cycled to differentiate one placeholder from the next. You can click a placeholder to cycle between its ID, size, and lorem ipsum text.

Outputplaceholder.pyplaceholder.css

PlaceholderApp Placeholder\u00a0p2\u00a0here! This\u00a0is\u00a0a\u00a0custom\u00a0label\u00a0for\u00a0p1. #p4 #p3 #p5Placeholder Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0 consectetur\u00a0adipiscing\u00a0elit.\u00a0 Etiam\u00a0feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0 accumsan.\u00a0Suspendisse\u00a0bibendum\u00a0 33\u00a0x\u00a011nec\u00a0libero\u00a0quis\u00a0gravida.\u00a034\u00a0x\u00a011 Phasellus\u00a0id\u00a0eleifend\u00a0ligula.\u00a0 Nullam\u00a0imperdiet\u00a0sem\u00a0tellus,\u00a0 sed\u00a0vehicula\u00a0nisl\u00a0faucibus\u00a0sit\u00a0 amet.\u00a0Praesent\u00a0iaculis\u00a0tempor\u00a0 Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0consectetur\u00a0 adipiscing\u00a0elit.\u00a0Etiam\u00a0feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0 accumsan.\u00a0Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis\u00a0 gravida.\u00a0Phasellus\u00a0id\u00a0eleifend\u00a0ligula.\u00a0Nullam\u00a0 imperdiet\u00a0sem\u00a0tellus,\u00a0sed\u00a0vehicula\u00a0nisl\u00a0faucibus50\u00a0x\u00a011 sit\u00a0amet.\u00a0Praesent\u00a0iaculis\u00a0tempor\u00a0ultricies.\u00a0Sed lacinia,\u00a0tellus\u00a0id\u00a0rutrum\u00a0lacinia,\u00a0sapien\u00a0sapien congue\u00a0mauris,\u00a0sit\u00a0amet\u00a0pellentesque\u00a0quam\u00a0quam\u00a0 vel\u00a0nisl.\u00a0Curabitur\u00a0vulputate\u00a0erat\u00a0pellentesque\u00a0 mauris\u00a0posuere,\u00a0non\u00a0dictum\u00a0risus\u00a0mattis. Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0consectetur\u00a0Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0consectetur\u00a0 adipiscing\u00a0elit.\u00a0Etiam\u00a0feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0adipiscing\u00a0elit.\u00a0Etiam\u00a0feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0 accumsan.\u00a0Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis\u00a0accumsan.\u00a0Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis\u00a0 gravida.\u00a0Phasellus\u00a0id\u00a0eleifend\u00a0ligula.\u00a0Nullam\u00a0gravida.\u00a0Phasellus\u00a0id\u00a0eleifend\u00a0ligula.\u00a0Nullam\u00a0 imperdiet\u00a0sem\u00a0tellus,\u00a0sed\u00a0vehicula\u00a0nisl\u00a0faucibusimperdiet\u00a0sem\u00a0tellus,\u00a0sed\u00a0vehicula\u00a0nisl\u00a0faucibus sit\u00a0amet.\u00a0Praesent\u00a0iaculis\u00a0tempor\u00a0ultricies.\u00a0Sedsit\u00a0amet.\u00a0Praesent\u00a0iaculis\u00a0tempor\u00a0ultricies.\u00a0Sed lacinia,\u00a0tellus\u00a0id\u00a0rutrum\u00a0lacinia,\u00a0sapien\u00a0sapienlacinia,\u00a0tellus\u00a0id\u00a0rutrum\u00a0lacinia,\u00a0sapien\u00a0sapien congue\u00a0mauris,\u00a0sit\u00a0amet\u00a0pellentesque\u00a0quam\u00a0quam\u00a0congue\u00a0mauris,\u00a0sit\u00a0amet\u00a0pellentesque\u00a0quam\u00a0quam\u00a0 vel\u00a0nisl.\u00a0Curabitur\u00a0vulputate\u00a0erat\u00a0pellentesque\u00a0vel\u00a0nisl.\u00a0Curabitur\u00a0vulputate\u00a0erat\u00a0pellentesque\u00a0 mauris\u00a0posuere,\u00a0non\u00a0dictum\u00a0risus\u00a0mattis.mauris\u00a0posuere,\u00a0non\u00a0dictum\u00a0risus\u00a0mattis.

from textual.app import App, ComposeResult\nfrom textual.containers import Container, Horizontal, VerticalScroll\nfrom textual.widgets import Placeholder\nclass PlaceholderApp(App):\nCSS_PATH = \"placeholder.tcss\"\ndef compose(self) -> ComposeResult:\nyield VerticalScroll(\nContainer(\nPlaceholder(\"This is a custom label for p1.\", id=\"p1\"),\nPlaceholder(\"Placeholder p2 here!\", id=\"p2\"),\nPlaceholder(id=\"p3\"),\nPlaceholder(id=\"p4\"),\nPlaceholder(id=\"p5\"),\nPlaceholder(),\nHorizontal(\nPlaceholder(variant=\"size\", id=\"col1\"),\nPlaceholder(variant=\"text\", id=\"col2\"),\nPlaceholder(variant=\"size\", id=\"col3\"),\nid=\"c1\",\n),\nid=\"bot\",\n),\nContainer(\nPlaceholder(variant=\"text\", id=\"left\"),\nPlaceholder(variant=\"size\", id=\"topright\"),\nPlaceholder(variant=\"text\", id=\"botright\"),\nid=\"top\",\n),\nid=\"content\",\n)\nif __name__ == \"__main__\":\napp = PlaceholderApp()\napp.run()\n
\n
"},{"location":"blog/2022/12/11/version-060/#fixes","title":"Fixes","text":"

As always, there are a number of fixes in this release. Mostly related to layout. See CHANGELOG.md for the details.

"},{"location":"blog/2022/12/11/version-060/#whats-next","title":"What's next?","text":"

The next release will focus on pain points we discovered while in a dog-fooding phase (see the DevLog for details on what Textual devs have been building).

"},{"location":"blog/2023/09/15/textual-0370-adds-a-command-palette/","title":"Textual 0.37.0 adds a command palette","text":"

Textual version 0.37.0 has landed! The highlight of this release is the new command palette.

A command palette gives users quick access to features in your app. If you hit ctrl+backslash in a Textual app, it will bring up the command palette where you can start typing commands. The commands are matched with a fuzzy search, so you only need to type two or three characters to get to any command.

Here's a video of it in action:

Adding your own commands to the command palette is a piece of cake. Here's the (command) Provider class used in the example above:

class ColorCommands(Provider):\n\"\"\"A command provider to select colors.\"\"\"\nasync def search(self, query: str) -> Hits:\n\"\"\"Called for each key.\"\"\"\nmatcher = self.matcher(query)\nfor color in COLOR_NAME_TO_RGB.keys():\nscore = matcher.match(color)\nif score > 0:\nyield Hit(\nscore,\nmatcher.highlight(color),\npartial(self.app.post_message, SwitchColor(color)),\n)\n

And here is how you add a provider to your app:

class ColorApp(App):\n\"\"\"Experiment with the command palette.\"\"\"\nCOMMANDS = App.COMMANDS | {ColorCommands}\n

We're excited about this feature because it is a step towards bringing a common user interface to Textual apps.

Quote

It's a Textual app. I know this.

\u2014 You, maybe.

The goal is to be able to build apps that may look quite different, but take no time to learn, because once you learn how to use one Textual app, you can use them all.

See the Guide for details on how to work with the command palette.

"},{"location":"blog/2023/09/15/textual-0370-adds-a-command-palette/#what-else","title":"What else?","text":"

Also in 0.37.0 we have a new Collapsible widget, which is a great way of adding content while avoiding a cluttered screen.

And of course, bug fixes and other updates. See the release page for the full details.

"},{"location":"blog/2023/09/15/textual-0370-adds-a-command-palette/#whats-next","title":"What's next?","text":"

Coming very soon, is a new TextEditor widget. This is a super powerful widget to enter arbitrary text, with beautiful syntax highlighting for a number of languages. We're expecting that to land next week. Watch this space, or join the Discord server if you want to be the first to try it out.

"},{"location":"blog/2023/09/15/textual-0370-adds-a-command-palette/#join-us","title":"Join us","text":"

Join our Discord server if you want to discuss Textual with the Textualize devs, or the community.

"},{"location":"blog/2022/12/07/letting-your-cook-multitask-while-bringing-water-to-a-boil/","title":"Letting your cook multitask while bringing water to a boil","text":"

Whenever you are cooking a time-consuming meal, you want to multitask as much as possible. For example, you do not want to stand still while you wait for a pot of water to start boiling. Similarly, you want your applications to remain responsive (i.e., you want the cook to \u201cmultitask\u201d) while they do some time-consuming operations in the background (e.g., while the water heats up).

The animation below shows an example of an application that remains responsive (colours on the left still change on click) even while doing a bunch of time-consuming operations (shown on the right).

In this blog post, I will teach you how to multitask like a good cook.

"},{"location":"blog/2022/12/07/letting-your-cook-multitask-while-bringing-water-to-a-boil/#wasting-time-staring-at-pots","title":"Wasting time staring at pots","text":"

There is no point in me presenting a solution to a problem if you don't understand the problem I am trying to solve. Suppose we have an application that needs to display a huge amount of data that needs to be read and parsed from a file. The first time I had to do something like this, I ended up writing an application that \u201cblocked\u201d. This means that while the application was reading and parsing the data, nothing else worked.

To exemplify this type of scenario, I created a simple application that spends five seconds preparing some data. After the data is ready, we display a Label on the right that says that the data has been loaded. On the left, the app has a big rectangle (a custom widget called ColourChanger) that you can click and that changes background colours randomly.

When you start the application, you can click the rectangle on the left to change the background colour of the ColourChanger, as the animation below shows:

However, as soon as you press l to trigger the data loading process, clicking the ColourChanger widget doesn't do anything. The app doesn't respond because it is busy working on the data. This is the code of the app so you can try it yourself:

import time\nfrom random import randint\nfrom textual.app import App, ComposeResult\nfrom textual.color import Color\nfrom textual.containers import Grid, VerticalScroll\nfrom textual.widget import Widget\nfrom textual.widgets import Footer, Label\nclass ColourChanger(Widget):  # (1)!\ndef on_click(self) -> None:\nself.styles.background = Color(\nrandint(1, 255),\nrandint(1, 255),\nrandint(1, 255),\n)\nclass MyApp(App[None]):\nBINDINGS = [(\"l\", \"load\", \"Load data\")]  # (2)!\nCSS = \"\"\"\n    Grid {\n        grid-size: 2;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Grid(\nColourChanger(),\nVerticalScroll(id=\"log\"),\n)\nyield Footer()\ndef action_load(self) -> None:  # (3)!\ntime.sleep(5)  # (4)!\nself.query_one(\"#log\").mount(Label(\"Data loaded \u2705\"))\nMyApp().run()\n
  1. The widget ColourChanger changes colours, randomly, when clicked.
  2. We create a binding to the key l that runs an action that we know will take some time (for example, reading and parsing a huge file).
  3. The method action_load is responsible for starting our time-consuming task and then reporting back.
  4. To simplify things a bit, our \u201ctime-consuming task\u201d is just standing still for 5 seconds.

I think it is easy to understand why the widget ColourChanger stops working when we hit the time.sleep call if we consider the cooking analogy I have written about before in my blog. In short, Python behaves like a lone cook in a kitchen:

  • the cook can be clever and multitask. For example, while water is heating up and being brought to a boil, the cook can go ahead and chop some vegetables.
  • however, there is only one cook in the kitchen, so if the cook is chopping up vegetables, they can't be seasoning a salad.

Things like \u201cchopping up vegetables\u201d and \u201cseasoning a salad\u201d are blocking, i.e., they need the cook's time and attention. In the app that I showed above, the call to time.sleep is blocking, so the cook can't go and do anything else until the time interval elapses.

"},{"location":"blog/2022/12/07/letting-your-cook-multitask-while-bringing-water-to-a-boil/#how-can-a-cook-multitask","title":"How can a cook multitask?","text":"

It makes a lot of sense to think that a cook would multitask in their kitchen, but Python isn't like a smart cook. Python is like a very dumb cook who only ever does one thing at a time and waits until each thing is completely done before doing the next thing. So, by default, Python would act like a cook who fills up a pan with water, starts heating the water, and then stands there staring at the water until it starts boiling instead of doing something else. It is by using the module asyncio from the standard library that our cook learns to do other tasks while awaiting the completion of the things they already started doing.

Textual is an async framework, which means it knows how to interoperate with the module asyncio and this will be the solution to our problem. By using asyncio with the tasks we want to run in the background, we will let the application remain responsive while we load and parse the data we need, or while we crunch the numbers we need to crunch, or while we connect to some slow API over the Internet, or whatever it is you want to do.

The module asyncio uses the keyword async to know which functions can be run asynchronously. In other words, you use the keyword async to identify functions that contain tasks that would otherwise force the cook to waste time. (Functions with the keyword async are called coroutines.)

The module asyncio also introduces a function asyncio.create_task that you can use to run coroutines concurrently. So, if we create a coroutine that is in charge of doing the time-consuming operation and then run it with asyncio.create_task, we are well on our way to fix our issues.

However, the keyword async and asyncio.create_task alone aren't enough. Consider this modification of the previous app, where the method action_load now uses asyncio.create_task to run a coroutine who does the sleeping:

import asyncio\nimport time\nfrom random import randint\nfrom textual.app import App, ComposeResult\nfrom textual.color import Color\nfrom textual.containers import Grid, VerticalScroll\nfrom textual.widget import Widget\nfrom textual.widgets import Footer, Label\nclass ColourChanger(Widget):\ndef on_click(self) -> None:\nself.styles.background = Color(\nrandint(1, 255),\nrandint(1, 255),\nrandint(1, 255),\n)\nclass MyApp(App[None]):\nBINDINGS = [(\"l\", \"load\", \"Load data\")]\nCSS = \"\"\"\n    Grid {\n        grid-size: 2;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Grid(\nColourChanger(),\nVerticalScroll(id=\"log\"),\n)\nyield Footer()\ndef action_load(self) -> None:  # (1)!\nasyncio.create_task(self._do_long_operation())  # (2)!\nasync def _do_long_operation(self) -> None:  # (3)!\ntime.sleep(5)\nself.query_one(\"#log\").mount(Label(\"Data loaded \u2705\"))\nMyApp().run()\n
  1. The action method action_load now defers the heavy lifting to another method we created.
  2. The time-consuming operation can be run concurrently with asyncio.create_task because it is a coroutine.
  3. The method _do_long_operation has the keyword async, so it is a coroutine.

This modified app also works but it suffers from the same issue as the one before! The keyword async tells Python that there will be things inside that function that can be awaited by the cook. That is, the function will do some time-consuming operation that doesn't require the cook's attention. However, we need to tell Python which time-consuming operation doesn't require the cook's attention, i.e., which time-consuming operation can be awaited, with the keyword await.

Whenever we want to use the keyword await, we need to do it with objects that are compatible with it. For many things, that means using specialised libraries:

  • instead of time.sleep, one can use await asyncio.sleep;
  • instead of the module requests to make Internet requests, use aiohttp; or
  • instead of using the built-in tools to read files, use aiofiles.
"},{"location":"blog/2022/12/07/letting-your-cook-multitask-while-bringing-water-to-a-boil/#achieving-good-multitasking","title":"Achieving good multitasking","text":"

To fix the last example application, all we need to do is replace the call to time.sleep with a call to asyncio.sleep and then use the keyword await to signal Python that we can be doing something else while we sleep. The animation below shows that we can still change colours while the application is completing the time-consuming operation.

CodeAnimation
import asyncio\nimport time\nfrom random import randint\nfrom textual.app import App, ComposeResult\nfrom textual.color import Color\nfrom textual.containers import Grid, VerticalScroll\nfrom textual.widget import Widget\nfrom textual.widgets import Footer, Label\nclass ColourChanger(Widget):\ndef on_click(self) -> None:\nself.styles.background = Color(\nrandint(1, 255),\nrandint(1, 255),\nrandint(1, 255),\n)\nclass MyApp(App[None]):\nBINDINGS = [(\"l\", \"load\", \"Load data\")]\nCSS = \"\"\"\n    Grid {\n        grid-size: 2;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Grid(\nColourChanger(),\nVerticalScroll(id=\"log\"),\n)\nyield Footer()\ndef action_load(self) -> None:\nasyncio.create_task(self._do_long_operation())\nasync def _do_long_operation(self) -> None:\nself.query_one(\"#log\").mount(Label(\"Starting \u23f3\"))  # (1)!\nawait asyncio.sleep(5)  # (2)!\nself.query_one(\"#log\").mount(Label(\"Data loaded \u2705\"))  # (3)!\nMyApp().run()\n
  1. We create a label that tells the user that we are starting our time-consuming operation.
  2. We await the time-consuming operation so that the application remains responsive.
  3. We create a label that tells the user that the time-consuming operation has been concluded.

Because our time-consuming operation runs concurrently, everything else in the application still works while we await for the time-consuming operation to finish. In particular, we can keep changing colours (like the animation above showed) but we can also keep activating the binding with the key l to start multiple instances of the same time-consuming operation! The animation below shows just this:

Warning

The animation GIFs in this blog post show low-quality colours in an attempt to reduce the size of the media files you have to download to be able to read this blog post. If you run Textual locally you will see beautiful colours \u2728

"},{"location":"blog/2023/07/27/using-rich-inspect-to-interrogate-python-objects/","title":"Using Rich Inspect to interrogate Python objects","text":"

The Rich library has a few functions that are admittedly a little out of scope for a terminal color library. One such function is inspect which is so useful you may want to pip install rich just for this feature.

The easiest way to describe inspect is that it is Python's builtin help() but easier on the eye (and with a few more features). If you invoke it with any object, inspect will display a nicely formatted report on that object \u2014 which makes it great for interrogating objects from the REPL. Here's an example:

>>> from rich import inspect\n>>> text_file = open(\"foo.txt\", \"w\")\n>>> inspect(text_file)\n

Here we're inspecting a file object, but it could be literally anything. You will see the following output in the terminal:

Rich \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<class'_io.TextIOWrapper'>\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502Character\u00a0and\u00a0line\u00a0based\u00a0layer\u00a0over\u00a0a\u00a0BufferedIOBase\u00a0object,\u00a0buffer.\u2502 \u2502\u2502 \u2502\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u2502 \u2502\u2502<_io.TextIOWrappername='foo.txt'mode='w'encoding='UTF-8'>\u2502\u2502 \u2502\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2502 \u2502\u2502 \u2502buffer\u00a0=<_io.BufferedWritername='foo.txt'>\u2502 \u2502closed\u00a0=False\u2502 \u2502encoding\u00a0='UTF-8'\u2502 \u2502errors\u00a0='strict'\u2502 \u2502line_buffering\u00a0=False\u2502 \u2502mode\u00a0='w'\u2502 \u2502name\u00a0='foo.txt'\u2502 \u2502newlines\u00a0=None\u2502 \u2502write_through\u00a0=False\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

By default, inspect will generate a data-oriented summary with a text representation of the object and its data attributes. You can also add methods=True to show all the methods in the public API. Here's an example:

>>> inspect(text_file, methods=True)\n
Rich \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<class'_io.TextIOWrapper'>\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502Character\u00a0and\u00a0line\u00a0based\u00a0layer\u00a0over\u00a0a\u00a0BufferedIOBase\u00a0object,\u00a0buffer.\u2502 \u2502\u2502 \u2502\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u2502 \u2502\u2502<_io.TextIOWrappername='foo.txt'mode='w'encoding='UTF-8'>\u2502\u2502 \u2502\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2502 \u2502\u2502 \u2502buffer\u00a0=<_io.BufferedWritername='foo.txt'>\u2502 \u2502closed\u00a0=False\u2502 \u2502encoding\u00a0='UTF-8'\u2502 \u2502errors\u00a0='strict'\u2502 \u2502line_buffering\u00a0=False\u2502 \u2502mode\u00a0='w'\u2502 \u2502name\u00a0='foo.txt'\u2502 \u2502newlines\u00a0=None\u2502 \u2502write_through\u00a0=False\u2502 \u2502close\u00a0=def\u00a0close():Flush\u00a0and\u00a0close\u00a0the\u00a0IO\u00a0object.\u2502 \u2502detach\u00a0=def\u00a0detach():Separate\u00a0the\u00a0underlying\u00a0buffer\u00a0from\u00a0the\u00a0TextIOBase\u00a0and\u00a0return\u00a0it.\u2502 \u2502fileno\u00a0=def\u00a0fileno():Returns\u00a0underlying\u00a0file\u00a0descriptor\u00a0if\u00a0one\u00a0exists.\u2502 \u2502flush\u00a0=def\u00a0flush():Flush\u00a0write\u00a0buffers,\u00a0if\u00a0applicable.\u2502 \u2502isatty\u00a0=def\u00a0isatty():Return\u00a0whether\u00a0this\u00a0is\u00a0an\u00a0'interactive'\u00a0stream.\u2502 \u2502read\u00a0=def\u00a0read(size=-1,\u00a0/):Read\u00a0at\u00a0most\u00a0n\u00a0characters\u00a0from\u00a0stream.\u2502 \u2502readable\u00a0=def\u00a0readable():Return\u00a0whether\u00a0object\u00a0was\u00a0opened\u00a0for\u00a0reading.\u2502 \u2502readline\u00a0=def\u00a0readline(size=-1,\u00a0/):Read\u00a0until\u00a0newline\u00a0or\u00a0EOF.\u2502 \u2502readlines\u00a0=def\u00a0readlines(hint=-1,\u00a0/):Return\u00a0a\u00a0list\u00a0of\u00a0lines\u00a0from\u00a0the\u00a0stream.\u2502 \u2502reconfigure\u00a0=def\u00a0reconfigure(*,\u00a0encoding=None,\u00a0errors=None,\u00a0newline=None,\u00a0line_buffering=None,\u00a0\u2502 \u2502write_through=None):Reconfigure\u00a0the\u00a0text\u00a0stream\u00a0with\u00a0new\u00a0parameters.\u2502 \u2502seek\u00a0=def\u00a0seek(cookie,\u00a0whence=0,\u00a0/):Change\u00a0stream\u00a0position.\u2502 \u2502seekable\u00a0=def\u00a0seekable():Return\u00a0whether\u00a0object\u00a0supports\u00a0random\u00a0access.\u2502 \u2502tell\u00a0=def\u00a0tell():Return\u00a0current\u00a0stream\u00a0position.\u2502 \u2502truncate\u00a0=def\u00a0truncate(pos=None,\u00a0/):Truncate\u00a0file\u00a0to\u00a0size\u00a0bytes.\u2502 \u2502writable\u00a0=def\u00a0writable():Return\u00a0whether\u00a0object\u00a0was\u00a0opened\u00a0for\u00a0writing.\u2502 \u2502write\u00a0=def\u00a0write(text,\u00a0/):\u2502 \u2502Write\u00a0string\u00a0to\u00a0stream.\u2502 \u2502Returns\u00a0the\u00a0number\u00a0of\u00a0characters\u00a0written\u00a0(which\u00a0is\u00a0always\u00a0equal\u00a0to\u2502 \u2502the\u00a0length\u00a0of\u00a0the\u00a0string).\u2502 \u2502writelines\u00a0=def\u00a0writelines(lines,\u00a0/):Write\u00a0a\u00a0list\u00a0of\u00a0lines\u00a0to\u00a0stream.\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

The documentation is summarized by default to avoid generating verbose reports. If you want to see the full unabbreviated help you can add help=True:

>>> inspect(text_file, methods=True, help=True)\n
Rich \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<class'_io.TextIOWrapper'>\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502Character\u00a0and\u00a0line\u00a0based\u00a0layer\u00a0over\u00a0a\u00a0BufferedIOBase\u00a0object,\u00a0buffer.\u2502 \u2502\u2502 \u2502encoding\u00a0gives\u00a0the\u00a0name\u00a0of\u00a0the\u00a0encoding\u00a0that\u00a0the\u00a0stream\u00a0will\u00a0be\u2502 \u2502decoded\u00a0or\u00a0encoded\u00a0with.\u00a0It\u00a0defaults\u00a0to\u00a0locale.getencoding().\u2502 \u2502\u2502 \u2502errors\u00a0determines\u00a0the\u00a0strictness\u00a0of\u00a0encoding\u00a0and\u00a0decoding\u00a0(see\u2502 \u2502help(codecs.Codec)\u00a0or\u00a0the\u00a0documentation\u00a0for\u00a0codecs.register)\u00a0and\u2502 \u2502defaults\u00a0to\u00a0\"strict\".\u2502 \u2502\u2502 \u2502newline\u00a0controls\u00a0how\u00a0line\u00a0endings\u00a0are\u00a0handled.\u00a0It\u00a0can\u00a0be\u00a0None,\u00a0'',\u2502 \u2502'\\n',\u00a0'\\r',\u00a0and\u00a0'\\r\\n'.\u00a0\u00a0It\u00a0works\u00a0as\u00a0follows:\u2502 \u2502\u2502 \u2502*\u00a0On\u00a0input,\u00a0if\u00a0newline\u00a0is\u00a0None,\u00a0universal\u00a0newlines\u00a0mode\u00a0is\u2502 \u2502\u00a0\u00a0enabled.\u00a0Lines\u00a0in\u00a0the\u00a0input\u00a0can\u00a0end\u00a0in\u00a0'\\n',\u00a0'\\r',\u00a0or\u00a0'\\r\\n',\u00a0and\u2502 \u2502\u00a0\u00a0these\u00a0are\u00a0translated\u00a0into\u00a0'\\n'\u00a0before\u00a0being\u00a0returned\u00a0to\u00a0the\u2502 \u2502\u00a0\u00a0caller.\u00a0If\u00a0it\u00a0is\u00a0'',\u00a0universal\u00a0newline\u00a0mode\u00a0is\u00a0enabled,\u00a0but\u00a0line\u2502 \u2502\u00a0\u00a0endings\u00a0are\u00a0returned\u00a0to\u00a0the\u00a0caller\u00a0untranslated.\u00a0If\u00a0it\u00a0has\u00a0any\u00a0of\u2502 \u2502\u00a0\u00a0the\u00a0other\u00a0legal\u00a0values,\u00a0input\u00a0lines\u00a0are\u00a0only\u00a0terminated\u00a0by\u00a0the\u00a0given\u2502 \u2502\u00a0\u00a0string,\u00a0and\u00a0the\u00a0line\u00a0ending\u00a0is\u00a0returned\u00a0to\u00a0the\u00a0caller\u00a0untranslated.\u2502 \u2502\u2502 \u2502*\u00a0On\u00a0output,\u00a0if\u00a0newline\u00a0is\u00a0None,\u00a0any\u00a0'\\n'\u00a0characters\u00a0written\u00a0are\u2502 \u2502\u00a0\u00a0translated\u00a0to\u00a0the\u00a0system\u00a0default\u00a0line\u00a0separator,\u00a0os.linesep.\u00a0If\u2502 \u2502\u00a0\u00a0newline\u00a0is\u00a0''\u00a0or\u00a0'\\n',\u00a0no\u00a0translation\u00a0takes\u00a0place.\u00a0If\u00a0newline\u00a0is\u00a0any\u2502 \u2502\u00a0\u00a0of\u00a0the\u00a0other\u00a0legal\u00a0values,\u00a0any\u00a0'\\n'\u00a0characters\u00a0written\u00a0are\u00a0translated\u2502 \u2502\u00a0\u00a0to\u00a0the\u00a0given\u00a0string.\u2502 \u2502\u2502 \u2502If\u00a0line_buffering\u00a0is\u00a0True,\u00a0a\u00a0call\u00a0to\u00a0flush\u00a0is\u00a0implied\u00a0when\u00a0a\u00a0call\u00a0to\u2502 \u2502write\u00a0contains\u00a0a\u00a0newline\u00a0character.\u2502 \u2502\u2502 \u2502\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u2502 \u2502\u2502<_io.TextIOWrappername='foo.txt'mode='w'encoding='UTF-8'>\u2502\u2502 \u2502\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2502 \u2502\u2502 \u2502buffer\u00a0=<_io.BufferedWritername='foo.txt'>\u2502 \u2502closed\u00a0=False\u2502 \u2502encoding\u00a0='UTF-8'\u2502 \u2502errors\u00a0='strict'\u2502 \u2502line_buffering\u00a0=False\u2502 \u2502mode\u00a0='w'\u2502 \u2502name\u00a0='foo.txt'\u2502 \u2502newlines\u00a0=None\u2502 \u2502write_through\u00a0=False\u2502 \u2502close\u00a0=def\u00a0close():\u2502 \u2502Flush\u00a0and\u00a0close\u00a0the\u00a0IO\u00a0object.\u2502 \u2502\u2502 \u2502This\u00a0method\u00a0has\u00a0no\u00a0effect\u00a0if\u00a0the\u00a0file\u00a0is\u00a0already\u00a0closed.\u2502 \u2502detach\u00a0=def\u00a0detach():\u2502 \u2502Separate\u00a0the\u00a0underlying\u00a0buffer\u00a0from\u00a0the\u00a0TextIOBase\u00a0and\u00a0return\u00a0it.\u2502 \u2502\u2502 \u2502After\u00a0the\u00a0underlying\u00a0buffer\u00a0has\u00a0been\u00a0detached,\u00a0the\u00a0TextIO\u00a0is\u00a0in\u00a0an\u2502 \u2502unusable\u00a0state.\u2502 \u2502fileno\u00a0=def\u00a0fileno():\u2502 \u2502Returns\u00a0underlying\u00a0file\u00a0descriptor\u00a0if\u00a0one\u00a0exists.\u2502 \u2502\u2502 \u2502OSError\u00a0is\u00a0raised\u00a0if\u00a0the\u00a0IO\u00a0object\u00a0does\u00a0not\u00a0use\u00a0a\u00a0file\u00a0descriptor.\u2502 \u2502flush\u00a0=def\u00a0flush():\u2502 \u2502Flush\u00a0write\u00a0buffers,\u00a0if\u00a0applicable.\u2502 \u2502\u2502 \u2502This\u00a0is\u00a0not\u00a0implemented\u00a0for\u00a0read-only\u00a0and\u00a0non-blocking\u00a0streams.\u2502 \u2502isatty\u00a0=def\u00a0isatty():\u2502 \u2502Return\u00a0whether\u00a0this\u00a0is\u00a0an\u00a0'interactive'\u00a0stream.\u2502 \u2502\u2502 \u2502Return\u00a0False\u00a0if\u00a0it\u00a0can't\u00a0be\u00a0determined.\u2502 \u2502read\u00a0=def\u00a0read(size=-1,\u00a0/):\u2502 \u2502Read\u00a0at\u00a0most\u00a0n\u00a0characters\u00a0from\u00a0stream.\u2502 \u2502\u2502 \u2502Read\u00a0from\u00a0underlying\u00a0buffer\u00a0until\u00a0we\u00a0have\u00a0n\u00a0characters\u00a0or\u00a0we\u00a0hit\u00a0EOF.\u2502 \u2502If\u00a0n\u00a0is\u00a0negative\u00a0or\u00a0omitted,\u00a0read\u00a0until\u00a0EOF.\u2502 \u2502readable\u00a0=def\u00a0readable():\u2502 \u2502Return\u00a0whether\u00a0object\u00a0was\u00a0opened\u00a0for\u00a0reading.\u2502 \u2502\u2502 \u2502If\u00a0False,\u00a0read()\u00a0will\u00a0raise\u00a0OSError.\u2502 \u2502readline\u00a0=def\u00a0readline(size=-1,\u00a0/):\u2502 \u2502Read\u00a0until\u00a0newline\u00a0or\u00a0EOF.\u2502 \u2502\u2502 \u2502Returns\u00a0an\u00a0empty\u00a0string\u00a0if\u00a0EOF\u00a0is\u00a0hit\u00a0immediately.\u2502 \u2502readlines\u00a0=def\u00a0readlines(hint=-1,\u00a0/):\u2502 \u2502Return\u00a0a\u00a0list\u00a0of\u00a0lines\u00a0from\u00a0the\u00a0stream.\u2502 \u2502\u2502 \u2502hint\u00a0can\u00a0be\u00a0specified\u00a0to\u00a0control\u00a0the\u00a0number\u00a0of\u00a0lines\u00a0read:\u00a0no\u00a0more\u2502 \u2502lines\u00a0will\u00a0be\u00a0read\u00a0if\u00a0the\u00a0total\u00a0size\u00a0(in\u00a0bytes/characters)\u00a0of\u00a0all\u2502 \u2502lines\u00a0so\u00a0far\u00a0exceeds\u00a0hint.\u2502 \u2502reconfigure\u00a0=def\u00a0reconfigure(*,\u00a0encoding=None,\u00a0errors=None,\u00a0newline=None,\u00a0line_buffering=None,\u00a0\u2502 \u2502write_through=None):\u2502 \u2502Reconfigure\u00a0the\u00a0text\u00a0stream\u00a0with\u00a0new\u00a0parameters.\u2502 \u2502\u2502 \u2502This\u00a0also\u00a0does\u00a0an\u00a0implicit\u00a0stream\u00a0flush.\u2502 \u2502seek\u00a0=def\u00a0seek(cookie,\u00a0whence=0,\u00a0/):\u2502 \u2502Change\u00a0stream\u00a0position.\u2502 \u2502\u2502 \u2502Change\u00a0the\u00a0stream\u00a0position\u00a0to\u00a0the\u00a0given\u00a0byte\u00a0offset.\u00a0The\u00a0offset\u00a0is\u2502 \u2502interpreted\u00a0relative\u00a0to\u00a0the\u00a0position\u00a0indicated\u00a0by\u00a0whence.\u00a0\u00a0Values\u2502 \u2502for\u00a0whence\u00a0are:\u2502 \u2502\u2502 \u2502*\u00a00\u00a0--\u00a0start\u00a0of\u00a0stream\u00a0(the\u00a0default);\u00a0offset\u00a0should\u00a0be\u00a0zero\u00a0or\u00a0positive\u2502 \u2502*\u00a01\u00a0--\u00a0current\u00a0stream\u00a0position;\u00a0offset\u00a0may\u00a0be\u00a0negative\u2502 \u2502*\u00a02\u00a0--\u00a0end\u00a0of\u00a0stream;\u00a0offset\u00a0is\u00a0usually\u00a0negative\u2502 \u2502\u2502 \u2502Return\u00a0the\u00a0new\u00a0absolute\u00a0position.\u2502 \u2502seekable\u00a0=def\u00a0seekable():\u2502 \u2502Return\u00a0whether\u00a0object\u00a0supports\u00a0random\u00a0access.\u2502 \u2502\u2502 \u2502If\u00a0False,\u00a0seek(),\u00a0tell()\u00a0and\u00a0truncate()\u00a0will\u00a0raise\u00a0OSError.\u2502 \u2502This\u00a0method\u00a0may\u00a0need\u00a0to\u00a0do\u00a0a\u00a0test\u00a0seek().\u2502 \u2502tell\u00a0=def\u00a0tell():Return\u00a0current\u00a0stream\u00a0position.\u2502 \u2502truncate\u00a0=def\u00a0truncate(pos=None,\u00a0/):\u2502 \u2502Truncate\u00a0file\u00a0to\u00a0size\u00a0bytes.\u2502 \u2502\u2502 \u2502File\u00a0pointer\u00a0is\u00a0left\u00a0unchanged.\u00a0\u00a0Size\u00a0defaults\u00a0to\u00a0the\u00a0current\u00a0IO\u2502 \u2502position\u00a0as\u00a0reported\u00a0by\u00a0tell().\u00a0\u00a0Returns\u00a0the\u00a0new\u00a0size.\u2502 \u2502writable\u00a0=def\u00a0writable():\u2502 \u2502Return\u00a0whether\u00a0object\u00a0was\u00a0opened\u00a0for\u00a0writing.\u2502 \u2502\u2502 \u2502If\u00a0False,\u00a0write()\u00a0will\u00a0raise\u00a0OSError.\u2502 \u2502write\u00a0=def\u00a0write(text,\u00a0/):\u2502 \u2502Write\u00a0string\u00a0to\u00a0stream.\u2502 \u2502Returns\u00a0the\u00a0number\u00a0of\u00a0characters\u00a0written\u00a0(which\u00a0is\u00a0always\u00a0equal\u00a0to\u2502 \u2502the\u00a0length\u00a0of\u00a0the\u00a0string).\u2502 \u2502writelines\u00a0=def\u00a0writelines(lines,\u00a0/):\u2502 \u2502Write\u00a0a\u00a0list\u00a0of\u00a0lines\u00a0to\u00a0stream.\u2502 \u2502\u2502 \u2502Line\u00a0separators\u00a0are\u00a0not\u00a0added,\u00a0so\u00a0it\u00a0is\u00a0usual\u00a0for\u00a0each\u00a0of\u00a0the\u2502 \u2502lines\u00a0provided\u00a0to\u00a0have\u00a0a\u00a0line\u00a0separator\u00a0at\u00a0the\u00a0end.\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

There are a few more arguments to refine the level of detail you need (private methods, dunder attributes etc). You can see the full range of options with this delightful little incantation:

>>> inspect(inspect)\n

If you are interested in Rich or Textual, join our Discord server!

"},{"location":"blog/2023/07/27/using-rich-inspect-to-interrogate-python-objects/#addendum","title":"Addendum","text":"

Here's how to have inspect always available without an explicit import:

Put this in your pythonrc file: pic.twitter.com/pXTi69ykZL

\u2014 Tushar Sadhwani (@sadhlife) July 27, 2023"},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/","title":"Spinners and progress bars in Textual","text":"

One of the things I love about mathematics is that you can solve a problem just by guessing the correct answer. That is a perfectly valid strategy for solving a problem. The only thing you need to do after guessing the answer is to prove that your guess is correct.

I used this strategy, to some success, to display spinners and indeterminate progress bars from Rich in Textual.

"},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/#display-an-indeterminate-progress-bar-in-textual","title":"Display an indeterminate progress bar in Textual","text":"

I have been playing around with Textual and recently I decided I needed an indeterminate progress bar to show that some data was loading. Textual is likely to get progress bars in the future, but I don't want to wait for the future! I want my progress bars now! Textual builds on top of Rich, so if Rich has progress bars, I reckoned I could use them in my Textual apps.

"},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/#progress-bars-in-rich","title":"Progress bars in Rich","text":"

Creating a progress bar in Rich is as easy as opening up the documentation for Progress and copying & pasting the code.

CodeOutput
import time\nfrom rich.progress import track\nfor _ in track(range(20), description=\"Processing...\"):\ntime.sleep(0.5)  # Simulate work being done\n

The function track provides a very convenient interface for creating progress bars that keep track of a well-specified number of steps. In the example above, we were keeping track of some task that was going to take 20 steps to complete. (For example, if we had to process a list with 20 elements.) However, I am looking for indeterminate progress bars.

Scrolling further down the documentation for rich.progress I found what I was looking for:

CodeOutput
import time\nfrom rich.progress import Progress\nwith Progress() as progress:\n_ = progress.add_task(\"Loading...\", total=None)  # (1)!\nwhile True:\ntime.sleep(0.01)\n
  1. Setting total=None is what makes it an indeterminate progress bar.

So, putting an indeterminate progress bar on the screen is easy. Now, I only needed to glue that together with the little I know about Textual to put an indeterminate progress bar in a Textual app.

"},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/#guessing-what-is-what-and-what-goes-where","title":"Guessing what is what and what goes where","text":"

What I want is to have an indeterminate progress bar inside my Textual app. Something that looks like this:

The GIF above shows just the progress bar. Obviously, the end goal is to have the progress bar be part of a Textual app that does something.

So, when I set out to do this, my first thought went to the stopwatch app in the Textual tutorial because it has a widget that updates automatically, the TimeDisplay. Below you can find the essential part of the code for the TimeDisplay widget and a small animation of it updating when the stopwatch is started.

TimeDisplay widgetOutput
from time import monotonic\nfrom textual.reactive import reactive\nfrom textual.widgets import Static\nclass TimeDisplay(Static):\n\"\"\"A widget to display elapsed time.\"\"\"\nstart_time = reactive(monotonic)\ntime = reactive(0.0)\ntotal = reactive(0.0)\ndef on_mount(self) -> None:\n\"\"\"Event handler called when widget is added to the app.\"\"\"\nself.update_timer = self.set_interval(1 / 60, self.update_time, pause=True)\ndef update_time(self) -> None:\n\"\"\"Method to update time to current.\"\"\"\nself.time = self.total + (monotonic() - self.start_time)\ndef watch_time(self, time: float) -> None:\n\"\"\"Called when the time attribute changes.\"\"\"\nminutes, seconds = divmod(time, 60)\nhours, minutes = divmod(minutes, 60)\nself.update(f\"{hours:02,.0f}:{minutes:02.0f}:{seconds:05.2f}\")\n

The reason the time display updates magically is due to the three methods that I highlighted in the code above:

  1. The method on_mount is called when the TimeDisplay widget is mounted on the app and, in it, we use the method set_interval to let Textual know that every 1 / 60 seconds we would like to call the method update_time. (In other words, we would like update_time to be called 60 times per second.)
  2. In turn, the method update_time (which is called automatically a bunch of times per second) will update the reactive attribute time. When this attribute update happens, the method watch_time kicks in.
  3. The method watch_time is a watcher method and gets called whenever the attribute self.time is assigned to. So, if the method update_time is called a bunch of times per second, the watcher method watch_time is also called a bunch of times per second. In it, we create a nice representation of the time that has elapsed and we use the method update to update the time that is being displayed.

I thought it would be reasonable if a similar mechanism needed to be in place for my progress bar, but then I realised that the progress bar seems to update itself... Looking at the indeterminate progress bar example from before, the only thing going on was that we used time.sleep to stop our program for a bit. We didn't do anything to update the progress bar... Look:

with Progress() as progress:\n_ = progress.add_task(\"Loading...\", total=None)  # (1)!\nwhile True:\ntime.sleep(0.01)\n

After pondering about this for a bit, I realised I would not need a watcher method for anything. The watcher method would only make sense if I needed to update an attribute related to some sort of artificial progress, but that clearly isn't needed to get the bar going...

At some point, I realised that the object progress is the object of interest. At first, I thought progress.add_task would return the progress bar, but it actually returns the integer ID of the task added, so the object of interest is progress. Because I am doing nothing to update the bar explicitly, the object progress must be updating itself.

The Textual documentation also says that we can build widgets from Rich renderables, so I concluded that if Progress were a renderable, then I could inherit from Static and use the method update to update the widget with my instance of Progress directly. I gave it a try and I put together this code:

from rich.progress import Progress, BarColumn\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass IndeterminateProgress(Static):\ndef __init__(self):\nsuper().__init__(\"\")\nself._bar = Progress(BarColumn())  # (1)!\nself._bar.add_task(\"\", total=None)  # (2)!\ndef on_mount(self) -> None:\n# When the widget is mounted start updating the display regularly.\nself.update_render = self.set_interval(\n1 / 60, self.update_progress_bar\n)  # (3)!\ndef update_progress_bar(self) -> None:\nself.update(self._bar)  # (4)!\nclass MyApp(App):\ndef compose(self) -> ComposeResult:\nyield IndeterminateProgress()\nif __name__ == \"__main__\":\napp = MyApp()\napp.run()\n
  1. Create an instance of Progress that just cares about the bar itself (Rich progress bars can have a label, an indicator for the time left, etc).
  2. We add the indeterminate task with total=None for the indeterminate progress bar.
  3. When the widget is mounted on the app, we want to start calling update_progress_bar 60 times per second.
  4. To update the widget of the progress bar we just call the method Static.update with the Progress object because self._bar is a Rich renderable.

And lo and behold, it worked:

"},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/#proving-it-works","title":"Proving it works","text":"

I finished writing this piece of code and I was ecstatic because it was working! After all, my Textual app starts and renders the progress bar. And so, I shared this simple app with someone who wanted to do a similar thing, but I was left with a bad taste in my mouth because I couldn't really connect all the dots and explain exactly why it worked.

Plot twist

By the end of the blog post, I will be much closer to a full explanation!

"},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/#display-a-rich-spinner-in-a-textual-app","title":"Display a Rich spinner in a Textual app","text":"

A day after creating my basic IndeterminateProgress widget, I found someone that was trying to display a Rich spinner in a Textual app. Actually, it was someone that had filed an issue against Rich. They didn't ask \u201chow can I display a Rich spinner in a Textual app?\u201d, but they filed an alleged bug that crept up on them when they tried displaying a spinner in a Textual app.

When reading the issue I realised that displaying a Rich spinner looked very similar to displaying a Rich progress bar, so I made a tiny change to my code and tried to run it:

CodeSpinner running
from rich.spinner import Spinner\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass SpinnerWidget(Static):\ndef __init__(self):\nsuper().__init__(\"\")\nself._spinner = Spinner(\"moon\")  # (1)!\ndef on_mount(self) -> None:\nself.update_render = self.set_interval(1 / 60, self.update_spinner)\ndef update_spinner(self) -> None:\nself.update(self._spinner)\nclass MyApp(App[None]):\ndef compose(self) -> ComposeResult:\nyield SpinnerWidget()\nMyApp().run()\n
  1. Instead of creating an instance of Progress, we create an instance of Spinner and save it so we can call self.update(self._spinner) later on.

"},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/#losing-the-battle-against-pausing-the-animations","title":"Losing the battle against pausing the animations","text":"

After creating the progress bar and spinner widgets I thought of creating the little display that was shown at the beginning of the blog post:

When writing the code for this app, I realised both widgets had a lot of shared code and logic and I tried abstracting away their common functionality. That led to the code shown below (more or less) where I implemented the updating functionality in IntervalUpdater and then let the IndeterminateProgressBar and SpinnerWidget instantiate the correct Rich renderable.

from rich.progress import Progress, BarColumn\nfrom rich.spinner import Spinner\nfrom textual.app import RenderableType\nfrom textual.widgets import Button, Static\nclass IntervalUpdater(Static):\n_renderable_object: RenderableType  # (1)!\ndef update_rendering(self) -> None:  # (2)!\nself.update(self._renderable_object)\ndef on_mount(self) -> None:  # (3)!\nself.interval_update = self.set_interval(1 / 60, self.update_rendering)\nclass IndeterminateProgressBar(IntervalUpdater):\n\"\"\"Basic indeterminate progress bar widget based on rich.progress.Progress.\"\"\"\ndef __init__(self) -> None:\nsuper().__init__(\"\")\nself._renderable_object = Progress(BarColumn())  # (4)!\nself._renderable_object.add_task(\"\", total=None)\nclass SpinnerWidget(IntervalUpdater):\n\"\"\"Basic spinner widget based on rich.spinner.Spinner.\"\"\"\ndef __init__(self, style: str) -> None:\nsuper().__init__(\"\")\nself._renderable_object = Spinner(style)  # (5)!\n
  1. Instances of IntervalUpdate should set the attribute _renderable_object to the instance of the Rich renderable that we want to animate.
  2. The methods update_rendering and on_mount are exactly the same as what we had before, both in the progress bar widget and in the spinner widget.
  3. The methods update_rendering and on_mount are exactly the same as what we had before, both in the progress bar widget and in the spinner widget.
  4. For an indeterminate progress bar we set the attribute _renderable_object to an instance of Progress.
  5. For a spinner we set the attribute _renderable_object to an instance of Spinner.

But I wanted something more! I wanted to make my app similar to the stopwatch app from the terminal and thus wanted to add a \u201cPause\u201d and a \u201cResume\u201d button. These buttons should, respectively, stop the progress bar and the spinner animations and resume them.

Below you can see the code I wrote and a short animation of the app working.

App codeCSSOutput
from rich.progress import Progress, BarColumn\nfrom rich.spinner import Spinner\nfrom textual.app import App, ComposeResult, RenderableType\nfrom textual.containers import Grid, Horizontal, Vertical\nfrom textual.widgets import Button, Static\nclass IntervalUpdater(Static):\n_renderable_object: RenderableType\ndef update_rendering(self) -> None:\nself.update(self._renderable_object)\ndef on_mount(self) -> None:\nself.interval_update = self.set_interval(1 / 60, self.update_rendering)\ndef pause(self) -> None:  # (1)!\nself.interval_update.pause()\ndef resume(self) -> None:  # (2)!\nself.interval_update.resume()\nclass IndeterminateProgressBar(IntervalUpdater):\n\"\"\"Basic indeterminate progress bar widget based on rich.progress.Progress.\"\"\"\ndef __init__(self) -> None:\nsuper().__init__(\"\")\nself._renderable_object = Progress(BarColumn())\nself._renderable_object.add_task(\"\", total=None)\nclass SpinnerWidget(IntervalUpdater):\n\"\"\"Basic spinner widget based on rich.spinner.Spinner.\"\"\"\ndef __init__(self, style: str) -> None:\nsuper().__init__(\"\")\nself._renderable_object = Spinner(style)\nclass LiveDisplayApp(App[None]):\n\"\"\"App showcasing some widgets that update regularly.\"\"\"\nCSS_PATH = \"myapp.css\"\ndef compose(self) -> ComposeResult:\nyield Vertical(\nGrid(\nSpinnerWidget(\"moon\"),\nIndeterminateProgressBar(),\nSpinnerWidget(\"aesthetic\"),\nSpinnerWidget(\"bouncingBar\"),\nSpinnerWidget(\"earth\"),\nSpinnerWidget(\"dots8Bit\"),\n),\nHorizontal(\nButton(\"Pause\", id=\"pause\"),  # (3)!\nButton(\"Resume\", id=\"resume\", disabled=True),\n),\n)\ndef on_button_pressed(self, event: Button.Pressed) -> None:  # (4)!\npressed_id = event.button.id\nassert pressed_id is not None\nfor widget in self.query(IntervalUpdater):\ngetattr(widget, pressed_id)()  # (5)!\nfor button in self.query(Button):  # (6)!\nif button.id == pressed_id:\nbutton.disabled = True\nelse:\nbutton.disabled = False\nLiveDisplayApp().run()\n
  1. The method pause looks at the attribute interval_update (returned by the method set_interval) and tells it to stop calling the method update_rendering 60 times per second.
  2. The method resume looks at the attribute interval_update (returned by the method set_interval) and tells it to resume calling the method update_rendering 60 times per second.
  3. We set two distinct IDs for the two buttons so we can easily tell which button was pressed and what the press of that button means.
  4. The event handler on_button_pressed will wait for button presses and will take care of pausing or resuming the animations.
  5. We look for all of the instances of IntervalUpdater in our app and use a little bit of introspection to call the correct method (pause or resume) in our widgets. Notice this was only possible because the buttons were assigned IDs that matched the names of the methods. (I love Python !)
  6. We go through our two buttons to disable the one that was just pressed and to enable the other one.
Screen {\nalign: center middle;\n}\nHorizontal {\nheight: 1fr;\nalign-horizontal: center;\n}\nButton {\nmargin: 0 3 0 3;\n}\nGrid {\nheight: 4fr;\nalign: center middle;\ngrid-size: 3 2;\ngrid-columns: 8;\ngrid-rows: 1;\ngrid-gutter: 1;\nborder: gray double;\n}\nIntervalUpdater {\ncontent-align: center middle;\n}\n

If you think this was a lot, take a couple of deep breaths before moving on.

The only issue with my app is that... it does not work! If you press the button to pause the animations, it looks like the widgets are paused. However, you can see that if I move my mouse over the paused widgets, they update:

Obviously, that caught me by surprise, in the sense that I expected it work. On the other hand, this isn't surprising. After all, I thought I had guessed how I could solve the problem of displaying these Rich renderables that update live and I thought I knew how to pause and resume their animations, but I hadn't convinced myself I knew exactly why it worked.

Warning

This goes to show that sometimes it is not the best idea to commit code that you wrote and that works if you don't know why it works. The code might seem to work and yet have deficiencies that will hurt you further down the road.

As it turns out, the reason why pausing is not working is that I did not grok why the rendering worked in the first place... So I had to go down that rabbit hole first.

"},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/#understanding-the-rich-rendering-magic","title":"Understanding the Rich rendering magic","text":""},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/#how-staticupdate-works","title":"How Static.update works","text":"

The most basic way of creating a Textual widget is to inherit from Widget and implement the method render that just returns the thing that must be printed on the screen. Then, the widget Static provides some functionality on top of that: the method update.

The method Static.update(renderable) is used to tell the widget in question that its method render (called when the widget needs to be drawn) should just return renderable. So, if the implementation of the method IntervalUpdater.update_rendering (the method that gets called 60 times per second) is this:

class IntervalUpdater(Static):\n# ...\ndef update_rendering(self) -> None:\nself.update(self._renderable_object)\n

Then, we are essentially saying \u201chey, the thing in self._renderable_object is what must be returned whenever Textual asks you to render yourself. So, this really proves that both Progress and Spinner from Rich are renderables. But what is more, this shows that my implementation of IntervalUpdater can be simplified greatly! In fact, we can boil it down to just this:

class IntervalUpdater(Static):\n_renderable_object: RenderableType\ndef __init__(self, renderable_object: RenderableType) -> None:  # (1)!\nsuper().__init__(renderable_object)  # (2)!\ndef on_mount(self) -> None:\nself.interval_update = self.set_interval(1 / 60, self.refresh)  # (3)!\n
  1. To create an instance of IntervalUpdater, now we give it the Rich renderable that we want displayed. If this Rich renderable is something that updates over time, then those changes will be reflected in the rendering.
  2. We initialise Static with the renderable object itself, instead of initialising with the empty string \"\" and then updating repeatedly.
  3. We call self.refresh 60 times per second. We don't need the auxiliary method update_rendering because this widget (an instance of Static) already knows what its renderable is.

Once you understand the code above you will realise that the previous implementation of update_rendering was actually doing superfluous work because the repeated calls to self.update always had the exact same object. Again, we see strong evidence that the Rich progress bars and the spinners have the inherent ability to display a different representation of themselves as time goes by.

"},{"location":"blog/2022/11/24/spinners-and-progress-bars-in-textual/#how-rich-spinners-get-updated","title":"How Rich spinners get updated","text":"

I kept seeing strong evidence that Rich spinners and Rich progress bars updated their own rendering but I still did not have actual proof. So, I went digging around to see how Spinner was implemented and I found this code (from the file spinner.py at the time of writing):

class Spinner:\n# ...\ndef __rich_console__(\nself, console: \"Console\", options: \"ConsoleOptions\"\n) -> \"RenderResult\":\nyield self.render(console.get_time())  # (1)!\n# ...\ndef render(self, time: float) -> \"RenderableType\":  # (2)!\n# ...\nframe_no = ((time - self.start_time) * self.speed) / (  # (3)!\nself.interval / 1000.0\n) + self.frame_no_offset\n# ...\n# ...\n
  1. The Rich spinner implements the function __rich_console__ that is supposed to return the result of rendering the spinner. Instead, it defers its work to the method render... However, to call the method render, we need to pass the argument console.get_time(), which the spinner uses to know in which state it is!
  2. The method render takes a time and returns a renderable!
  3. To determine the frame number (the current look of the spinner) we do some calculations with the \u201ccurrent time\u201d, given by the parameter time, and the time when the spinner started!

The snippet of code shown above, from the implementation of Spinner, explains why moving the mouse over a spinner (or a progress bar) that supposedly was paused makes it move. We no longer get repeated updates (60 times per second) because we told our app that we wanted to pause the result of set_interval, so we no longer get automatic updates. However, moving the mouse over the spinners and the progress bar makes Textual want to re-render them and, when it does, it figures out that time was not frozen (obviously!) and so the spinners and the progress bar have a different frame to show.

To get a better feeling for this, do the following experiment:

  1. Run the command textual console in a terminal to open the Textual devtools console.
  2. Add a print statement like print(\"Rendering from within spinner\") to the beginning of the method Spinner.render (from Rich).
  3. Add a print statement like print(\"Rendering static\") to the beginning of the method Static.render (from Textual).
  4. Put a blank terminal and the devtools console side by side.
  5. Run the app: notice that you get a lot of both print statements.
  6. Hit the Pause button: the print statements stop.
  7. Move your mouse over a widget or two: you get a couple of print statements, one from the Static.render and another from the Spinner.render.

The result of steps 6 and 7 are shown below. Notice that, in the beginning of the animation, the screen on the right shows some prints but is quiet because no more prints are coming in. When the mouse enters the screen and starts going over widgets, the screen on the right gets new prints in pairs, first from Static.render (which Textual calls to render the widget) and then from Spinner.render because ultimately we need to know how the Spinner looks.

Now, at this point, I made another educated guess and deduced that progress bars work in the same way! I still have to prove it, and I guess I will do so in another blog post, coming soon, where our spinner and progress bar widgets can be properly paused!

I will see you soon

"},{"location":"blog/2022/11/20/stealing-open-source-code-from-textual/","title":"Stealing Open Source code from Textual","text":"

I would like to talk about a serious issue in the Free and Open Source software world. Stealing code. You wouldn't steal a car would you?

But you should steal code from Open Source projects. Respect the license (you may need to give attribution) but stealing code is not like stealing a car. If I steal your car, I have deprived you of a car. If you steal my open source code, I haven't lost anything.

Warning

I'm not advocating for piracy. Open source code gives you explicit permission to use it.

From my point of view, I feel like code has greater value when it has been copied / modified in another project.

There are a number of files and modules in Textual that could either be lifted as is, or wouldn't require much work to extract. I'd like to cover a few here. You might find them useful in your next project.

"},{"location":"blog/2022/11/20/stealing-open-source-code-from-textual/#loop-first-last","title":"Loop first / last","text":"

How often do you find yourself looping over an iterable and needing to know if an element is the first and/or last in the sequence? It's a simple thing, but I find myself needing this a lot, so I wrote some helpers in _loop.py.

I'm sure there is an equivalent implementation on PyPI, but steal this if you need it.

Here's an example of use:

for last, (y, line) in loop_last(enumerate(self.lines, self.region.y)):\nyield move_to(x, y)\nyield from line\nif not last:\nyield new_line\n
"},{"location":"blog/2022/11/20/stealing-open-source-code-from-textual/#lru-cache","title":"LRU Cache","text":"

Python's lru_cache can be the one-liner that makes your code orders of magnitude faster. But it has a few gotchas.

The main issue is managing the lifetime of these caches. The decorator keeps a single global cache, which will keep a reference to every object in the function call. On an instance method that means you keep references to self for the lifetime of your app.

For a more flexibility you can use the LRUCache implementation from Textual. This uses essentially the same algorithm as the stdlib decorator, but it is implemented as a container.

Here's a quick example of its use. It works like a dictionary until you reach a maximum size. After that, new elements will kick out the element that was used least recently.

>>> from textual._cache import LRUCache\n>>> cache = LRUCache(maxsize=3)\n>>> cache[\"foo\"] = 1\n>>> cache[\"bar\"] = 2\n>>> cache[\"baz\"] = 3\n>>> dict(cache)\n{'foo': 1, 'bar': 2, 'baz': 3}\n>>> cache[\"egg\"] = 4\n>>> dict(cache)\n{'bar': 2, 'baz': 3, 'egg': 4}\n

In Textual, we use a LRUCache to store the results of rendering content to the terminal. For example, in a datatable it is too costly to render everything up front. So Textual renders only the lines that are currently visible on the \"screen\". The cache ensures that scrolling only needs to render the newly exposed lines, and lines that haven't been displayed in a while are discarded to save memory.

"},{"location":"blog/2022/11/20/stealing-open-source-code-from-textual/#color","title":"Color","text":"

Textual has a Color class which could be extracted in to a module of its own.

The Color class can parse colors encoded in a variety of HTML and CSS formats. Color object support a variety of methods and operators you can use to manipulate colors, in a fairly natural way.

Here's some examples in the REPL.

>>> from textual.color import Color\n>>> color = Color.parse(\"lime\")\n>>> color\nColor(0, 255, 0, a=1.0)\n>>> color.darken(0.8)\nColor(0, 45, 0, a=1.0)\n>>> color + Color.parse(\"red\").with_alpha(0.1)\nColor(25, 229, 0, a=1.0)\n>>> color = Color.parse(\"#12a30a\")\n>>> color\nColor(18, 163, 10, a=1.0)\n>>> color.css\n'rgb(18,163,10)'\n>>> color.hex\n'#12A30A'\n>>> color.monochrome\nColor(121, 121, 121, a=1.0)\n>>> color.monochrome.hex\n'#797979'\n>>> color.hsl\nHSL(h=0.3246187363834423, s=0.8843930635838151, l=0.33921568627450976)\n>>>\n

There are some very good color libraries in PyPI, which you should also consider using. But Textual's Color class is lean and performant, with no C dependencies.

"},{"location":"blog/2022/11/20/stealing-open-source-code-from-textual/#geometry","title":"Geometry","text":"

This may be my favorite module in Textual: geometry.py.

The geometry module contains a number of classes responsible for storing and manipulating 2D geometry. There is an Offset class which is a two dimensional point. A Region class which is a rectangular region defined by a coordinate and dimensions. There is a Spacing class which defines additional space around a region. And there is a Size class which defines the dimensions of an area by its width and height.

These objects are used by Textual's layout engine and compositor, which makes them the oldest and most thoroughly tested part of the project.

There's a lot going on in this module, but the docstrings are quite detailed and have unicode art like this to help explain things.

              cut_x \u2193\n          \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2510\n          \u2502        \u2502 \u2502   \u2502\n          \u2502    0   \u2502 \u2502 1 \u2502\n          \u2502        \u2502 \u2502   \u2502\n  cut_y \u2192 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2518\n          \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2510\n          \u2502    2   \u2502 \u2502 3 \u2502\n          \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2518\n
"},{"location":"blog/2022/11/20/stealing-open-source-code-from-textual/#you-should-steal-our-code","title":"You should steal our code","text":"

There is a lot going on in the Textual Repository. Including a CSS parser, renderer, layout and compositing engine. All written in pure Python. Steal it with my blessing.

"},{"location":"blog/2023/09/18/things-i-learned-while-building-textuals-textarea/","title":"Things I learned while building Textual's TextArea","text":"

TextArea is the latest widget to be added to Textual's growing collection. It provides a multi-line space to edit text, and features optional syntax highlighting for a selection of languages.

Adding a TextArea to your Textual app is as simple as adding this to your compose method:

yield TextArea()\n

Enabling syntax highlighting for a language is as simple as:

yield TextArea(language=\"python\")\n

Working on the TextArea widget for Textual taught me a lot about Python and my general approach to software engineering. It gave me an appreciation for the subtle functionality behind the editors we use on a daily basis \u2014 features we may not even notice, despite some engineer spending hours perfecting it to provide a small boost to our development experience.

This post is a tour of some of these learnings.

"},{"location":"blog/2023/09/18/things-i-learned-while-building-textuals-textarea/#vertical-cursor-movement-is-more-than-just-cursor_row","title":"Vertical cursor movement is more than just cursor_row++","text":"

When you move the cursor vertically, you can't simply keep the same column index and clamp it within the line. Editors should maintain the visual column offset where possible, meaning they must account for double-width emoji (sigh \ud83d\ude14) and East-Asian characters.

Notice that although the cursor is on column 11 while on line 1, it lands on column 6 when it arrives at line 3. This is because the 6th character of line 3 visually aligns with the 11th character of line 1.

"},{"location":"blog/2023/09/18/things-i-learned-while-building-textuals-textarea/#edits-from-other-sources-may-move-my-cursor","title":"Edits from other sources may move my cursor","text":"

There are two ways to interact with the TextArea:

  1. You can type into it.
  2. You can make API calls to edit the content in it.

In the example below, Hello, world!\\n is repeatedly inserted at the start of the document via the API. Notice that this updates the location of my cursor, ensuring that I don't lose my place.

This subtle feature should aid those implementing collaborative and multi-cursor editing.

This turned out to be one of the more complex features of the whole project, and went through several iterations before I was happy with the result.

Thankfully it resulted in some wonderful Tetris-esque whiteboards along the way!

A TetrisArea white-boarding session.

Sometimes stepping away from the screen and scribbling on a whiteboard with your colleagues (thanks Dave!) is what's needed to finally crack a tough problem.

Many thanks to David Brochart for sending me down this rabbit hole!

"},{"location":"blog/2023/09/18/things-i-learned-while-building-textuals-textarea/#spending-a-few-minutes-running-a-profiler-can-be-really-beneficial","title":"Spending a few minutes running a profiler can be really beneficial","text":"

While building the TextArea widget I avoided heavy optimisation work that may have affected readability or maintainability.

However, I did run a profiler in an attempt to detect flawed assumptions or mistakes which were affecting the performance of my code.

I spent around 30 minutes profiling TextArea using pyinstrument, and the result was a ~97% reduction in the time taken to handle a key press. What an amazing return on investment for such a minimal time commitment!

\"pyinstrument -r html\" produces this beautiful output.

pyinstrument unveiled two issues that were massively impacting performance.

"},{"location":"blog/2023/09/18/things-i-learned-while-building-textuals-textarea/#1-reparsing-highlighting-queries-on-each-key-press","title":"1. Reparsing highlighting queries on each key press","text":"

I was constructing a tree-sitter Query object on each key press, incorrectly assuming it was a low-overhead call. This query was completely static, so I moved it into the constructor ensuring the object was created only once. This reduced key processing time by around 94% - a substantial and very much noticeable improvement.

This seems obvious in hindsight, but the code in question was written earlier in the project and had been relegated in my mind to \"code that works correctly and will receive less attention from here on out\". pyinstrument quickly brought this code back to my attention and highlighted it as a glaring performance bug.

"},{"location":"blog/2023/09/18/things-i-learned-while-building-textuals-textarea/#2-namedtuples-are-slower-than-i-expected","title":"2. NamedTuples are slower than I expected","text":"

In Python, NamedTuples are slow to create relative to tuples, and this cost was adding up inside an extremely hot loop which was instantiating a large number of them. pyinstrument revealed that a large portion of the time during syntax highlighting was spent inside NamedTuple.__new__.

Here's a quick benchmark which constructs 10,000 NamedTuples:

\u276f hyperfine -w 2 'python sandbox/darren/make_namedtuples.py'\nBenchmark 1: python sandbox/darren/make_namedtuples.py\nTime (mean \u00b1 \u03c3):      15.9 ms \u00b1   0.5 ms    [User: 12.8 ms, System: 2.5 ms]\nRange (min \u2026 max):    15.2 ms \u2026  18.4 ms    165 runs\n

Here's the same benchmark using tuple instead:

\u276f hyperfine -w 2 'python sandbox/darren/make_tuples.py'\nBenchmark 1: python sandbox/darren/make_tuples.py\nTime (mean \u00b1 \u03c3):       9.3 ms \u00b1   0.5 ms    [User: 6.8 ms, System: 2.0 ms]\nRange (min \u2026 max):     8.7 ms \u2026  12.3 ms    256 runs\n

Switching to tuple resulted in another noticeable increase in responsiveness. Key-press handling time dropped by almost 50%! Unfortunately, this change does impact readability. However, the scope in which these tuples were used was very small, and so I felt it was a worthy trade-off.

"},{"location":"blog/2023/09/18/things-i-learned-while-building-textuals-textarea/#syntax-highlighting-is-very-different-from-what-i-expected","title":"Syntax highlighting is very different from what I expected","text":"

In order to support syntax highlighting, we make use of the tree-sitter library, which maintains a syntax tree representing the structure of our document.

To perform highlighting, we follow these steps:

  1. The user edits the document.
  2. We inform tree-sitter of the location of this edit.
  3. tree-sitter intelligently parses only the subset of the document impacted by the change, updating the tree.
  4. We run a query against the tree to retrieve ranges of text we wish to highlight.
  5. These ranges are mapped to styles (defined by the chosen \"theme\").
  6. These styles to the appropriate text ranges when rendering the widget.

Cycling through a few of the builtin themes.

Another benefit that I didn't consider before working on this project is that tree-sitter parsers can also be used to highlight syntax errors in a document. This can be useful in some situations - for example, highlighting mismatched HTML closing tags:

Highlighting mismatched closing HTML tags in red.

Before building this widget, I was oblivious as to how we might approach syntax highlighting. Without tree-sitter's incremental parsing approach, I'm not sure reasonable performance would have been feasible.

"},{"location":"blog/2023/09/18/things-i-learned-while-building-textuals-textarea/#edits-are-replacements","title":"Edits are replacements","text":"

All single-cursor edits can be distilled into a single behaviour: replace_range. This replaces a range of characters with some text. We can use this one method to easily implement deletion, insertion, and replacement of text.

  • Inserting text is replacing a zero-width range with the text to insert.
  • Pressing backspace (delete left) is just replacing the character behind the cursor with an empty string.
  • Selecting text and pressing delete is just replacing the selected text with an empty string.
  • Selecting text and pasting is replacing the selected text with some other text.

This greatly simplified my initial approach, which involved unique implementations for inserting and deleting.

"},{"location":"blog/2023/09/18/things-i-learned-while-building-textuals-textarea/#the-line-between-text-area-and-vscode-in-the-terminal","title":"The line between \"text area\" and \"VSCode in the terminal\"","text":"

A project like this has no clear finish line. There are always new features, optimisations, and refactors waiting to be made.

So where do we draw the line?

We want to provide a widget which can act as both a basic multiline text area that anyone can drop into their app, yet powerful and extensible enough to act as the foundation for a Textual-powered text editor.

Yet, the more features we add, the more opinionated the widget becomes, and the less that users will feel like they can build it into their own thing. Finding the sweet spot between feature-rich and flexible is no easy task.

I don't think the answer is clear, and I don't believe it's possible to please everyone.

Regardless, I'm happy with where we've landed, and I'm really excited to see what people build using TextArea in the future!

"},{"location":"blog/2023/09/06/what-is-textual-web/","title":"What is Textual Web?","text":"

If you know us, you will know that we are the team behind Rich and Textual \u2014 two popular Python libraries that work magic in the terminal.

Note

Not to mention Rich-CLI, Trogon, and Frogmouth

Today we are adding one project more to that lineup: textual-web.

Textual Web takes a Textual-powered TUI and turns it in to a web application. Here's a video of that in action:

With the textual-web command you can publish any Textual app on the web, making it available to anyone you send the URL to. This works without creating a socket server on your machine, so you won't have to configure firewalls and ports to share your applications.

We're excited about the possibilities here. Textual web apps are fast to spin up and tear down, and they can run just about anywhere that has an outgoing internet connection. They can be built by a single developer without any experience with a traditional web stack. All you need is proficiency in Python and a little time to read our lovely docs.

Future releases will expose more of the Web platform APIs to Textual apps, such as notifications and file system access. We plan to do this in a way that allows the same (Python) code to drive those features. For instance, a Textual app might save a file to disk in a terminal, but offer to download it in the browser.

Also in the pipeline is PWA support, so you can build terminal apps, web apps, and desktop apps with a single codebase.

Textual Web is currently in a public beta. Join our Discord server if you would like to help us test, or if you have any questions.

"},{"location":"blog/2023/06/06/to-tui-or-not-to-tui/","title":"To TUI or not to TUI","text":"

Tech moves pretty fast. If you don\u2019t stop and look around once in a while, you could miss it. And yet some technology feels like it has been around forever.

Terminals are one of those forever-technologies.

My interest is in Text User Interfaces: interactive apps that run within a terminal. I spend lot of time thinking about where TUIs might fit within the tech ecosystem, and how much more they could be doing for developers. Hardly surprising, since that is what we do at Textualize.

Recently I had the opportunity to test how new TUI projects would be received. You can consider these to be \"testing the water\", and hopefully representative of TUI apps in general.

"},{"location":"blog/2023/06/06/to-tui-or-not-to-tui/#the-projects","title":"The projects","text":"

In April we took a break from building Textual, to building apps with Textual. We had three ideas to work on, and three devs to do the work. One idea we parked for later. The other two were so promising we devoted more time to them. Both projects took around three developer-weeks to build, which also included work on Textual itself and standard duties for responding to issues / community requests. We released them in May.

The first project was Frogmouth, a Markdown browser. I think this TUI does better than the equivalent web experience in many ways. The only notable missing feature is images, and that will happen before too long.

Here's a screenshot:

Frogmouth /Users/willmcgugan/projects/textual/FAQ.md ContentsLocalBookmarksHistory\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u258e\u258a \u258eHow\u00a0do\u00a0I\u00a0pass\u00a0arguments\u00a0to\u00a0an\u00a0app?\u258a \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u258e\u258a \u2503\u25bc\u00a0\u2160\u00a0Frequently\u00a0Asked\u00a0Questions\u2503\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0Does\u00a0Textual\u00a0support\u00a0images?\u2503When\u00a0creating\u00a0your\u00a0App\u00a0class,\u00a0override\u00a0__init__\u00a0as\u00a0you\u00a0would\u00a0wheninheriting\u00a0normally.\u00a0For\u00a0example: \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0can\u00a0I\u00a0fix\u00a0ImportError\u00a0cannot\u00a0i\u2503 \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0can\u00a0I\u00a0select\u00a0and\u00a0copy\u00a0text\u00a0in\u00a0\u2503 \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0can\u00a0I\u00a0set\u00a0a\u00a0translucent\u00a0app\u00a0ba\u2503fromtextual.appimportApp,ComposeResult \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0do\u00a0I\u00a0center\u00a0a\u00a0widget\u00a0in\u00a0a\u00a0scre\u2503fromtextual.widgetsimportStatic \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0How\u00a0do\u00a0I\u00a0pass\u00a0arguments\u00a0to\u00a0an\u00a0app?\u2503 \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0Why\u00a0do\u00a0some\u00a0key\u00a0combinations\u00a0never\u2503classGreetings(App[None]): \u2503\u251c\u2500\u2500\u00a0\u2161\u00a0Why\u00a0doesn't\u00a0Textual\u00a0look\u00a0good\u00a0on\u00a0m\u2503\u2502\u00a0\u00a0\u00a0 \u2503\u2514\u2500\u2500\u00a0\u2161\u00a0Why\u00a0doesn't\u00a0Textual\u00a0support\u00a0ANSI\u00a0t\u2503\u2502\u00a0\u00a0\u00a0def__init__(self,greeting:str=\"Hello\",to_greet:str=\"World\")->None: \u2503\u2503\u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0self.greeting=greeting \u2503\u2503\u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0self.to_greet=to_greet \u2503\u2503\u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0super().__init__() \u2503\u2503\u2502\u00a0\u00a0\u00a0 \u2503\u2503\u2502\u00a0\u00a0\u00a0defcompose(self)->ComposeResult: \u2503\u2503\u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0yieldStatic(f\"{self.greeting},\u00a0{self.to_greet}\") \u2503\u2503 \u2503\u2503 \u2503\u2503Then\u00a0the\u00a0app\u00a0can\u00a0be\u00a0run,\u00a0passing\u00a0in\u00a0various\u00a0arguments;\u00a0for\u00a0example: \u2503\u2503\u2585\u2585 \u2503\u2503 \u2503\u2503#\u00a0Running\u00a0with\u00a0default\u00a0arguments. \u2503\u2503Greetings().run() \u2503\u2503 \u2503\u2503#\u00a0Running\u00a0with\u00a0a\u00a0keyword\u00a0arguyment. \u2503\u2503Greetings(to_greet=\"davep\").run()\u2585\u2585 \u2503\u2503 \u2503\u2503#\u00a0Running\u00a0with\u00a0both\u00a0positional\u00a0arguments. \u2503\u2503Greetings(\"Well\u00a0hello\",\"there\").run() \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2503\u2589\u2503\u258e\u258a \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u00a0F1\u00a0\u00a0Help\u00a0\u00a0F2\u00a0\u00a0About\u00a0\u00a0CTRL+N\u00a0\u00a0Navigation\u00a0\u00a0CTRL+Q\u00a0\u00a0Quit\u00a0

Info

Quick aside about these \"screenshots\", because its a common ask. They aren't true screenshots, but rather SVGs exported by Textual.

We posted Frogmouth on Hacker News and Reddit on a Sunday morning (US time). A day later, it had 1,000 stars and lots of positive feedback.

The second project was Trogon, a library this time. Trogon automatically creates a TUI for command line apps. Same deal: we released it on a Sunday morning, and it reached 1K stars even quicker than Frogmouth.

Trogon sqlite-utilstransform v3.31Transform\u00a0a\u00a0table\u00a0beyond\u00a0the\u00a0capabilities\u00a0of\u00a0ALTER\u00a0TABLE \u258a\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258e disable-wal\u258a\u258a\u258e\u258e drop-table\u258a\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258e drop-view\u258a\u258e\u2587\u2587 dump\u258aOptions\u258e duplicate\u258a\u258e enable-counts\u258a--type\u00a0multiple\u00a0<text\u00a0choice>\u258e enable-fts\u258a\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u258e enable-wal\u258a\u2502\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2502\u258e extract\u258a\u2502\u258a\u258e\u2502\u258e\u2585\u2585 index-foreign-keys\u258a\u2502\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u2502\u258e indexes\u258a\u2502\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2502\u258e insert\u258a\u2502\u258aSelect\u25b2\u258e\u2502\u258e insert-files\u258a\u2502\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u2502\u258e install\u258a\u2514\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2518\u258e memory\u258a\u258aSelect\u258e\u258e optimize\u258a\u258aINTEGER\u258e\u258e populate-fts\u258a\u258aTEXT\u258e\u258e query\u258a\u258aFLOAT\u258e\u258e rebuild-fts\u258a\u258a\u258aBLOB\u258e\u258e\u258e reset-counts\u258a\u258a\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258e\u258e rows\u258a\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258e schema\u258a+\u00a0value\u258e search\u258aDrop\u00a0this\u00a0column\u258e tables\u258a\u258e transform\u258a--rename\u00a0multiple\u00a0<text\u00a0text>\u258e triggers\u258a\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u258e uninstall\u258a\u2502\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2502\u258e upsert\u258a\u2502\u258a\u258e\u2502\u258e vacuum views$\u00a0sqlite-utils\u00a0transform \u00a0CTRL+R\u00a0\u00a0Close\u00a0&\u00a0Run\u00a0\u00a0CTRL+T\u00a0Focus\u00a0Command\u00a0Tree\u00a0\u00a0CTRL+O\u00a0\u00a0Command\u00a0Info\u00a0\u00a0CTRL+S\u00a0\u00a0Search\u00a0\u00a0F1\u00a0\u00a0About\u00a0

Both of these projects are very young, but off to a great start. I'm looking forward to seeing how far we can taken them.

"},{"location":"blog/2023/06/06/to-tui-or-not-to-tui/#wrapping-up","title":"Wrapping up","text":"

With previous generations of software, TUIs have required a high degree of motivation to build. That has changed with the work that we (and others) have been doing. A TUI can be a powerful and maintainable piece of software which works as a standalone project, or as a value-add to an existing project.

As a forever-technology, a TUI is a safe bet.

"},{"location":"blog/2023/06/06/to-tui-or-not-to-tui/#discord","title":"Discord","text":"

Want to discuss this post with myself or other Textualize devs? Join our Discord server...

"},{"location":"css_types/","title":"CSS Types","text":"

CSS types define the values that Textual CSS styles accept.

CSS types will be linked from within the styles reference in the \"Formal Syntax\" section of each style. The CSS types will be denoted by a keyword enclosed by angle brackets < and >.

For example, the style align-horizontal references the CSS type <horizontal>:

\nalign-horizontal: <horizontal>;\n
"},{"location":"css_types/border/","title":"<border>","text":"

The <border> CSS type represents a border style.

"},{"location":"css_types/border/#syntax","title":"Syntax","text":"

The <border> type can take any of the following values:

Border type Description ascii A border with plus, hyphen, and vertical bar characters. blank A blank border (reserves space for a border). dashed Dashed line border. double Double lined border. heavy Heavy border. hidden Alias for \"none\". hkey Horizontal key-line border. inner Thick solid border. none Disabled border. outer Solid border with additional space around content. round Rounded corners. solid Solid border. tall Solid border with additional space top and bottom. thick Border style that is consistently thick across edges. vkey Vertical key-line border. wide Solid border with additional space left and right."},{"location":"css_types/border/#border-command","title":"Border command","text":"

The textual CLI has a subcommand which will let you explore the various border types interactively, when applied to the CSS rule border:

textual borders\n
"},{"location":"css_types/border/#examples","title":"Examples","text":""},{"location":"css_types/border/#css","title":"CSS","text":"
#container {\nborder: heavy red;\n}\n#heading {\nborder-bottom: solid blue;\n}\n
"},{"location":"css_types/border/#python","title":"Python","text":"
widget.styles.border = (\"heavy\", \"red\")\nwidget.styles.border_bottom = (\"solid\", \"blue\")\n
"},{"location":"css_types/color/","title":"<color>","text":"

The <color> CSS type represents a color.

Warning

Not to be confused with the color CSS rule to set text color.

"},{"location":"css_types/color/#syntax","title":"Syntax","text":"

A <color> should be in one of the formats explained in this section. A bullet point summary of the formats available follows:

  • a recognised named color (e.g., red);
  • a 3 or 6 hexadecimal digit number representing the RGB values of the color (e.g., #F35573);
  • a 4 or 8 hexadecimal digit number representing the RGBA values of the color (e.g., #F35573A0);
  • a color description in the RGB system, with or without opacity (e.g., rgb(23, 78, 200));
  • a color description in the HSL system, with or without opacity (e.g., hsl(290, 70%, 80%));

Textual's default themes also provide many CSS variables with colors that can be used out of the box.

"},{"location":"css_types/color/#named-colors","title":"Named colors","text":"

A named color is a <name> that Textual recognises. Below, you can find a (collapsed) list of all of the named colors that Textual recognises, along with their hexadecimal values, their RGB values, and a visual sample.

All named colors available. colors \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Name\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503hex\u00a0\u00a0\u00a0\u00a0\u2503RGB\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Color\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503 \u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529 \u2502\"aliceblue\"\u2502#F0F8FF\u2502rgb(240,\u00a0248,\u00a0255)\u2502\u2502 \u2502\"ansi_black\"\u2502#000000\u2502rgb(0,\u00a00,\u00a00)\u2502\u2502 \u2502\"ansi_blue\"\u2502#000080\u2502rgb(0,\u00a00,\u00a0128)\u2502\u2502 \u2502\"ansi_bright_black\"\u2502#808080\u2502rgb(128,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"ansi_bright_blue\"\u2502#0000FF\u2502rgb(0,\u00a00,\u00a0255)\u2502\u2502 \u2502\"ansi_bright_cyan\"\u2502#00FFFF\u2502rgb(0,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"ansi_bright_green\"\u2502#00FF00\u2502rgb(0,\u00a0255,\u00a00)\u2502\u2502 \u2502\"ansi_bright_magenta\"\u2502#FF00FF\u2502rgb(255,\u00a00,\u00a0255)\u2502\u2502 \u2502\"ansi_bright_red\"\u2502#FF0000\u2502rgb(255,\u00a00,\u00a00)\u2502\u2502 \u2502\"ansi_bright_white\"\u2502#FFFFFF\u2502rgb(255,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"ansi_bright_yellow\"\u2502#FFFF00\u2502rgb(255,\u00a0255,\u00a00)\u2502\u2502 \u2502\"ansi_cyan\"\u2502#008080\u2502rgb(0,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"ansi_green\"\u2502#008000\u2502rgb(0,\u00a0128,\u00a00)\u2502\u2502 \u2502\"ansi_magenta\"\u2502#800080\u2502rgb(128,\u00a00,\u00a0128)\u2502\u2502 \u2502\"ansi_red\"\u2502#800000\u2502rgb(128,\u00a00,\u00a00)\u2502\u2502 \u2502\"ansi_white\"\u2502#C0C0C0\u2502rgb(192,\u00a0192,\u00a0192)\u2502\u2502 \u2502\"ansi_yellow\"\u2502#808000\u2502rgb(128,\u00a0128,\u00a00)\u2502\u2502 \u2502\"antiquewhite\"\u2502#FAEBD7\u2502rgb(250,\u00a0235,\u00a0215)\u2502\u2502 \u2502\"aqua\"\u2502#00FFFF\u2502rgb(0,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"aquamarine\"\u2502#7FFFD4\u2502rgb(127,\u00a0255,\u00a0212)\u2502\u2502 \u2502\"azure\"\u2502#F0FFFF\u2502rgb(240,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"beige\"\u2502#F5F5DC\u2502rgb(245,\u00a0245,\u00a0220)\u2502\u2502 \u2502\"bisque\"\u2502#FFE4C4\u2502rgb(255,\u00a0228,\u00a0196)\u2502\u2502 \u2502\"black\"\u2502#000000\u2502rgb(0,\u00a00,\u00a00)\u2502\u2502 \u2502\"blanchedalmond\"\u2502#FFEBCD\u2502rgb(255,\u00a0235,\u00a0205)\u2502\u2502 \u2502\"blue\"\u2502#0000FF\u2502rgb(0,\u00a00,\u00a0255)\u2502\u2502 \u2502\"blueviolet\"\u2502#8A2BE2\u2502rgb(138,\u00a043,\u00a0226)\u2502\u2502 \u2502\"brown\"\u2502#A52A2A\u2502rgb(165,\u00a042,\u00a042)\u2502\u2502 \u2502\"burlywood\"\u2502#DEB887\u2502rgb(222,\u00a0184,\u00a0135)\u2502\u2502 \u2502\"cadetblue\"\u2502#5F9EA0\u2502rgb(95,\u00a0158,\u00a0160)\u2502\u2502 \u2502\"chartreuse\"\u2502#7FFF00\u2502rgb(127,\u00a0255,\u00a00)\u2502\u2502 \u2502\"chocolate\"\u2502#D2691E\u2502rgb(210,\u00a0105,\u00a030)\u2502\u2502 \u2502\"coral\"\u2502#FF7F50\u2502rgb(255,\u00a0127,\u00a080)\u2502\u2502 \u2502\"cornflowerblue\"\u2502#6495ED\u2502rgb(100,\u00a0149,\u00a0237)\u2502\u2502 \u2502\"cornsilk\"\u2502#FFF8DC\u2502rgb(255,\u00a0248,\u00a0220)\u2502\u2502 \u2502\"crimson\"\u2502#DC143C\u2502rgb(220,\u00a020,\u00a060)\u2502\u2502 \u2502\"cyan\"\u2502#00FFFF\u2502rgb(0,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"darkblue\"\u2502#00008B\u2502rgb(0,\u00a00,\u00a0139)\u2502\u2502 \u2502\"darkcyan\"\u2502#008B8B\u2502rgb(0,\u00a0139,\u00a0139)\u2502\u2502 \u2502\"darkgoldenrod\"\u2502#B8860B\u2502rgb(184,\u00a0134,\u00a011)\u2502\u2502 \u2502\"darkgray\"\u2502#A9A9A9\u2502rgb(169,\u00a0169,\u00a0169)\u2502\u2502 \u2502\"darkgreen\"\u2502#006400\u2502rgb(0,\u00a0100,\u00a00)\u2502\u2502 \u2502\"darkgrey\"\u2502#A9A9A9\u2502rgb(169,\u00a0169,\u00a0169)\u2502\u2502 \u2502\"darkkhaki\"\u2502#BDB76B\u2502rgb(189,\u00a0183,\u00a0107)\u2502\u2502 \u2502\"darkmagenta\"\u2502#8B008B\u2502rgb(139,\u00a00,\u00a0139)\u2502\u2502 \u2502\"darkolivegreen\"\u2502#556B2F\u2502rgb(85,\u00a0107,\u00a047)\u2502\u2502 \u2502\"darkorange\"\u2502#FF8C00\u2502rgb(255,\u00a0140,\u00a00)\u2502\u2502 \u2502\"darkorchid\"\u2502#9932CC\u2502rgb(153,\u00a050,\u00a0204)\u2502\u2502 \u2502\"darkred\"\u2502#8B0000\u2502rgb(139,\u00a00,\u00a00)\u2502\u2502 \u2502\"darksalmon\"\u2502#E9967A\u2502rgb(233,\u00a0150,\u00a0122)\u2502\u2502 \u2502\"darkseagreen\"\u2502#8FBC8F\u2502rgb(143,\u00a0188,\u00a0143)\u2502\u2502 \u2502\"darkslateblue\"\u2502#483D8B\u2502rgb(72,\u00a061,\u00a0139)\u2502\u2502 \u2502\"darkslategray\"\u2502#2F4F4F\u2502rgb(47,\u00a079,\u00a079)\u2502\u2502 \u2502\"darkslategrey\"\u2502#2F4F4F\u2502rgb(47,\u00a079,\u00a079)\u2502\u2502 \u2502\"darkturquoise\"\u2502#00CED1\u2502rgb(0,\u00a0206,\u00a0209)\u2502\u2502 \u2502\"darkviolet\"\u2502#9400D3\u2502rgb(148,\u00a00,\u00a0211)\u2502\u2502 \u2502\"deeppink\"\u2502#FF1493\u2502rgb(255,\u00a020,\u00a0147)\u2502\u2502 \u2502\"deepskyblue\"\u2502#00BFFF\u2502rgb(0,\u00a0191,\u00a0255)\u2502\u2502 \u2502\"dimgray\"\u2502#696969\u2502rgb(105,\u00a0105,\u00a0105)\u2502\u2502 \u2502\"dimgrey\"\u2502#696969\u2502rgb(105,\u00a0105,\u00a0105)\u2502\u2502 \u2502\"dodgerblue\"\u2502#1E90FF\u2502rgb(30,\u00a0144,\u00a0255)\u2502\u2502 \u2502\"firebrick\"\u2502#B22222\u2502rgb(178,\u00a034,\u00a034)\u2502\u2502 \u2502\"floralwhite\"\u2502#FFFAF0\u2502rgb(255,\u00a0250,\u00a0240)\u2502\u2502 \u2502\"forestgreen\"\u2502#228B22\u2502rgb(34,\u00a0139,\u00a034)\u2502\u2502 \u2502\"fuchsia\"\u2502#FF00FF\u2502rgb(255,\u00a00,\u00a0255)\u2502\u2502 \u2502\"gainsboro\"\u2502#DCDCDC\u2502rgb(220,\u00a0220,\u00a0220)\u2502\u2502 \u2502\"ghostwhite\"\u2502#F8F8FF\u2502rgb(248,\u00a0248,\u00a0255)\u2502\u2502 \u2502\"gold\"\u2502#FFD700\u2502rgb(255,\u00a0215,\u00a00)\u2502\u2502 \u2502\"goldenrod\"\u2502#DAA520\u2502rgb(218,\u00a0165,\u00a032)\u2502\u2502 \u2502\"gray\"\u2502#808080\u2502rgb(128,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"green\"\u2502#008000\u2502rgb(0,\u00a0128,\u00a00)\u2502\u2502 \u2502\"greenyellow\"\u2502#ADFF2F\u2502rgb(173,\u00a0255,\u00a047)\u2502\u2502 \u2502\"grey\"\u2502#808080\u2502rgb(128,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"honeydew\"\u2502#F0FFF0\u2502rgb(240,\u00a0255,\u00a0240)\u2502\u2502 \u2502\"hotpink\"\u2502#FF69B4\u2502rgb(255,\u00a0105,\u00a0180)\u2502\u2502 \u2502\"indianred\"\u2502#CD5C5C\u2502rgb(205,\u00a092,\u00a092)\u2502\u2502 \u2502\"indigo\"\u2502#4B0082\u2502rgb(75,\u00a00,\u00a0130)\u2502\u2502 \u2502\"ivory\"\u2502#FFFFF0\u2502rgb(255,\u00a0255,\u00a0240)\u2502\u2502 \u2502\"khaki\"\u2502#F0E68C\u2502rgb(240,\u00a0230,\u00a0140)\u2502\u2502 \u2502\"lavender\"\u2502#E6E6FA\u2502rgb(230,\u00a0230,\u00a0250)\u2502\u2502 \u2502\"lavenderblush\"\u2502#FFF0F5\u2502rgb(255,\u00a0240,\u00a0245)\u2502\u2502 \u2502\"lawngreen\"\u2502#7CFC00\u2502rgb(124,\u00a0252,\u00a00)\u2502\u2502 \u2502\"lemonchiffon\"\u2502#FFFACD\u2502rgb(255,\u00a0250,\u00a0205)\u2502\u2502 \u2502\"lightblue\"\u2502#ADD8E6\u2502rgb(173,\u00a0216,\u00a0230)\u2502\u2502 \u2502\"lightcoral\"\u2502#F08080\u2502rgb(240,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"lightcyan\"\u2502#E0FFFF\u2502rgb(224,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"lightgoldenrodyellow\"\u2502#FAFAD2\u2502rgb(250,\u00a0250,\u00a0210)\u2502\u2502 \u2502\"lightgray\"\u2502#D3D3D3\u2502rgb(211,\u00a0211,\u00a0211)\u2502\u2502 \u2502\"lightgreen\"\u2502#90EE90\u2502rgb(144,\u00a0238,\u00a0144)\u2502\u2502 \u2502\"lightgrey\"\u2502#D3D3D3\u2502rgb(211,\u00a0211,\u00a0211)\u2502\u2502 \u2502\"lightpink\"\u2502#FFB6C1\u2502rgb(255,\u00a0182,\u00a0193)\u2502\u2502 \u2502\"lightsalmon\"\u2502#FFA07A\u2502rgb(255,\u00a0160,\u00a0122)\u2502\u2502 \u2502\"lightseagreen\"\u2502#20B2AA\u2502rgb(32,\u00a0178,\u00a0170)\u2502\u2502 \u2502\"lightskyblue\"\u2502#87CEFA\u2502rgb(135,\u00a0206,\u00a0250)\u2502\u2502 \u2502\"lightslategray\"\u2502#778899\u2502rgb(119,\u00a0136,\u00a0153)\u2502\u2502 \u2502\"lightslategrey\"\u2502#778899\u2502rgb(119,\u00a0136,\u00a0153)\u2502\u2502 \u2502\"lightsteelblue\"\u2502#B0C4DE\u2502rgb(176,\u00a0196,\u00a0222)\u2502\u2502 \u2502\"lightyellow\"\u2502#FFFFE0\u2502rgb(255,\u00a0255,\u00a0224)\u2502\u2502 \u2502\"lime\"\u2502#00FF00\u2502rgb(0,\u00a0255,\u00a00)\u2502\u2502 \u2502\"limegreen\"\u2502#32CD32\u2502rgb(50,\u00a0205,\u00a050)\u2502\u2502 \u2502\"linen\"\u2502#FAF0E6\u2502rgb(250,\u00a0240,\u00a0230)\u2502\u2502 \u2502\"magenta\"\u2502#FF00FF\u2502rgb(255,\u00a00,\u00a0255)\u2502\u2502 \u2502\"maroon\"\u2502#800000\u2502rgb(128,\u00a00,\u00a00)\u2502\u2502 \u2502\"mediumaquamarine\"\u2502#66CDAA\u2502rgb(102,\u00a0205,\u00a0170)\u2502\u2502 \u2502\"mediumblue\"\u2502#0000CD\u2502rgb(0,\u00a00,\u00a0205)\u2502\u2502 \u2502\"mediumorchid\"\u2502#BA55D3\u2502rgb(186,\u00a085,\u00a0211)\u2502\u2502 \u2502\"mediumpurple\"\u2502#9370DB\u2502rgb(147,\u00a0112,\u00a0219)\u2502\u2502 \u2502\"mediumseagreen\"\u2502#3CB371\u2502rgb(60,\u00a0179,\u00a0113)\u2502\u2502 \u2502\"mediumslateblue\"\u2502#7B68EE\u2502rgb(123,\u00a0104,\u00a0238)\u2502\u2502 \u2502\"mediumspringgreen\"\u2502#00FA9A\u2502rgb(0,\u00a0250,\u00a0154)\u2502\u2502 \u2502\"mediumturquoise\"\u2502#48D1CC\u2502rgb(72,\u00a0209,\u00a0204)\u2502\u2502 \u2502\"mediumvioletred\"\u2502#C71585\u2502rgb(199,\u00a021,\u00a0133)\u2502\u2502 \u2502\"midnightblue\"\u2502#191970\u2502rgb(25,\u00a025,\u00a0112)\u2502\u2502 \u2502\"mintcream\"\u2502#F5FFFA\u2502rgb(245,\u00a0255,\u00a0250)\u2502\u2502 \u2502\"mistyrose\"\u2502#FFE4E1\u2502rgb(255,\u00a0228,\u00a0225)\u2502\u2502 \u2502\"moccasin\"\u2502#FFE4B5\u2502rgb(255,\u00a0228,\u00a0181)\u2502\u2502 \u2502\"navajowhite\"\u2502#FFDEAD\u2502rgb(255,\u00a0222,\u00a0173)\u2502\u2502 \u2502\"navy\"\u2502#000080\u2502rgb(0,\u00a00,\u00a0128)\u2502\u2502 \u2502\"oldlace\"\u2502#FDF5E6\u2502rgb(253,\u00a0245,\u00a0230)\u2502\u2502 \u2502\"olive\"\u2502#808000\u2502rgb(128,\u00a0128,\u00a00)\u2502\u2502 \u2502\"olivedrab\"\u2502#6B8E23\u2502rgb(107,\u00a0142,\u00a035)\u2502\u2502 \u2502\"orange\"\u2502#FFA500\u2502rgb(255,\u00a0165,\u00a00)\u2502\u2502 \u2502\"orangered\"\u2502#FF4500\u2502rgb(255,\u00a069,\u00a00)\u2502\u2502 \u2502\"orchid\"\u2502#DA70D6\u2502rgb(218,\u00a0112,\u00a0214)\u2502\u2502 \u2502\"palegoldenrod\"\u2502#EEE8AA\u2502rgb(238,\u00a0232,\u00a0170)\u2502\u2502 \u2502\"palegreen\"\u2502#98FB98\u2502rgb(152,\u00a0251,\u00a0152)\u2502\u2502 \u2502\"paleturquoise\"\u2502#AFEEEE\u2502rgb(175,\u00a0238,\u00a0238)\u2502\u2502 \u2502\"palevioletred\"\u2502#DB7093\u2502rgb(219,\u00a0112,\u00a0147)\u2502\u2502 \u2502\"papayawhip\"\u2502#FFEFD5\u2502rgb(255,\u00a0239,\u00a0213)\u2502\u2502 \u2502\"peachpuff\"\u2502#FFDAB9\u2502rgb(255,\u00a0218,\u00a0185)\u2502\u2502 \u2502\"peru\"\u2502#CD853F\u2502rgb(205,\u00a0133,\u00a063)\u2502\u2502 \u2502\"pink\"\u2502#FFC0CB\u2502rgb(255,\u00a0192,\u00a0203)\u2502\u2502 \u2502\"plum\"\u2502#DDA0DD\u2502rgb(221,\u00a0160,\u00a0221)\u2502\u2502 \u2502\"powderblue\"\u2502#B0E0E6\u2502rgb(176,\u00a0224,\u00a0230)\u2502\u2502 \u2502\"purple\"\u2502#800080\u2502rgb(128,\u00a00,\u00a0128)\u2502\u2502 \u2502\"rebeccapurple\"\u2502#663399\u2502rgb(102,\u00a051,\u00a0153)\u2502\u2502 \u2502\"red\"\u2502#FF0000\u2502rgb(255,\u00a00,\u00a00)\u2502\u2502 \u2502\"rosybrown\"\u2502#BC8F8F\u2502rgb(188,\u00a0143,\u00a0143)\u2502\u2502 \u2502\"royalblue\"\u2502#4169E1\u2502rgb(65,\u00a0105,\u00a0225)\u2502\u2502 \u2502\"saddlebrown\"\u2502#8B4513\u2502rgb(139,\u00a069,\u00a019)\u2502\u2502 \u2502\"salmon\"\u2502#FA8072\u2502rgb(250,\u00a0128,\u00a0114)\u2502\u2502 \u2502\"sandybrown\"\u2502#F4A460\u2502rgb(244,\u00a0164,\u00a096)\u2502\u2502 \u2502\"seagreen\"\u2502#2E8B57\u2502rgb(46,\u00a0139,\u00a087)\u2502\u2502 \u2502\"seashell\"\u2502#FFF5EE\u2502rgb(255,\u00a0245,\u00a0238)\u2502\u2502 \u2502\"sienna\"\u2502#A0522D\u2502rgb(160,\u00a082,\u00a045)\u2502\u2502 \u2502\"silver\"\u2502#C0C0C0\u2502rgb(192,\u00a0192,\u00a0192)\u2502\u2502 \u2502\"skyblue\"\u2502#87CEEB\u2502rgb(135,\u00a0206,\u00a0235)\u2502\u2502 \u2502\"slateblue\"\u2502#6A5ACD\u2502rgb(106,\u00a090,\u00a0205)\u2502\u2502 \u2502\"slategray\"\u2502#708090\u2502rgb(112,\u00a0128,\u00a0144)\u2502\u2502 \u2502\"slategrey\"\u2502#708090\u2502rgb(112,\u00a0128,\u00a0144)\u2502\u2502 \u2502\"snow\"\u2502#FFFAFA\u2502rgb(255,\u00a0250,\u00a0250)\u2502\u2502 \u2502\"springgreen\"\u2502#00FF7F\u2502rgb(0,\u00a0255,\u00a0127)\u2502\u2502 \u2502\"steelblue\"\u2502#4682B4\u2502rgb(70,\u00a0130,\u00a0180)\u2502\u2502 \u2502\"tan\"\u2502#D2B48C\u2502rgb(210,\u00a0180,\u00a0140)\u2502\u2502 \u2502\"teal\"\u2502#008080\u2502rgb(0,\u00a0128,\u00a0128)\u2502\u2502 \u2502\"thistle\"\u2502#D8BFD8\u2502rgb(216,\u00a0191,\u00a0216)\u2502\u2502 \u2502\"tomato\"\u2502#FF6347\u2502rgb(255,\u00a099,\u00a071)\u2502\u2502 \u2502\"turquoise\"\u2502#40E0D0\u2502rgb(64,\u00a0224,\u00a0208)\u2502\u2502 \u2502\"violet\"\u2502#EE82EE\u2502rgb(238,\u00a0130,\u00a0238)\u2502\u2502 \u2502\"wheat\"\u2502#F5DEB3\u2502rgb(245,\u00a0222,\u00a0179)\u2502\u2502 \u2502\"white\"\u2502#FFFFFF\u2502rgb(255,\u00a0255,\u00a0255)\u2502\u2502 \u2502\"whitesmoke\"\u2502#F5F5F5\u2502rgb(245,\u00a0245,\u00a0245)\u2502\u2502 \u2502\"yellow\"\u2502#FFFF00\u2502rgb(255,\u00a0255,\u00a00)\u2502\u2502 \u2502\"yellowgreen\"\u2502#9ACD32\u2502rgb(154,\u00a0205,\u00a050)\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"},{"location":"css_types/color/#hex-rgb-value","title":"Hex RGB value","text":"

The hexadecimal RGB format starts with an octothorpe # and is then followed by 3 or 6 hexadecimal digits: 0123456789ABCDEF. Casing is ignored.

  • If 6 digits are used, the format is #RRGGBB:
  • RR represents the red channel;
  • GG represents the green channel; and
  • BB represents the blue channel.
  • If 3 digits are used, the format is #RGB.

In a 3 digit color, each channel is represented by a single digit which is duplicated when converting to the 6 digit format. For example, the color #A2F is the same as #AA22FF.

"},{"location":"css_types/color/#hex-rgba-value","title":"Hex RGBA value","text":"

This is the same as the hex RGB value, but with an extra channel for the alpha component (that sets opacity).

  • If 8 digits are used, the format is #RRGGBBAA, equivalent to the format #RRGGBB with two extra digits for opacity.
  • If 4 digits are used, the format is #RGBA, equivalent to the format #RGB with an extra digit for opacity.
"},{"location":"css_types/color/#rgb-description","title":"rgb description","text":"

The rgb format description is a functional description of a color in the RGB color space. This description follows the format rgb(red, green, blue), where red, green, and blue are decimal integers between 0 and 255. They represent the value of the channel with the same name.

For example, rgb(0, 255, 32) is equivalent to #00FF20.

"},{"location":"css_types/color/#rgba-description","title":"rgba description","text":"

The rgba format description is the same as the rgb with an extra parameter for opacity, which should be a value between 0 and 1.

For example, rgba(0, 255, 32, 0.5) is the color rgb(0, 255, 32) with 50% opacity.

"},{"location":"css_types/color/#hsl-description","title":"hsl description","text":"

The hsl format description is a functional description of a color in the HSL color space. This description follows the format hsl(hue, saturation, lightness), where

  • hue is a float between 0 and 360;
  • saturation is a percentage between 0% and 100%; and
  • lightness is a percentage between 0% and 100%.

For example, the color #00FF20 would be represented as hsl(128, 100%, 50%) in the HSL color space.

"},{"location":"css_types/color/#hsla-description","title":"hsla description","text":"

The hsla format description is the same as the hsl with an extra parameter for opacity, which should be a value between 0 and 1.

For example, hsla(128, 100%, 50%, 0.5) is the color hsl(128, 100%, 50%) with 50% opacity.

"},{"location":"css_types/color/#examples","title":"Examples","text":""},{"location":"css_types/color/#css","title":"CSS","text":"
Header {\nbackground: red;           /* Color name */\n}\n.accent {\ncolor: $accent;            /* Textual variable */\n}\n#footer {\ntint: hsl(300, 20%, 70%);  /* HSL description */\n}\n
"},{"location":"css_types/color/#python","title":"Python","text":"

In Python, rules that expect a <color> can also accept an instance of the type Color.

# Mimicking the CSS syntax\nwidget.styles.background = \"red\"           # Color name\nwidget.styles.color = \"$accent\"            # Textual variable\nwidget.styles.tint = \"hsl(300, 20%, 70%)\"  # HSL description\nfrom textual.color import Color\n# Using a Color object directly...\ncolor = Color(16, 200, 45)\n# ... which can also parse the CSS syntax\ncolor = Color.parse(\"#A8F\")\n
"},{"location":"css_types/horizontal/","title":"<horizontal>","text":"

The <horizontal> CSS type represents a position along the horizontal axis.

"},{"location":"css_types/horizontal/#syntax","title":"Syntax","text":"

The <horizontal> type can take any of the following values:

Value Description center Aligns in the center of the horizontal axis. left (default) Aligns on the left of the horizontal axis. right Aligns on the right of the horizontal axis."},{"location":"css_types/horizontal/#examples","title":"Examples","text":""},{"location":"css_types/horizontal/#css","title":"CSS","text":"
.container {\nalign-horizontal: right;\n}\n
"},{"location":"css_types/horizontal/#python","title":"Python","text":"
widget.styles.align_horizontal = \"right\"\n
"},{"location":"css_types/integer/","title":"<integer>","text":"

The <integer> CSS type represents an integer number.

"},{"location":"css_types/integer/#syntax","title":"Syntax","text":"

An <integer> is any valid integer number like -10 or 42.

Note

Some CSS rules may expect an <integer> within certain bounds. If that is the case, it will be noted in that rule.

"},{"location":"css_types/integer/#examples","title":"Examples","text":""},{"location":"css_types/integer/#css","title":"CSS","text":"
.classname {\noffset: 10 -20\n}\n
"},{"location":"css_types/integer/#python","title":"Python","text":"

In Python, a rule that expects a CSS type <integer> will expect a value of the type int:

widget.styles.offset = (10, -20)\n
"},{"location":"css_types/name/","title":"<name>","text":"

The <name> type represents a sequence of characters that identifies something.

"},{"location":"css_types/name/#syntax","title":"Syntax","text":"

A <name> is any non-empty sequence of characters:

  • starting with a letter a-z, A-Z, or underscore _; and
  • followed by zero or more letters a-zA-Z, digits 0-9, underscores _, and hiphens -.
"},{"location":"css_types/name/#examples","title":"Examples","text":""},{"location":"css_types/name/#css","title":"CSS","text":"
Screen {\nlayers: onlyLetters Letters-and-hiphens _lead-under letters-1-digit;\n}\n
"},{"location":"css_types/name/#python","title":"Python","text":"
widget.styles.layers = \"onlyLetters Letters-and-hiphens _lead-under letters-1-digit\"\n
"},{"location":"css_types/number/","title":"<number>","text":"

The <number> CSS type represents a real number, which can be an integer or a number with a decimal part (akin to a float in Python).

"},{"location":"css_types/number/#syntax","title":"Syntax","text":"

A <number> is an <integer>, optionally followed by the decimal point . and a decimal part composed of one or more digits.

"},{"location":"css_types/number/#examples","title":"Examples","text":""},{"location":"css_types/number/#css","title":"CSS","text":"
Grid {\ngrid-size: 3 6  /* Integers are numbers */\n}\n.translucid {\nopacity: 0.5    /* Numbers can have a decimal part */\n}\n
"},{"location":"css_types/number/#python","title":"Python","text":"

In Python, a rule that expects a CSS type <number> will accept an int or a float:

widget.styles.grid_size = (3, 6)  # Integers are numbers\nwidget.styles.opacity = 0.5       # Numbers can have a decimal part\n
"},{"location":"css_types/overflow/","title":"<overflow>","text":"

The <overflow> CSS type represents overflow modes.

"},{"location":"css_types/overflow/#syntax","title":"Syntax","text":"

The <overflow> type can take any of the following values:

Value Description auto Determine overflow mode automatically. hidden Don't overflow. scroll Allow overflowing."},{"location":"css_types/overflow/#examples","title":"Examples","text":""},{"location":"css_types/overflow/#css","title":"CSS","text":"
#container {\noverflow-y: hidden;  /* Don't overflow */\n}\n
"},{"location":"css_types/overflow/#python","title":"Python","text":"
widget.styles.overflow_y = \"hidden\"  # Don't overflow\n
"},{"location":"css_types/percentage/","title":"<percentage>","text":"

The <percentage> CSS type represents a percentage value. It is often used to represent values that are relative to the parent's values.

Warning

Not to be confused with the <scalar> type.

"},{"location":"css_types/percentage/#syntax","title":"Syntax","text":"

A <percentage> is a <number> followed by the percent sign % (without spaces). Some rules may clamp the values between 0% and 100%.

"},{"location":"css_types/percentage/#examples","title":"Examples","text":""},{"location":"css_types/percentage/#css","title":"CSS","text":"
#footer {\n/* Integer followed by % */\ncolor: red 70%;\n/* The number can be negative/decimal, although that may not make sense */\noffset: -30% 12.5%;\n}\n
"},{"location":"css_types/percentage/#python","title":"Python","text":"
# Integer followed by %\nwidget.styles.color = \"red 70%\"\n# The number can be negative/decimal, although that may not make sense\nwidget.styles.offset = (\"-30%\", \"12.5%\")\n
"},{"location":"css_types/scalar/","title":"<scalar>","text":"

The <scalar> CSS type represents a length. It can be a <number> and a unit, or the special value auto. It is used to represent lengths, for example in the width and height rules.

Warning

Not to be confused with the <number> or <percentage> types.

"},{"location":"css_types/scalar/#syntax","title":"Syntax","text":"

A <scalar> can be any of the following:

  • a fixed number of cells (e.g., 10);
  • a fractional proportion relative to the sizes of the other widgets (e.g., 1fr);
  • a percentage relative to the container widget (e.g., 50%);
  • a percentage relative to the container width/height (e.g., 25w/75h);
  • a percentage relative to the viewport width/height (e.g., 25vw/75vh); or
  • the special value auto to compute the optimal size to fit without scrolling.

A complete reference table and detailed explanations follow. You can skip to the examples.

Unit symbol Unit Example Description \"\" Cell 10 Number of cells (rows or columns). \"fr\" Fraction 1fr Specifies the proportion of space the widget should occupy. \"%\" Percent 75% Length relative to the container widget. \"w\" Width 25w Percentage relative to the width of the container widget. \"h\" Height 75h Percentage relative to the height of the container widget. \"vw\" Viewport width 25vw Percentage relative to the viewport width. \"vh\" Viewport height 75vh Percentage relative to the viewport height. - Auto auto Tries to compute the optimal size to fit without scrolling."},{"location":"css_types/scalar/#cell","title":"Cell","text":"

The number of cells is the only unit for a scalar that is absolute. This can be an integer or a float but floats are truncated to integers.

If used to specify a horizontal length, it corresponds to the number of columns. For example, in width: 15, this sets the width of a widget to be equal to 15 cells, which translates to 15 columns.

If used to specify a vertical length, it corresponds to the number of lines. For example, in height: 10, this sets the height of a widget to be equal to 10 cells, which translates to 10 lines.

"},{"location":"css_types/scalar/#fraction","title":"Fraction","text":"

The unit fraction is used to represent proportional sizes.

For example, if two widgets are side by side and one has width: 1fr and the other has width: 3fr, the second one will be three times as wide as the first one.

"},{"location":"css_types/scalar/#percent","title":"Percent","text":"

The percent unit matches a <percentage> and is used to specify a total length relative to the space made available by the container widget.

If used to specify a horizontal length, it will be relative to the width of the container. For example, width: 50% sets the width of a widget to 50% of the width of its container.

If used to specify a vertical length, it will be relative to the height of the container. For example, height: 50% sets the height of a widget to 50% of the height of its container.

"},{"location":"css_types/scalar/#width","title":"Width","text":"

The width unit is similar to the percent unit, except it sets the percentage to be relative to the width of the container.

For example, width: 25w sets the width of a widget to 25% of the width of its container and height: 25w sets the height of a widget to 25% of the width of its container. So, if the container has a width of 100 cells, the width and the height of the child widget will be of 25 cells.

"},{"location":"css_types/scalar/#height","title":"Height","text":"

The height unit is similar to the percent unit, except it sets the percentage to be relative to the height of the container.

For example, height: 75h sets the height of a widget to 75% of the height of its container and width: 75h sets the width of a widget to 75% of the height of its container. So, if the container has a height of 100 cells, the width and the height of the child widget will be of 75 cells.

"},{"location":"css_types/scalar/#viewport-width","title":"Viewport width","text":"

This is the same as the width unit, except that it is relative to the width of the viewport instead of the width of the immediate container. The width of the viewport is the width of the terminal minus the widths of widgets that are docked left or right.

For example, width: 25vw will try to set the width of a widget to be 25% of the viewport width, regardless of the widths of its containers.

"},{"location":"css_types/scalar/#viewport-height","title":"Viewport height","text":"

This is the same as the height unit, except that it is relative to the height of the viewport instead of the height of the immediate container. The height of the viewport is the height of the terminal minus the heights of widgets that are docked top or bottom.

For example, height: 75vh will try to set the height of a widget to be 75% of the viewport height, regardless of the height of its containers.

"},{"location":"css_types/scalar/#auto","title":"Auto","text":"

This special value will try to calculate the optimal size to fit the contents of the widget without scrolling.

For example, if its container is big enough, a label with width: auto will be just as wide as its text.

"},{"location":"css_types/scalar/#examples","title":"Examples","text":""},{"location":"css_types/scalar/#css","title":"CSS","text":"
Horizontal {\nwidth: 60;     /* 60 cells */\nheight: 1fr;   /* proportional size of 1 */\n}\n
"},{"location":"css_types/scalar/#python","title":"Python","text":"
widget.styles.width = 16       # Cell unit can be specified with an int/float\nwidget.styles.height = \"1fr\"   # proportional size of 1\n
"},{"location":"css_types/text_align/","title":"<text-align>","text":"

The <text-align> CSS type represents alignments that can be applied to text.

Warning

Not to be confused with the text-align CSS rule that sets the alignment of text in a widget.

"},{"location":"css_types/text_align/#syntax","title":"Syntax","text":"

A <text-align> can be any of the following values:

Value Alignment type center Center alignment. end Alias for right. justify Text is justified inside the widget. left Left alignment. right Right alignment. start Alias for left.

Tip

The meanings of start and end will likely change when RTL languages become supported by Textual.

"},{"location":"css_types/text_align/#examples","title":"Examples","text":""},{"location":"css_types/text_align/#css","title":"CSS","text":"
Label {\nrule: justify;\n}\n
"},{"location":"css_types/text_align/#python","title":"Python","text":"
widget.styles.text_align = \"justify\"\n
"},{"location":"css_types/text_style/","title":"<text-style>","text":"

The <text-style> CSS type represents styles that can be applied to text.

Warning

Not to be confused with the text-style CSS rule that sets the style of text in a widget.

"},{"location":"css_types/text_style/#syntax","title":"Syntax","text":"

A <text-style> can be the value none for plain text with no styling, or any space-separated combination of the following values:

Value Description bold Bold text. italic Italic text. reverse Reverse video text (foreground and background colors reversed). strike Strikethrough text. underline Underline text."},{"location":"css_types/text_style/#examples","title":"Examples","text":""},{"location":"css_types/text_style/#css","title":"CSS","text":"
#label1 {\n/* You can specify any value by itself. */\nrule: strike;\n}\n#label2 {\n/* You can also combine multiple values. */\nrule: strike bold italic reverse;\n}\n
"},{"location":"css_types/text_style/#python","title":"Python","text":"
# You can specify any value by itself\nwidget.styles.text_style = \"strike\"\n# You can also combine multiple values\nwidget.styles.text_style = \"strike bold italic reverse\n
"},{"location":"css_types/vertical/","title":"<vertical>","text":"

The <vertical> CSS type represents a position along the vertical axis.

"},{"location":"css_types/vertical/#syntax","title":"Syntax","text":"

The <vertical> type can take any of the following values:

Value Description bottom Aligns at the bottom of the vertical axis. middle Aligns in the middle of the vertical axis. top (default) Aligns at the top of the vertical axis."},{"location":"css_types/vertical/#examples","title":"Examples","text":""},{"location":"css_types/vertical/#css","title":"CSS","text":"
.container {\nalign-vertical: top;\n}\n
"},{"location":"css_types/vertical/#python","title":"Python","text":"
widget.styles.align_vertical = \"top\"\n
"},{"location":"events/","title":"Events","text":"

A reference to Textual events.

See the links to the left of the page, or in the hamburger menu (three horizontal bars, top left).

"},{"location":"events/blur/","title":"Blur","text":"

The Blur event is sent to a widget when it loses focus.

  • Bubbles
  • Verbose
"},{"location":"events/blur/#attributes","title":"Attributes","text":"

No other attributes

"},{"location":"events/blur/#code","title":"Code","text":""},{"location":"events/blur/#textual.events.Blur","title":"textual.events.Blur class","text":"

Bases: Event

Sent when a widget is blurred (un-focussed).

  • Bubbles
  • Verbose
"},{"location":"events/blur/#see-also","title":"See also","text":"
  • DescendantBlur
  • DescendantFocus
  • Focus
"},{"location":"events/click/","title":"Click","text":"

The Click event is sent to a widget when the user clicks a mouse button.

  • Bubbles
  • Verbose
"},{"location":"events/click/#attributes","title":"Attributes","text":"attribute type purpose x int Mouse x coordinate, relative to Widget y int Mouse y coordinate, relative to Widget delta_x int Change in x since last mouse event delta_y int Change in y since last mouse event button int Index of mouse button shift bool Shift key pressed if True meta bool Meta key pressed if True ctrl bool Ctrl key pressed if True screen_x int Mouse x coordinate relative to the screen screen_y int Mouse y coordinate relative to the screen"},{"location":"events/click/#code","title":"Code","text":""},{"location":"events/click/#textual.events.Click","title":"textual.events.Click class","text":"

Bases: MouseEvent

Sent when a widget is clicked.

  • Bubbles
  • Verbose
"},{"location":"events/descendant_blur/","title":"DescendantBlur","text":"

The DescendantBlur event is sent to a widget when one of its children loses focus.

  • Bubbles
  • Verbose
"},{"location":"events/descendant_blur/#attributes","title":"Attributes","text":"

No other attributes

"},{"location":"events/descendant_blur/#code","title":"Code","text":""},{"location":"events/descendant_blur/#textual.events.DescendantBlur","title":"textual.events.DescendantBlur class","text":"

Bases: Event

Sent when a child widget is blurred.

  • Bubbles
  • Verbose
"},{"location":"events/descendant_blur/#textual.events.DescendantBlur.control","title":"control property","text":"
control: Widget\n

The widget that was blurred (alias of widget).

"},{"location":"events/descendant_blur/#textual.events.DescendantBlur.widget","title":"widget instance-attribute","text":"
widget: Widget\n

The widget that was blurred.

"},{"location":"events/descendant_blur/#see-also","title":"See also","text":"
  • Blur
  • DescendantFocus
  • Focus
"},{"location":"events/descendant_focus/","title":"DescendantFocus","text":"

The DescendantFocus event is sent to a widget when one of its descendants receives focus.

  • Bubbles
  • Verbose
"},{"location":"events/descendant_focus/#attributes","title":"Attributes","text":"

No other attributes

"},{"location":"events/descendant_focus/#code","title":"Code","text":""},{"location":"events/descendant_focus/#textual.events.DescendantFocus","title":"textual.events.DescendantFocus class","text":"

Bases: Event

Sent when a child widget is focussed.

  • Bubbles
  • Verbose
"},{"location":"events/descendant_focus/#textual.events.DescendantFocus.control","title":"control property","text":"
control: Widget\n

The widget that was focused (alias of widget).

"},{"location":"events/descendant_focus/#textual.events.DescendantFocus.widget","title":"widget instance-attribute","text":"
widget: Widget\n

The widget that was focused.

"},{"location":"events/descendant_focus/#see-also","title":"See also","text":"
  • Blur
  • DescendantBlur
  • Focus
"},{"location":"events/enter/","title":"Enter","text":"

The Enter event is sent to a widget when the mouse pointer first moves over a widget.

  • Bubbles
  • Verbose
"},{"location":"events/enter/#attributes","title":"Attributes","text":"

No other attributes

"},{"location":"events/enter/#code","title":"Code","text":""},{"location":"events/enter/#textual.events.Enter","title":"textual.events.Enter class","text":"

Bases: Event

Sent when the mouse is moved over a widget.

  • Bubbles
  • Verbose
"},{"location":"events/focus/","title":"Focus","text":"

The Focus event is sent to a widget when it receives input focus.

  • Bubbles
  • Verbose
"},{"location":"events/focus/#attributes","title":"Attributes","text":"

No other attributes

"},{"location":"events/focus/#code","title":"Code","text":""},{"location":"events/focus/#textual.events.Focus","title":"textual.events.Focus class","text":"

Bases: Event

Sent when a widget is focussed.

  • Bubbles
  • Verbose
"},{"location":"events/focus/#see-also","title":"See also","text":"
  • Blur
  • DescendantBlur
  • DescendantFocus
"},{"location":"events/hide/","title":"Hide","text":"

The Hide event is sent to a widget when it is hidden from view.

  • Bubbles
  • Verbose
"},{"location":"events/hide/#attributes","title":"Attributes","text":"

No additional attributes

"},{"location":"events/hide/#code","title":"Code","text":""},{"location":"events/hide/#textual.events.Hide","title":"textual.events.Hide class","text":"

Bases: Event

Sent when a widget has been hidden.

  • Bubbles
  • Verbose

A widget may be hidden by setting its visible flag to False, if it is no longer in a layout, or if it has been offset beyond the edges of the terminal.

"},{"location":"events/key/","title":"Key","text":"

The Key event is sent to a widget when the user presses a key on the keyboard.

  • Bubbles
  • Verbose
"},{"location":"events/key/#attributes","title":"Attributes","text":"attribute type purpose key str Name of the key that was pressed. char str or None The character that was pressed, or None it isn't printable."},{"location":"events/key/#code","title":"Code","text":""},{"location":"events/key/#textual.events.Key","title":"textual.events.Key class","text":"
def __init__(self, key, character):\n

Bases: InputEvent

Sent when the user hits a key on the keyboard.

  • Bubbles
  • Verbose
Parameters Name Type Description Default key str

The key that was pressed.

required character str | None

A printable character or None if it is not printable.

required Attributes Name Type Description aliases list[str]

The aliases for the key, including the key itself.

"},{"location":"events/key/#textual.events.Key.is_printable","title":"is_printable property","text":"
is_printable: bool\n

Check if the key is printable (produces a unicode character).

Returns Type Description bool

True if the key is printable.

"},{"location":"events/key/#textual.events.Key.name","title":"name property","text":"
name: str\n

Name of a key suitable for use as a Python identifier.

"},{"location":"events/key/#textual.events.Key.name_aliases","title":"name_aliases property","text":"
name_aliases: list[str]\n

The corresponding name for every alias in aliases list.

"},{"location":"events/leave/","title":"Leave","text":"

The Leave event is sent to a widget when the mouse pointer moves off a widget.

  • Bubbles
  • Verbose
"},{"location":"events/leave/#attributes","title":"Attributes","text":"

No other attributes

"},{"location":"events/leave/#code","title":"Code","text":""},{"location":"events/leave/#textual.events.Leave","title":"textual.events.Leave class","text":"

Bases: Event

Sent when the mouse is moved away from a widget.

  • Bubbles
  • Verbose
"},{"location":"events/load/","title":"Load","text":"

The Load event is sent to the app prior to switching the terminal to application mode.

The load event is typically used to do any setup actions required by the app that don't change the display.

  • Bubbles
  • Verbose
"},{"location":"events/load/#attributes","title":"Attributes","text":"

No additional attributes

"},{"location":"events/load/#code","title":"Code","text":""},{"location":"events/load/#textual.events.Load","title":"textual.events.Load class","text":"

Bases: Event

Sent when the App is running but before the terminal is in application mode.

Use this event to run any set up that doesn't require any visuals such as loading configuration and binding keys.

  • Bubbles
  • Verbose
"},{"location":"events/mount/","title":"Mount","text":"

The Mount event is sent to a widget and Application when it is first mounted.

The mount event is typically used to set the initial state of a widget or to add new children widgets.

  • Bubbles
  • Verbose
"},{"location":"events/mount/#attributes","title":"Attributes","text":"

No additional attributes

"},{"location":"events/mount/#code","title":"Code","text":""},{"location":"events/mount/#textual.events.Mount","title":"textual.events.Mount class","text":"

Bases: Event

Sent when a widget is mounted and may receive messages.

  • Bubbles
  • Verbose
"},{"location":"events/mouse_capture/","title":"MouseCapture","text":"

The MouseCapture event is sent to a widget when it is capturing mouse events from outside of its borders on the screen.

  • Bubbles
  • Verbose
"},{"location":"events/mouse_capture/#attributes","title":"Attributes","text":"attribute type purpose mouse_position Offset Mouse coordinates when the mouse was captured"},{"location":"events/mouse_capture/#code","title":"Code","text":""},{"location":"events/mouse_capture/#textual.events.MouseCapture","title":"textual.events.MouseCapture class","text":"
def __init__(self, mouse_position):\n

Bases: Event

Sent when the mouse has been captured.

  • Bubbles
  • Verbose

When a mouse has been captured, all further mouse events will be sent to the capturing widget.

Parameters Name Type Description Default mouse_position Offset

The position of the mouse when captured.

required"},{"location":"events/mouse_down/","title":"MouseDown","text":"

The MouseDown event is sent to a widget when a mouse button is pressed.

  • Bubbles
  • Verbose
"},{"location":"events/mouse_down/#attributes","title":"Attributes","text":"attribute type purpose x int Mouse x coordinate, relative to Widget y int Mouse y coordinate, relative to Widget delta_x int Change in x since last mouse event delta_y int Change in y since last mouse event button int Index of mouse button shift bool Shift key pressed if True meta bool Meta key pressed if True ctrl bool Ctrl key pressed if True screen_x int Mouse x coordinate relative to the screen screen_y int Mouse y coordinate relative to the screen"},{"location":"events/mouse_down/#code","title":"Code","text":""},{"location":"events/mouse_down/#textual.events.MouseDown","title":"textual.events.MouseDown class","text":"

Bases: MouseEvent

Sent when a mouse button is pressed.

  • Bubbles
  • Verbose
"},{"location":"events/mouse_move/","title":"MouseMove","text":"

The MouseMove event is sent to a widget when the mouse pointer is moved over a widget.

  • Bubbles
  • Verbose
"},{"location":"events/mouse_move/#attributes","title":"Attributes","text":"attribute type purpose x int Mouse x coordinate, relative to Widget y int Mouse y coordinate, relative to Widget delta_x int Change in x since last mouse event delta_y int Change in y since last mouse event button int Index of mouse button shift bool Shift key pressed if True meta bool Meta key pressed if True ctrl bool Ctrl key pressed if True screen_x int Mouse x coordinate relative to the screen screen_y int Mouse y coordinate relative to the screen"},{"location":"events/mouse_move/#code","title":"Code","text":""},{"location":"events/mouse_move/#textual.events.MouseMove","title":"textual.events.MouseMove class","text":"

Bases: MouseEvent

Sent when the mouse cursor moves.

  • Bubbles
  • Verbose
"},{"location":"events/mouse_release/","title":"MouseRelease","text":"

The MouseRelease event is sent to a widget when it is no longer receiving mouse events outside of its borders.

  • Bubbles
  • Verbose
"},{"location":"events/mouse_release/#attributes","title":"Attributes","text":"attribute type purpose mouse_position Offset Mouse coordinates when the mouse was released"},{"location":"events/mouse_release/#code","title":"Code","text":""},{"location":"events/mouse_release/#textual.events.MouseRelease","title":"textual.events.MouseRelease class","text":"
def __init__(self, mouse_position):\n

Bases: Event

Mouse has been released.

  • Bubbles
  • Verbose
Parameters Name Type Description Default mouse_position Offset

The position of the mouse when released.

required"},{"location":"events/mouse_scroll_down/","title":"MouseScrollDown","text":"

The MouseScrollDown event is sent to a widget when the scroll wheel (or trackpad equivalent) is moved down.

  • Bubbles
  • Verbose
"},{"location":"events/mouse_scroll_down/#attributes","title":"Attributes","text":"attribute type purpose x int Mouse x coordinate, relative to Widget y int Mouse y coordinate, relative to Widget"},{"location":"events/mouse_scroll_down/#code","title":"Code","text":""},{"location":"events/mouse_scroll_down/#textual.events.MouseScrollDown","title":"textual.events.MouseScrollDown class","text":"

Bases: MouseEvent

Sent when the mouse wheel is scrolled down.

  • Bubbles
  • Verbose
"},{"location":"events/mouse_scroll_up/","title":"MouseScrollUp","text":"

The MouseScrollUp event is sent to a widget when the scroll wheel (or trackpad equivalent) is moved up.

  • Bubbles
  • Verbose
"},{"location":"events/mouse_scroll_up/#attributes","title":"Attributes","text":"attribute type purpose x int Mouse x coordinate, relative to Widget y int Mouse y coordinate, relative to Widget"},{"location":"events/mouse_scroll_up/#code","title":"Code","text":""},{"location":"events/mouse_scroll_up/#textual.events.MouseScrollUp","title":"textual.events.MouseScrollUp class","text":"

Bases: MouseEvent

Sent when the mouse wheel is scrolled up.

  • Bubbles
  • Verbose
"},{"location":"events/mouse_up/","title":"MouseUp","text":"

The MouseUp event is sent to a widget when the user releases a mouse button.

  • Bubbles
  • Verbose
"},{"location":"events/mouse_up/#attributes","title":"Attributes","text":"attribute type purpose x int Mouse x coordinate, relative to Widget y int Mouse y coordinate, relative to Widget delta_x int Change in x since last mouse event delta_y int Change in y since last mouse event button int Index of mouse button shift bool Shift key pressed if True meta bool Meta key pressed if True ctrl bool Ctrl key pressed if True screen_x int Mouse x coordinate relative to the screen screen_y int Mouse y coordinate relative to the screen"},{"location":"events/mouse_up/#code","title":"Code","text":""},{"location":"events/mouse_up/#textual.events.MouseUp","title":"textual.events.MouseUp class","text":"

Bases: MouseEvent

Sent when a mouse button is released.

  • Bubbles
  • Verbose
"},{"location":"events/paste/","title":"Paste","text":"

The Paste event is sent to a widget when the user pastes text.

  • Bubbles
  • Verbose
"},{"location":"events/paste/#attributes","title":"Attributes","text":"attribute type purpose text str The text that was pasted"},{"location":"events/paste/#code","title":"Code","text":""},{"location":"events/paste/#textual.events.Paste","title":"textual.events.Paste class","text":"
def __init__(self, text):\n

Bases: Event

Event containing text that was pasted into the Textual application. This event will only appear when running in a terminal emulator that supports bracketed paste mode. Textual will enable bracketed pastes when an app starts, and disable it when the app shuts down.

  • Bubbles
  • Verbose
Parameters Name Type Description Default text str

The text that has been pasted.

required"},{"location":"events/resize/","title":"Resize","text":"

The Resize event is sent to a widget when its size changes and when it is first made visible.

  • Bubbles
  • Verbose
"},{"location":"events/resize/#attributes","title":"Attributes","text":"attribute type purpose size Size The new size of the Widget virtual_size Size The virtual size (scrollable area) of the Widget container_size Size The size of the container (parent widget)"},{"location":"events/resize/#code","title":"Code","text":""},{"location":"events/resize/#textual.events.Resize","title":"textual.events.Resize class","text":"
def __init__(self, size, virtual_size, container_size=None):\n

Bases: Event

Sent when the app or widget has been resized.

  • Bubbles
  • Verbose
Parameters Name Type Description Default size Size

The new size of the Widget.

required virtual_size Size

The virtual size (scrollable size) of the Widget.

required container_size Size | None

The size of the Widget's container widget.

None"},{"location":"events/screen_resume/","title":"ScreenResume","text":"

The ScreenResume event is sent to a Screen when it becomes current.

  • Bubbles
  • Verbose
"},{"location":"events/screen_resume/#attributes","title":"Attributes","text":"

No other attributes

"},{"location":"events/screen_resume/#code","title":"Code","text":""},{"location":"events/screen_resume/#textual.events.ScreenResume","title":"textual.events.ScreenResume class","text":"

Bases: Event

Sent to screen that has been made active.

  • Bubbles
  • Verbose
"},{"location":"events/screen_suspend/","title":"ScreenSuspend","text":"

The ScreenSuspend event is sent to a Screen when it is replaced by another screen.

  • Bubbles
  • Verbose
"},{"location":"events/screen_suspend/#attributes","title":"Attributes","text":"

No other attributes

"},{"location":"events/screen_suspend/#code","title":"Code","text":""},{"location":"events/screen_suspend/#textual.events.ScreenSuspend","title":"textual.events.ScreenSuspend class","text":"

Bases: Event

Sent to screen when it is no longer active.

  • Bubbles
  • Verbose
"},{"location":"events/show/","title":"Show","text":"

The Show event is sent to a widget when it becomes visible.

  • Bubbles
  • Verbose
"},{"location":"events/show/#attributes","title":"Attributes","text":"

No additional attributes

"},{"location":"events/show/#code","title":"Code","text":""},{"location":"events/show/#textual.events.Show","title":"textual.events.Show class","text":"

Bases: Event

Sent when a widget has become visible.

  • Bubbles
  • Verbose
"},{"location":"examples/styles/","title":"Index","text":"

These are the examples from the documentation, used to generate screenshots.

You can run them with the textual CLI.

For example:

textual run text_style.py\n
"},{"location":"guide/","title":"Guide","text":"

Welcome to the Textual Guide! An in-depth reference on how to build apps with Textual.

"},{"location":"guide/#example-code","title":"Example code","text":"

Most of the code in this guide is fully working\u2014you could cut and paste it if you wanted to.

Although it is probably easier to check out the Textual repository and navigate to the docs/examples/guide directory and run the examples from there.

"},{"location":"guide/CSS/","title":"Textual CSS","text":"

Textual uses CSS to apply style to widgets. If you have any exposure to web development you will have encountered CSS, but don't worry if you haven't: this chapter will get you up to speed.

VSCode User?

The official Textual CSS extension adds syntax highlighting for both external files and inline CSS.

"},{"location":"guide/CSS/#stylesheets","title":"Stylesheets","text":"

CSS stands for Cascading Stylesheet. A stylesheet is a list of styles and rules about how those styles should be applied to a web page. In the case of Textual, the stylesheet applies styles to widgets, but otherwise it is the same idea.

Let's look at some Textual CSS.

Header {\ndock: top;\nheight: 3;\ncontent-align: center middle;\nbackground: blue;\ncolor: white;\n}\n

This is an example of a CSS rule set. There may be many such sections in any given CSS file.

Let's break this CSS code down a bit.

Header {\ndock: top;\nheight: 3;\ncontent-align: center middle;\nbackground: blue;\ncolor: white;\n}\n

The first line is a selector which tells Textual which widget(s) to modify. In the above example, the styles will be applied to a widget defined by the Python class Header.

Header {\ndock: top;\nheight: 3;\ncontent-align: center middle;\nbackground: blue;\ncolor: white;\n}\n

The lines inside the curly braces contains CSS rules, which consist of a rule name and rule value separated by a colon and ending in a semicolon. Such rules are typically written one per line, but you could add additional rules as long as they are separated by semicolons.

The first rule in the above example reads \"dock: top;\". The rule name is dock which tells Textual to place the widget on an edge of the screen. The text after the colon is top which tells Textual to dock to the top of the screen. Other valid values for dock are \"right\", \"bottom\", or \"left\"; but \"top\" is most appropriate for a header.

"},{"location":"guide/CSS/#the-dom","title":"The DOM","text":"

The DOM, or Document Object Model, is a term borrowed from the web world. Textual doesn't use documents but the term has stuck. In Textual CSS, the DOM is an arrangement of widgets you can visualize as a tree-like structure.

Some widgets contain other widgets: for instance, a list control widget will likely also have item widgets, or a dialog widget may contain button widgets. These child widgets form the branches of the tree.

Let's look at a trivial Textual app.

dom1.pyOutput
from textual.app import App\nclass ExampleApp(App):\npass\nif __name__ == \"__main__\":\napp = ExampleApp()\napp.run()\n

ExampleApp

This example creates an instance of ExampleApp, which will implicitly create a Screen object. In DOM terms, the Screen is a child of ExampleApp.

With the above example, the DOM will look like the following:

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nM1Ya0/jOFx1MDAxNP3Or6i6X3YlXGKOY8fxSKtcdTAwMTXPpSywo1x1MDAwMVxyj9VcYrmJaT3Na1x1MDAxMpfHIP77XqdMXHUwMDFlbVxiZVx1MDAxN0ZEUZv4OtfX1+fc4+R+pdfr67tU9j/0+vLWXHUwMDE3oVxuMnHTXzXt1zLLVVx1MDAxMoNcdFx1MDAxN/d5Ms38oudY6zT/sL5cdTAwMWWJbFwidVx1MDAxYVxuX1rXKp+KMNfTQCWWn0TrSsso/8P8XHUwMDFliUj+niZRoDOrXHUwMDFhZE1cdTAwMDZKJ9lsLFx1MDAxOcpIxjpcdTAwMDfv/8B9r3df/NaiXHUwMDBilIiSOCi6XHUwMDE3hlp4njvfepTERaiUIVx1MDAwN3PqkbKDyrdhMC1cdTAwMDOwXkHAsrKYpn56sbN1pD45eiNcdTAwMWKyfNP5Nvy6d1WNeqXC8FjfhbM8XGJ/PM1kZc11lkzkqVxu9Fx1MDAxOOz2XFx7+VxcnkBcbqqnsmQ6XHUwMDFhxzLPXHUwMDFizySp8JW+M21cYpWtXCJcdTAwMWVcdTAwMTU+qpZbk1x1MDAwMeJamHmO5zpcdTAwMGV1XHUwMDEwqc23cECY5VLsXHUwMDEwh9G5mLaSXHUwMDEw1lx1MDAwMGL6XHUwMDA1XHUwMDE1R1x1MDAxNdVQ+JNcdTAwMTGEXHUwMDE2XHUwMDA3VVx1MDAxZlx1MDAwZvvcrs335sdMa1x1MDAwM46lXHUwMDFhjbVpxNjyXHUwMDEwcT1GZ75r+ZBF/m3P5pRcdTAwMTKMcWkxI6aDoFx1MDAwMMKX+fyNRZY+5qmfm5tatCbQnXlcdTAwMTTVkVRbY+dcIkv5vlx1MDAxYVxmvk7GfyX88CxcdTAwMWRcdTAwMGZcdTAwMGVLX1xy2Gl5q/ul4WG1y+2Ze1x1MDAxMm1cdTAwMGUv7evp9v6BPls7+8jRfrtbkWXJzfN+XHUwMDFiUawuO5HK7eNVlchpXHUwMDFhiFx1MDAxOfZt10XE5sjjXHUwMDBl4aU9VPFcdTAwMDSM8TRcZqu2xJ9UdFmpxbtA0kacdYba5CmG2thQXHUwMDE0XHUwMDEw4i1N0e7le69cdTAwMTSldidFObeAXG6GLP+HoTpcdTAwMTNxnopcZljQwlLWxlK+wErmeraDXFxcdTAwMWK9Piu7kMihOr1cdTAwMDSJ1YInsT5W31x1MDAwYjS5XHUwMDE2hWKEsIsw41x1MDAxY1HW6LUrXCJcdTAwMTXeNdawgCxEvnMrojSUXHUwMDFiafrrb/VcdTAwMTTnXHUwMDEyXCIpXFyTxjNcdTAwMWKhXHUwMDFhXHUwMDE5aPd9mJvMXHUwMDFhqNdcbkSu7Fx1MDAxMKkgXGJrXGL0IVx1MDAxMFx1MDAwMT6zwTKCk2RqpGJcdTAwMTGetMXZScZM+nqGxVx1MDAxNkZS+qRmYlx1MDAwNCDkUJXdpVx1MDAxOXn+PdGXXyfDk+PRwblzQsefkvPLd89IXHUwMDE3W8hlhHheXHUwMDFiI1x1MDAxZNuxXHUwMDEwI9h+U0pSukhJj0GlmFx1MDAxM+tHalx1MDAwMqRcdTAwMTHFXHUwMDFlcV+fml3KXHUwMDE27MfnQ0rOXHUwMDBmtlx1MDAwMrw33tldu9zDn9+jYM78nu5/vr45INuHXHUwMDA3XHUwMDE5XHL+vMNTTLbdV/CLT4PB3u7EP/Q2iH1cdTAwMTKFf+/EXHUwMDE3ozdcdTAwMTX49sS/QOCZkVZe7a/eSOBcdPXmW3+UXHUwMDEzwinUYUKX34J3o+3dVlx1MDAxM9ZZTVxisZhdaNzbXHUwMDE1XHUwMDEz0lJMsDNfREBcdTAwMWFhXHUwMDE3wp2fKu8vx2GbvGPUaO2Q82M/kzJ+SspZo/+rSfkzMjgv5WWMnZSbVZJcdTAwMTbOMfxcdTAwMTTlQCZAv+FcXF7Bu0vxO+Wc43BcdTAwMGJe7lx1MDAxMXNaOYdcdTAwMTm1XFzOjYJcdTAwMTNujjdjXHUwMDFlslxid5vkLlx06Fx1MDAxMIsz7FJcdTAwMTcvyLlcdTAwMDebXuDGf9loXHUwMDE3wf1sJuZaZHpTxYGKR2CslFxm2OhPzbhryEKO7VLCoVx1MDAxNlKOXHTyylmb6YnU7D0tXHUwMDAyckBcdTAwMWPYg1x1MDAxYYxWr5+98kNQ19b4sXMpqX1cdTAwMTlcdTAwMDfPXHUwMDA2hThUX8Tg1Vx1MDAwME7KmLdcdTAwMTBcdTAwMTW24LWh2HVcdTAwMTXfKmyHPVx1MDAxNVY7zVx1MDAxN8JcbkWut5IoUlx1MDAxYdL/MVGxnk9zkc9ccsPvsVx1MDAxNMG8XHUwMDE1plW3zVx1MDAxN4LUeGzu3KqrXsWU4qa8/rLa2nttXHUwMDExweaoYbfysFL/NzuQwmdfpOmxXHUwMDA2pJVrXHUwMDAwYFbBY+GuJta/VvJms+Xb0lVxmDRcdTAwMTYpNCVHmundP6w8/Fx1MDAwYlxiYlx1MDAxObwifQ== ExampleApp()Screen()

This doesn't look much like a tree yet. Let's add a header and a footer to this application, which will create more branches of the tree:

dom2.pyOutput
from textual.app import App, ComposeResult\nfrom textual.widgets import Header, Footer\nclass ExampleApp(App):\ndef compose(self) -> ComposeResult:\nyield Header()\nyield Footer()\nif __name__ == \"__main__\":\napp = ExampleApp()\napp.run()\n

ExampleApp \u2b58ExampleApp

With a header and a footer widget the DOM looks like this:

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1aa0/bSFx1MDAxNP3Or0DZL7tS4877UWm1glx1MDAxNpZ3aWFcdTAwMDNlVVWuPSReP2s7QKj633dsgu28XHUwMDFjXHUwMDEzXHUwMDEyNpXWQiGZmdy5nrnnzLk3/r6xudlKXHUwMDA3kWq92WypO8v0XHUwMDFjOzZvW6+y9lx1MDAxYlx1MDAxNSdOXHUwMDE46C6Uf07CfmzlI3tpXHUwMDFhJW9ev/bN2FVp5JmWMm6cpG96Sdq3ndCwQv+1kyo/+SN7PTF99XtcdTAwMTT6dlx1MDAxYVx1MDAxYuUkbWU7aVx1MDAxOD/MpTzlqyBNtPW/9efNze/5a8U72zH9MLDz4XlHxT0hx1tPwiB3XHUwMDE1Ulx1MDAwNLEkklx1MDAxNFx1MDAwM5zknZ4sVbbuvdZcdTAwMGWrsidralxyTi//7Jn/qItLwY5Odq7hh17nvJz12vG8s3TgPayDafX6sSp7kzRcdTAwMGVddeHYaS+bfKy9+F5cdTAwMTLqJSi/XHUwMDE1h/1uL1BJMvKdMDItJ1x1MDAxZOg2XG6KRjPo5ibKlrtsXHUwMDAxIDVcdTAwMTBcdTAwMTdYMIwpXHUwMDA2RJS3m31fSINRhFx05nTMo7ehp3dAe/RcdTAwMGLIr9Knr6bldrVjgV2OXHUwMDExyJKwcre3j/dZma+nnG4vzVx1MDAxYVx1MDAxMTJcdTAwMDQgTHD6YLuyXHUwMDFhKl99yCDjknAhip5sxmjfzsPg8/jq9cw4XHUwMDFhrlIryT5UvM1cdTAwMWPdXHUwMDE5j6FqXHUwMDFjVXbY/bgtrlx1MDAwZTBoR9euc9x7f+6eqm+FrZGgS9Vd2io6fryqM1x1MDAwYjto76vf+3JLOsHX+Pro+vh+f3u6WTOOw9umdpfu7sjoV00nLM1cdTAwMGXflfvTj2wzXHUwMDFkbilcdTAwMDNcdTAwMDTpXHUwMDAwpITgot9zXHUwMDAyV3dcdTAwMDZ9zyvbQsstMbhR8XdcdTAwMDL5I35WYVx1MDAwZsFM2DMuJEKYoca4r1/mNcU9XHUwMDAydbiHXHUwMDA0XHUwMDFhlOZcdTAwMTB8XHUwMDBl7tPYXGaSyIw1tqZgn0/Dvlx1MDAxY8c6oYBwKjFaPtSXXHUwMDE5h+V2h0F65tw/xJJBNcNcdTAwMDHEXHUwMDAw4lJcdTAwMDLKR0btmr7jXHJGdjBcdTAwMGZY7fnOnelHntqKol9/q65worQnuWky8p0tz+lmgd2y9L2peCTmU0efm8VcdTAwMDDfsW2vXHUwMDEyf5Z2xNQ24/0mZ1hcdTAwMTg7XScwvfNpftZCMVZW+lx1MDAxMIpT8EhcdTAwMTmcjcd87Vx1MDAwNGON8eh9ROdcdTAwMDcn51dXXHUwMDFjfGA+3btcbkkvXXc8YmhcdTAwMDDGXHRcdTAwMTFiXHUwMDFhXHUwMDFlXHUwMDExpVx1MDAwNuBcdTAwMDTBlVx1MDAwMpLSSUBcbq55YkxcdTAwMDA8XHUwMDFlwlx1MDAxNHLOKV9cdTAwMDEy6061I6ftfzlcdTAwMWFcdTAwMWO+9c47g0/3XHUwMDFkeb69dbK+h/DFQefm9oi8Oz6Kqf3nXHUwMDAw9Vx1MDAxMXnHlmBcdTAwMTdd2Pt7u651LLZcYjz3vfc7wVV3XHR2l76880TD9Fx0XHUwMDFiekturtt+6rZcdTAwMDdcdTAwMWbIXHUwMDE3c0dYd+q47y9hXHUwMDE1tlx1MDAwZb+ddtPww5dTR4pcdTAwMDO307uEnzrN7DZcdTAwMTE5XHUwMDE4gVx1MDAxMjUrXHUwMDEyOYTNXHUwMDE2OZhcbkkolOWIeaRaXHUwMDFmXHUwMDE260qqrJZUXHUwMDA1MzhcdTAwMDTymclNPaeSKZyKSmHxyKVQICQpYCtIaJZcdTAwMTmI01RcdTAwMGVcdTAwMDIjrTWq5syKlVxuZilcdTAwMWE+Mn5pimaOXHUwMDFhXHUwMDE4VzSFj7WYe8D8XHUwMDE00HExXHUwMDEzc1x1MDAxMFx1MDAxMEq0lm1eUKg/ktZcdTAwMTNzXHUwMDE4XGJDUlx1MDAwMTiejjnIXHImZSZkiMyulSFcdTAwMGZcdTAwMThEslFwXHUwMDE3XHUwMDAwxMSQXHUwMDFjMcrQpKrRniGh4bhcdTAwMDBcdTAwMTJz71x1MDAxNkWiwIwvgsQkNeN021x0bCfo6s7yLNNotPrZvG1gXHUwMDAwrNVcdTAwMWGRmlxmqURcdTAwMDSI4raz2zOjbGNcckKylIdSJlx0YkRWRlxmS2x1XHUwMDE5wnBwcai2VGDPdVxuSE2/gOtcZkn/UV5ip/Bcblx1MDAxOTp7ytVnXlx1MDAwN4KYz3JrOswn3PLMJH1cdTAwMWL6vpPq5T9ccp0gXHUwMDFkX+Z8PbcyfPeUaY/36tuq9o1cdTAwMTNBlFlcdTAwMWNVsOW7zVx1MDAxMir5h+L951dTR7cnQzi7KsFbWtio/l8oXHUwMDA3g1x1MDAwMM1OwpD2g2KEmlx1MDAxN0VOXHUwMDBmXHUwMDA3b4OrvnQv/Y8n9uG9+5f7z81/y1061uaQXHUwMDE31OSFMIJcdTAwMDRcdMr1a4XNM1x1MDAwM4RcdTAwMTBDY4Q9dldy0v84XHUwMDE304QlKIa0dOhFUjHn6F03OtlNXHUwMDBmLv0t92T70N/12zPU9/+p2NPtrmh5l252XoY3fcKG3j4jw3skxdnHblx1MDAwNqfKT0ArysQkJuOtXHUwMDE1ZoX64GWoeXmrfvvWlVkhrmVWzlxmJiBmkIFVM2uzjFxmccZYRqsvmpA9OVx1MDAxZZ+XkO1pXHUwMDE1o+JcdTAwMTdOyOYog/GErPDxXHUwMDE50oaBurSMIy2NafNSiJfsXHUwMDFlmGfx9o06oJ3j+z3w7XhcdTAwMWKsO1x1MDAwMDFhXHUwMDA2gZzAXHUwMDFjX9mvXHUwMDFiY9KGXHUwMDFiWId8MWBdlI3kOkmQ1VrWiyib40+nZF/sWIdcdTAwMDNcdTAwMTd+ci/aN1F/XHUwMDAw/1c2y1I2K1ren8XsPME0fcKG3q60dI04rXLNilx1MDAwNFx1MDAxM6SYjTdcdTAwMTeEzTHQvoAnKKb6/VtXwqawlrC5NCjEWFx1MDAwZVx1MDAwNdVcblx0u2FcdDvLPzlcdTAwMDGlmy8jmJ5cdTAwMTiPz1x1MDAxM0y7YZi+uGCaozfGXHUwMDA1U+FjLfRmVrBcdTAwMTma/UicoEBcdTAwMTJcZnDzXHUwMDEydn32tq7QXHUwMDAzwFx1MDAxMFRcYplVQ1x1MDAwNVx1MDAxNmhcdTAwMDR5WEslwXV+wIfIw6uDXHUwMDFlXHUwMDAyhqSMS0klg1JCMYlEgVxyqZNIJFx1MDAxONY+M4nGgUmAhFx1MDAxNEm0XHUwMDAwMJ9R0F48k5ld0G5Q8C2PuWqlmVBcdTAwMDBcdTAwMDGlXHUwMDAy6pVgXGLDyqjH6jdFXHUwMDE0wmH2KTBcdTAwMWVcdTAwMGWYX89cdTAwMWXxqT61XHUwMDE59YkhoONcZnFJXHUwMDAw1uGEJnyCyOA6USZMZ8Ukq1x1MDAxM6BcdKd+qmr27GDOrokwLu1tVP8/mc8gwLNcdI3RXGbmXFw011x1MDAxMvXqal1cdFxyXHUwMDBiXHUwMDAzScFcdTAwMDVikmnJUFx1MDAxZVRcdTAwMGaEpqVcdTAwMDTCNPuVnFxiQenqXGJNYoNBiHQ8M4xw5cHeks6kgVx1MDAwNOeSUo6ZoHLy2V/B9Z2QhTLCZ/HZokJj2XxcdTAwMDZcZk1j2lx1MDAxYlx1MDAwMfR2MSRk+ZxiwVx1MDAxZNyA+TOTXHUwMDEwPGzognxWrzxGfKJASL1OTGtcdTAwMDRcbinhXHUwMDEzLlx0gzJ9XGZcdTAwMDGYXHUwMDFmm1iIn5rNZlx1MDAwNXJ2TYTwLCrbXHUwMDE4mm+ZUXSW6ngrtkKHtGNcdTAwMGbVaXmPrVx1MDAxYkfdbk95vP46vzLBl69mxkIqu9PvPzZ+/Fx1MDAwYlx0sVx1MDAwYuIifQ== ExampleApp()Screen()Header()Footer()

Note

We've simplified the above example somewhat. Both the Header and Footer widgets contain children of their own. When building an app with pre-built widgets you rarely need to know how they are constructed unless you plan on changing the styles of individual components.

Both Header and Footer are children of the Screen object.

To further explore the DOM, we're going to build a simple dialog with a question and two buttons. To do this we're going to import and use a few more builtin widgets:

  • textual.layout.Container For our top-level dialog.
  • textual.layout.Horizontal To arrange widgets left to right.
  • textual.widgets.Static For simple content.
  • textual.widgets.Button For a clickable button.
dom3.py
from textual.app import App, ComposeResult\nfrom textual.containers import Container, Horizontal\nfrom textual.widgets import Button, Footer, Header, Static\nQUESTION = \"Do you want to learn about Textual CSS?\"\nclass ExampleApp(App):\ndef compose(self) -> ComposeResult:\nyield Header()\nyield Footer()\nyield Container(\nStatic(QUESTION, classes=\"question\"),\nHorizontal(\nButton(\"Yes\", variant=\"success\"),\nButton(\"No\", variant=\"error\"),\nclasses=\"buttons\",\n),\nid=\"dialog\",\n)\nif __name__ == \"__main__\":\napp = ExampleApp()\napp.run()\n

We've added a Container to our DOM which (as the name suggests) is a container for other widgets. The container has a number of other widgets passed as positional arguments which will be added as the children of the container. Not all widgets accept child widgets in this way. A Button widget doesn't require any children, for example.

Here's the DOM created by the above code:

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1d+1PiyFx1MDAxNv59/oop95e9VWO2+/T7Vt265Vx1MDAwYnVcdTAwMWSfOFx1MDAwYnrnllx1MDAxNSFcdTAwMDIjLyGIurX/+z1cdTAwMWSVXHUwMDA0Qlx1MDAwMihB2DvU7qhJ6Jx0n/Od7+tX/vz0+fOa/9j21v75ec17KLn1Wrnj9te+2OP3XqdbazXxXHUwMDE0XHUwMDA0f3dbvU4puLLq++3uP3/7reF2bj2/XXdLnnNf6/bcetfvlWstp9Rq/FbzvUb33/bfI7fh/avdapT9jlx1MDAxM95k3SvX/Fbn+V5e3Wt4Tb+Lpf9cdTAwMDf//vz5z+DfiHXlmttoNcvB5cGJiHlGjlx1MDAxZT1qNVx1MDAwM1NccuGCXHUwMDAzIzC4oNbdxpv5Xlx1MDAxOc/eoMFeeMZcdTAwMWVa29y+K3b2XHUwMDFlL5t5wXP7f0C+d212w7ve1Or1vP9Yf65cdTAwMDe3VO11vPBs1++0br1CrexX8TxcdTAwMWQ5Pvhet4VVXHUwMDEwfqvT6lWqTa/bXHUwMDFk+k6r7ZZq/iNcdTAwMWVcdTAwMTNkcNBtVoJcIsIjXHUwMDBmwVx1MDAwNeBQJkBcdTAwMTCquDBE8cHp4PvUYYRqo1x1MDAxOFBcdTAwMGVajtq11apjO6Bdv5DgXHUwMDEzWnbtlm4raF6zXHUwMDFjXqOhZGjkmfuvT6uZwzlcdTAwMDPA21x1MDAxOGZcYojBJVWvVqn69lx1MDAxYVx1MDAwMEdcdTAwMTMutVx1MDAxMs+3ipjiXHUwMDA1TUJcdTAwMDVQ4FLqsEmtXHUwMDAx7f1y4Fx1MDAxYv9cdTAwMWSt0qrbab9U3VrX/lx1MDAxMTHe2r0z6lhR54o0u5bnXHUwMDFi5sdlsVrq7JOTk8vCuVuQg7KGPNH3XHUwMDFl/LXBib++pFx1MDAxNXt4uv90kXso+p1tb72a71x1MDAxNlx1MDAxNfCb8cW6nU6rP225XHUwMDE5mTv3Yoeu/jLtXHLDYl9+XHUwMDBim73XLrvPwUulJFxco1trqfXgfL3WvMWTzV69XHUwMDFlXHUwMDFla5Vuw3j/XHUwMDE0sTeGMkN2RiGG6NGjr1x1MDAxMCOAXHUwMDE50IqEXHUwMDBlO1x0YtJreVkhRqVBXGZFXHUwMDA0XHUwMDAywoFTot6PMX7HbXbbblx1MDAwN1x1MDAwM3dcZs6oyThcdTAwMDNxXFxB41xmXHUwMDEwakK754Yr8/TO0Fx1MDAwYlpNP1978oKyXHUwMDFjQTUnIFx0KGOIUENX5dxGrf441LCBXHUwMDFio+VcdTAwMWLt9q//iFZ110NcdTAwMTOCMsXQxVx1MDAxYvVaxfr5Wlx0XHUwMDFmyutcZoWAX8OUPbigUSuX61x1MDAxMX8soVx1MDAwNS6W2dmfJn22OrVKrenWz4dcZkxccsmOV/KffXJMXFxcbkWS4pIqIyhorvTUgVx0rdO9XHUwMDFi/fitcX2+sd+oXHUwMDFlVcVV/2TZXHUwMDAzkyrHXHUwMDAwZ1x1MDAxNORzYFx1MDAwZcUloO8oYFxu/3/O/dmFpVx1MDAxOFx1MDAxM4daOVx1MDAxOKacKVx1MDAxMYtHrlxyIZpcbj3/eEzLcGfyxtvtq1x1MDAxZtWzwl3ha+OSi527+vLm+cLvf9z3v/Ltw69cdTAwMWRR3n2EXHUwMDFl8O2EhDxTuVAo7+/lbkuHeoPT80b9eKd5WZlDuVx1MDAxOVVvRsWq+9pe7m6HNjo7V+Vt/1x1MDAxZe6P6/OoXHUwMDA1Tjpt2rwvXHUwMDFkXHUwMDE1clx1MDAwN0e6sF1oXHUwMDFk+lx1MDAwN+8qd1x1MDAxMo9cdTAwMWFfQVOae/2jsXFSv5dU+OubZVM5OGo9PE5n7rLwMy7V6NFBXHUwMDFlkIgykis2fVx1MDAxZUh3tyXNXHUwMDAzKK1S8lx1MDAwMNdcdTAwMGXD9jCv/Cy7PMDH8TFcdTAwMTbDf6VBWDKTgc7Lmo9cdTAwMDFcdTAwMTk6msK/8qWO5zWTKJhcdTAwMWG6fm5cdTAwMTRsXHUwMDAyi1x1MDAxOaVgXHUwMDAzXHUwMDFiU1x1MDAwM+858MdEnoakwFx1MDAwM+CCXHUwMDEzXHUwMDFkYdyT4i49i35I3FEyMfCMcDTlhqlxgYcs1GFcXFx1MDAwZlximMgs8IjDjSQmyrVcdTAwMDbxx7hjXHUwMDE0SCEhTsRcdTAwMDQzmnEuYfZAXGasW3Qgdn2342/WmuVas4Inw3yGwVjq2fuuXHUwMDEzhzAqXHUwMDA1R3RRXHUwMDAyQZGE1W5cdTAwMWbPbdtmQ1x1MDAxMSlQR1xuIVxyXHUwMDA3yU3kipfeyDRF83LxILGuec3yRKOIUehPXG6lXHUwMDFj/idUqExcdTAwMDZWgYMyLyDMQe9cdTAwMThlKsms8VFcdTAwMWUzq+52/a1Wo1HzsfpPWrWmP1rNQX1u2PCuem559Cw+VvTcKFx1MDAwZbRticOkO/ztc1x1MDAxOCnBXHUwMDFmg9//+2Xs1etxXHUwMDE3tp+I84YlfIr+fJN0pJSz0cOv0MUlw3yKXjo1dDVcdTAwMGVcbnDyXGJ721vffpQ2L2BcdTAwMDOKXHUwMDA172Ohi09CLkaZI1x1MDAxOJJcdTAwMDFuOOJTRIpcdTAwMDVfZ9rRRFx1MDAxM2OoXHUwMDA0TcjSSEfJXHUwMDA1YGTDgnuIycPW9UFLXlx1MDAxZJT13f3ZUfuGnlx1MDAxZPKfynFeyjGj6l2tYucvSCdcdMfxXHUwMDBmXHUwMDEyXHUwMDE2+4qzXHUwMDFmLfAoiYDEXGJaY1wipVxiUtN3wKe33rKCtU5cdTAwMDNrRVx1MDAxY8OxNZShXHUwMDAxWC+BwFx1MDAwM6Q7iohcdTAwMGbocDczeOP7XHUwMDA03lx1MDAxZdJcIq+zYIE3gWuMXG68gY3v4Epa0KTo41x1MDAxMt2NSja9zDvYOqnkjqhqyZvzvavTi2qp+vWDx78mhlx1MDAxZlx1MDAxMlBkQ1x1MDAwNFx1MDAwNZ4kmqlcYvlcYr7OiaMkRiVjWiFtJ9nJvFx1MDAxOclcdTAwMTJcYoPijrxB3L2HK12RnD686lx1MDAxNVx1MDAwZp425FaDbPbORK/4kyvNiytlVL0/i/2QvvvxXHUwMDBmMjNcdTAwMDWbJem9jYIpZUZcdTAwMGZcdTAwMGYmQVx1MDAxMKlcdTAwMTjRfHpcdTAwMGWW3nxLmlx1MDAwNFx1MDAxOE1LXHUwMDAyXG5cdTAwMWNtXGZRQFnWSWA6XHUwMDBlXHUwMDA2XHUwMDA0oVx1MDAxZqniR3SyL46D5Votf+FcdTAwMWNsXHUwMDAyh1x1MDAxOeVgXHUwMDAzXHUwMDFiUyMvuZNd8sTIk8LYvs7pIy9dZC7p6JamXHUwMDBlhlx1MDAxNuGGcEKJNEORx4hxJDOCaFx1MDAwM1ooynl2kWdQaFx1MDAxOabBSKBcXI2LQ2rn63BGXHUwMDE4wlx1MDAwMGgqqVx1MDAxOY1LKvCo5lGVtrA+9zfFZXKf+1x1MDAxNH3SYcqLdoZzQTUxQiCY2s68cETyc9hDLzmAeVx1MDAxZLJk7OWCyX3uQ0alq6Vho7CtXHUwMDEwuLlcdTAwMTRcdTAwMWFcdTAwMTRmu7hNXHUwMDE0XHUwMDFjXHRCXGKtJeVcdTAwMTRcZo/ZtFJcdTAwMWTuid5sP3E/XHUwMDBly/tcdTAwMTT9OTOcof8ndr1TJrF6XHKfYT5lOmVbTkCTTDvAqMK4tLOhR1x1MDAwMY1Sh1wi2+DUgLQ/Mlx1MDAwNDTJXHUwMDFkaWPMoFSkhof9SpFZ29QhdpYrXHUwMDA1jf9Hc81gNpfSXGZcdTAwMTBkVlx1MDAxZtBe0YA41FBlQYrhXHUwMDAzXHUwMDAzeiRcdTAwMGKD41x1MDAwNVxyXHUwMDE0Nlx1MDAxMuproGiMXHUwMDE2KP4jV7xlsG7SXHUwMDE4XCJxbIojXHUwMDFjMHtcYkFcdTAwMDVXMZO0g4RcdTAwMDBcdTAwMTDFlE2ETOskk8ZcdTAwMTOYlYazRFdcdTAwMGVOxpx4XmiGzZ9cdTAwMDRmKFx1MDAwNdArpJl+XHUwMDFjMX0u1pJOgUA1ZFeYIEkzXFxhK1xmz1xyt8OMhklCMckwxSC7rmnpXHUwMDE4XHUwMDE0XtyuMEF6xWSYZFwi5Fxmg1xiXHUwMDA1kUJYZaBRxsWwjGDcc6pM+IyrimVvJmdAQFAk2kZgXHUwMDE2UiQ+e8Jgi2rkb1x1MDAxYUOKMKNeqcGM3Kx0ly/+vv/Acr9fX5y6hzfXh+R4N8EmgsmQXCJhMVx1MDAwMVx1MDAxZFHAY0ZR7mhO0dHAXHUwMDAwIKc0sNJwtp7ozvZcdTAwMTN35Fx1MDAxOfEstcNcdTAwMWaISOzroUhJXHUwMDE4g1kmdqW385KiXHUwMDFh5n5cdTAwMDdcdTAwMTBcbjhcdTAwMTOgsFx1MDAxMUKkeJ5cdTAwMWVhXHUwMDFjhjrBKNQoQI1cdTAwMWGxa55cZi1EzLDHP0wpr7jFgGk0KFSkXHUwMDBi6ep/8vvC2z2R3fLvKt8/qNxvbVx1MDAxZW/+7OqfV1d/RtW7WsVmNU9/tWrhXHUwMDFk0/RcdTAwMTPKnTQyMf5BpjT3x7fqU1x1MDAxOfLFYt4tnpeqV4WN+lnCmo2ZXHUwMDFh7eBcXO+c9HKHXHUwMDA3Z113vXrQudl4qO9OV+5rXpwjXHUwMDBiS02xyatJVeJyXHUwMDA14NoopF7TS4Z0d1vW5FxuLC25XG6kcotJrmJMco0ssHxNrlx1MDAxMrOrnbKbQXbNeiSFyqGjKSMpW69jXHUwMDFjv35v2lx1MDAxM7Xyv77bjVx1MDAxN+qtyve1783xIyxcdTAwMDKGylx1MDAxOVxmoNS9m2Hnn2l8ZVx1MDAwMmNcdTAwMWNcdTAwMWRfmWj5O6gwJ4mDL+jAXHUwMDAwis6wvcTR3vXm7Teyv5477uzU1pv89NvjzrJcdTAwMDer5Kg17LwzrpRcclZcdTAwMThcblZhuGNQh2CsZlx1MDAxZKxcdTAwMTFNXHUwMDFlMuF4b6TQilx0XHUwMDEzXHUwMDFkgF1cYlx1MDAxNW7vNftcdTAwMTe3x9c7ZVLNre+vXHUwMDFmk3rx8CdcdTAwMTWeXHUwMDE3XHUwMDE1zqh6V6vYrKjwatXC/KlwRuZOYtjjb5g9XHUwMDEzTi13v3/WKPTFjr7eJFx1MDAxN/6RPKrRq4TJ7TOVu9voXHUwMDFll76dNd2LgtgvbNx5lav1/nTlLlxyc+cscVwirFCEK1x1MDAxMd1cdTAwMTllXHUwMDEyXHUwMDE5SHe3pSVcdTAwMDNcIoVcZkhiXHUwMDFjvlx1MDAxODKgx5CBMczdjklcdTAwMTi7XHUwMDA2/O/M3PeQXHUwMDEwP1lcdTAwMGVcXP/VXHUwMDFlfybBpbrb7XpdZMLXPd9vNbuLJvFcdTAwMTPIbmyi+lxmXHUwMDBm8Vx1MDAwZT4vk/eM4VRwXCJcdTAwMDUjU4dweX/f39l+2uHXrFuu7l+oRv/SXfZcdTAwMTBcdTAwMTZ2N1x1MDAwMCUkl1JcdTAwMDCG8PB4nZLS4YBS1273xIBnuJXTlHzeLmoxoFx1MDAxNzyJfbt597he8lxi9b8+Ulwi+24+d978SefnReczqt7VKjYrOr9atZBcdTAwMTWdX61aqJvNLX29u8W3pPTy51x1MDAwN1x1MDAwZpVcXG5KevxG9TH+Qf6P2Hx0ksdo11x1MDAxZWFMaVxyYno6n+5cdTAwMTfLylx1MDAwNVx1MDAwNEvjXHUwMDAyii6KXHUwMDBijKPzKsZcdTAwMDVcZppIuFAruKp0eja/XHUwMDE5MN1cYlx0/r524SHx/fL8173bqblNXHUwMDFmKXG3Vyrh0yXzejVcXPiceP1cdTAwMDTSO8rr3/Y472D4KD6TwtpQ6+UgZpiR97Tlnp6ap7PKZvGylds4h0b+dNmjWlx1MDAxOe1IXHUwMDAzxE5cdTAwMDWFmEhXUjh2wi5+1FIwfCCUMY46fcG7QbKvxW5+s974arbyxe5cdTAwMWb8x5Ms/tzTY25cdTAwMTQ/o+pdrWKzovirVVx1MDAwYllR/NWqhflT/IzMnaRcdTAwMWPG33BKa98xvvDy28crh2h/cmxcdTAwMDVcdTAwMTNcdTAwMDXgTNLplUN6+y0px9CEpXFcZkVcdTAwMTbFMaZTXHUwMDBlVFx1MDAxOY5qLjqz6v9DOlx1MDAxY7XGUG1cdTAwMGZcdTAwMDOrs2jdMIFKT6FcdTAwMWImPUtqMKeLXHUwMDA2XHUwMDAxyX1cdTAwMDFcdTAwMDJQz3NcdTAwMDPTr0ms9o+L9fouK/hf20y15GX+zr1e9ohmmjtcdTAwMTiqWmKYXHUwMDA0fVx1MDAwMWIopIWRjkBtpbnKXFw2jFm6M2ZgwFx1MDAxMIFBRt+yd+l7ZMN957Kfq2z+qKpK+3ZrY+Obq9tnP2XDvGRDRtW7WsVmJVx1MDAxYlarXHUwMDE2spJccqtSXHUwMDBik3j4+Fx1MDAxYi4hX1x1MDAxNjqRL2thqN1cdTAwMGV++uSaXs1Lm1xcTVpylYQuKrnqMcl1XGZfJswgX1ZvWFx1MDAwNrs6dDnvu8hgXyaNn37byZ/vXHUwMDFmXHUwMDFmfbF/jM4+uet5Xb+WOokmXHUwMDFi0jyBSca283/rXHUwMDEzpcZ14uJ3mVx1MDAxONacXHUwMDFhLYDNsJAlfcXQkoZ1sP0/U6hcdTAwMTCQjGL0XHUwMDBl78sqXHUwMDE4c4BJxlXmKlx1MDAxOIyDXHUwMDE1zijeXGLsmmFcdTAwMTVcdTAwMGZyyZ3xu0RyorRcdTAwMWHaROpvseR9luXlQjBit8IhWIfcaFx1MDAxYbnqdTtcIq00XHUwMDExxK7GJnY19stcdTAwMDVcdEveh1x1MDAxZmOVlp0ne5L9XGZ8KCznU/Tn7JtcdTAwMDFFXHUwMDEy/ygz0NpcbrxcdTAwMTmW16RPiV5SXGKRXHUwMDAwXHUwMDBlXHUwMDE3WNtCWM+iw3tcdTAwMDFcdCZcdTAwMWNcdEoxXHUwMDA1zDBcdTAwMDZmxK45QlxikY7dt9A6ONGM6DFcdTAwMTBcIrD57Yt77IxcdTAwMDBjKJD4XHUwMDBiRYDYxXJcdTAwMWM+4IVcIktcdTAwMDEmxOFKcFCSSqVcdTAwMTFRTPw1XHUwMDFm2lx1MDAwMWk4J4Tad3wgXHUwMDE3TMeSJJPS59eOmKSEXCJUMlwiqNAmvpdcdTAwMTF37I5mTHGBXHUwMDE2XHUwMDBiyVTMpFVcdTAwMDKxZFe2n7hcdTAwMTPPXHUwMDBizCRJ3jaDM/vuVD3DsED6QMmSoplcdTAwMTLSXHUwMDAxu1x1MDAxZj/XytjN3IZljlx1MDAwMcegrmBcdTAwMTK4nYmhR+ya42ZAxNFcdTAwMDKBk1x1MDAwMlD7hoKw2kM+JFx1MDAxY5RiRiq7ZT5nXHUwMDEw31ODW9/h5i1vjV1mLJtcdTAwMDE4wM7+4kiJhGG2VaPvSVx1MDAxYeyJSKyu5VxiL1x1MDAwNONN67ehWfrowDBbXHUwMDAzKkBSXHUwMDEwOliCQlh8IzTlIFx1MDAwMCuqXHUwMDA3RGU1gSzRi4OTo/47L1x1MDAxOItsZVx1MDAxN+NklGKiYDO8TTF9ouiyolx1MDAxOJNcdTAwMGV7fte1XHUwMDA0VFx1MDAwNsNbPVtcdTAwMTSzkk4pad/LrNiIXfNDMbxcdTAwMTFcYlRnRnOmkVSNQzHlgFx1MDAwNo2pXHUwMDA1qN2bKbZcdTAwMDSKoZnYZvxvRsimXHUwMDA2seC9YkhcdTAwMDFcdTAwMDBZqaaMKjlmL0dKXHUwMDFkgdyIXGIuUPNgXHUwMDEye+N2s+lzI0esYsq+XCJcdTAwMDVcdTAwMThcdTAwMTNKo9aKb2kmXHUwMDFkXGZuJIlcdTAwMDSI3Ss0btMqYdl6ojPbT8yNk8Ds08tcctbcdtv2dnmD1kC/rpVf+lx1MDAwMMOnXFy7r3n9zXjc/XJcdTAwMTN8bMdXUJ9cdTAwMTaJPPusf/716a//XHUwMDAxsk3fXHUwMDAxIn0= App()Screen()Header()Footer()Container( id=\"dialog\")Horizontal( classes=\"buttons\")Button( \"Yes\", variant=\"success\")Button( \"No\", variant=\"error\")Static( QUESTION, classes=\"questions\")

Here's the output from this example:

ExampleApp \u2b58ExampleApp Do\u00a0you\u00a0want\u00a0to\u00a0learn\u00a0about\u00a0Textual\u00a0CSS? \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Yes\u00a0\u00a0No\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

You may recognize some elements in the above screenshot, but it doesn't quite look like a dialog. This is because we haven't added a stylesheet.

"},{"location":"guide/CSS/#css-files","title":"CSS files","text":"

To add a stylesheet set the CSS_PATH classvar to a relative path:

Note

Textual CSS files are typically given the extension .tcss to differentiate them from browser CSS (.css).

dom4.py
from textual.app import App, ComposeResult\nfrom textual.containers import Container, Horizontal\nfrom textual.widgets import Button, Footer, Header, Static\nQUESTION = \"Do you want to learn about Textual CSS?\"\nclass ExampleApp(App):\nCSS_PATH = \"dom4.tcss\"\ndef compose(self) -> ComposeResult:\nyield Header()\nyield Footer()\nyield Container(\nStatic(QUESTION, classes=\"question\"),\nHorizontal(\nButton(\"Yes\", variant=\"success\"),\nButton(\"No\", variant=\"error\"),\nclasses=\"buttons\",\n),\nid=\"dialog\",\n)\nif __name__ == \"__main__\":\napp = ExampleApp()\napp.run()\n

You may have noticed that some constructors have additional keyword arguments: id and classes. These are used by the CSS to identify parts of the DOM. We will cover these in the next section.

Here's the CSS file we are applying:

dom4.tcss
/* The top level dialog (a Container) */\n#dialog {\nheight: 100%;\nmargin: 4 8;\nbackground: $panel;\ncolor: $text;\nborder: tall $background;\npadding: 1 2;\n}\n/* The button class */\nButton {\nwidth: 1fr;\n}\n/* Matches the question text */\n.question {\ntext-style: bold;\nheight: 100%;\ncontent-align: center middle;\n}\n/* Matches the button container */\n.buttons {\nwidth: 100%;\nheight: auto;\ndock: bottom;\n}\n

The CSS contains a number of rule sets with a selector and a list of rules. You can also add comments with text between /* and */ which will be ignored by Textual. Add comments to leave yourself reminders or to temporarily disable selectors.

With the CSS in place, the output looks very different:

ExampleApp \u2b58ExampleApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258aDo\u00a0you\u00a0want\u00a0to\u00a0learn\u00a0about\u00a0Textual\u00a0CSS?\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u00a0Yes\u00a0\u00a0No\u00a0\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

"},{"location":"guide/CSS/#using-multiple-css-files","title":"Using multiple CSS files","text":"

You can also set the CSS_PATH class variable to a list of paths. Textual will combine the rules from all of the supplied paths.

"},{"location":"guide/CSS/#why-css","title":"Why CSS?","text":"

It is reasonable to ask why use CSS at all? Python is a powerful and expressive language. Wouldn't it be easier to set styles in your .py files?

A major advantage of CSS is that it separates how your app looks from how it works. Setting styles in Python can generate a lot of spaghetti code which can make it hard to see the important logic in your application.

A second advantage of CSS is that you can customize builtin and third-party widgets just as easily as you can your own app or widgets.

Finally, Textual CSS allows you to live edit the styles in your app. If you run your application with the following command, any changes you make to the CSS file will be instantly updated in the terminal:

textual run my_app.py --dev\n

Being able to iterate on the design without restarting the application makes it easier and faster to design beautiful interfaces.

"},{"location":"guide/CSS/#selectors","title":"Selectors","text":"

A selector is the text which precedes the curly braces in a set of rules. It tells Textual which widgets it should apply the rules to.

Selectors can target a kind of widget or a very specific widget. For instance, you could have a selector that modifies all buttons, or you could target an individual button used in one dialog. This gives you a lot of flexibility in customizing your user interface.

Let's look at the selectors supported by Textual CSS.

"},{"location":"guide/CSS/#type-selector","title":"Type selector","text":"

The type selector matches the name of the (Python) class. For example, the following widget can be matched with a Button selector:

from textual.widgets import Static\nclass Button(Static):\npass\n

The following rule applies a border to this widget:

Button {\nborder: solid blue;\n}\n

The type selector will also match a widget's base classes. Consequently, a Static selector will also style the button because the Button Python class extends Static.

Static {\nbackground: blue;\nborder: rounded white;\n}\n

Note

The fact that the type selector matches base classes is a departure from browser CSS which doesn't have the same concept.

You may have noticed that the border rule exists in both Static and Button. When this happens, Textual will use the most recently defined sub-class within a list of bases. So Button wins over Static, and Static wins over Widget (the base class of all widgets). Hence if both rules were in a stylesheet, the buttons would be \"solid blue\" and not \"rounded white\".

"},{"location":"guide/CSS/#id-selector","title":"ID selector","text":"

Every Widget can have a single id attribute, which is set via the constructor. The ID should be unique to its container.

Here's an example of a widget with an ID:

yield Button(id=\"next\")\n

You can match an ID with a selector starting with a hash (#). Here is how you might draw a red outline around the above button:

#next {\noutline: red;\n}\n

A Widget's id attribute can not be changed after the Widget has been constructed.

"},{"location":"guide/CSS/#class-name-selector","title":"Class-name selector","text":"

Every widget can have a number of class names applied. The term \"class\" here is borrowed from web CSS, and has a different meaning to a Python class. You can think of a CSS class as a tag of sorts. Widgets with the same tag will share styles.

CSS classes are set via the widget's classes parameter in the constructor. Here's an example:

yield Button(classes=\"success\")\n

This button will have a single class called \"success\" which we could target via CSS to make the button a particular color.

You may also set multiple classes separated by spaces. For instance, here is a button with both an error class and a disabled class:

yield Button(classes=\"error disabled\")\n

To match a Widget with a given class in CSS you can precede the class name with a dot (.). Here's a rule with a class selector to match the \"success\" class name:

.success {\nbackground: green;\ncolor: white;\n}\n

Note

You can apply a class name to any widget, which means that widgets of different types could share classes.

Class name selectors may be chained together by appending another full stop and class name. The selector will match a widget that has all of the class names set. For instance, the following sets a red background on widgets that have both error and disabled class names.

.error.disabled {\nbackground: darkred;\n}\n

Unlike the id attribute, a widget's classes can be changed after the widget was created. Adding and removing CSS classes is the recommended way of changing the display while your app is running. There are a few methods you can use to manage CSS classes.

  • add_class() Adds one or more classes to a widget.
  • remove_class() Removes class name(s) from a widget.
  • toggle_class() Removes a class name if it is present, or adds the name if it's not already present.
  • has_class() Checks if one or more classes are set on a widget.
  • classes Is a frozen set of the class(es) set on a widget.
"},{"location":"guide/CSS/#universal-selector","title":"Universal selector","text":"

The universal selector is denoted by an asterisk and will match all widgets.

For example, the following will draw a red outline around all widgets:

* {\noutline: solid red;\n}\n
"},{"location":"guide/CSS/#pseudo-classes","title":"Pseudo classes","text":"

Pseudo classes can be used to match widgets in a particular state. Pseudo classes are set automatically by Textual. For instance, you might want a button to have a green background when the mouse cursor moves over it. We can do this with the :hover pseudo selector.

Button:hover {\nbackground: green;\n}\n

The background: green is only applied to the Button underneath the mouse cursor. When you move the cursor away from the button it will return to its previous background color.

Here are some other pseudo classes:

  • :disabled Matches widgets which are in a disabled state.
  • :enabled Matches widgets which are in an enabled state.
  • :focus Matches widgets which have input focus.
  • :focus-within Matches widgets with a focused a child widget.
  • :dark Matches widgets in dark mode (where App.dark == True).
  • :light Matches widgets in dark mode (where App.dark == False).
"},{"location":"guide/CSS/#combinators","title":"Combinators","text":"

More sophisticated selectors can be created by combining simple selectors. The logic used to combine selectors is know as a combinator.

"},{"location":"guide/CSS/#descendant-combinator","title":"Descendant combinator","text":"

If you separate two selectors with a space it will match widgets with the second selector that have an ancestor that matches the first selector.

Here's a section of DOM to illustrate this combinator:

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1dWXPiSFx1MDAxMn7vX+HwPOzsRKOp+5iIiVxyfODbbjdu293bXHUwMDEzXHUwMDBljGSQXHUwMDExXHUwMDEylmSwPTH/favARlx1MDAwMiRZXHUwMDFjYmCnefAhQSmVyuP7KiuLPz9sbGyGz1x1MDAxZGvzt41N66lec2zTr/U2P+rjXctcdTAwMGZsz1WnUP//wHv06/13NsOwXHUwMDEz/Pbrr+2a37LCjlOrW0bXXHUwMDBlXHUwMDFla05cdTAwMTA+mrZn1L32r3ZotYP/6J+ntbb1e8drm6FvRFx1MDAxNylZplx1MDAxZHr+4FqWY7UtN1xm1Oj/Vf9vbPzZ/1x1MDAxOZPOt+phzW04Vv9cdTAwMDP9UzFcdTAwMDFcdTAwMTEg44dPPbcvLVx1MDAxNFx1MDAxY1x1MDAwYimAkMN32MGOumBomer0nVx1MDAxMtqKzpj9a1x1MDAwNVx1MDAwN7x5b1Pil1x1MDAxOWx/2nX2v5Ur0XXvbMephs/OQFx1MDAxN7V689GPSVx1MDAxNYS+17KubDNs6quPXHUwMDFkXHUwMDFmfi7wlFx1MDAxYaJP+d5jo+laQTDyXHUwMDE5r1Or2+GzPlx1MDAwNsDw6EBccr9tREee1H+UY4MxTlx1MDAwNGFcdTAwMDBxQfDwrP48ZtJcdTAwMDBEQCoxoOokXHUwMDE5k2vbc9SzUHL9ZDHOXHUwMDAxiCS7rdVbXHIlnmtcdTAwMGXfXHUwMDEz+jU36NR89cSi9/Xe7pix4bGmZTeaoTooRHQ9q693iFx1MDAwMFaCMFx1MDAxYd2FvkrnwOxcdTAwMWLBXHUwMDFm43pr1vzOq342XHUwMDAz/U9MQi3c7rhcdTAwMDXFrSj2bKl7V7rCsve1aVx1MDAxZcKb26dcdTAwMTPGYDBcdTAwMWNrxORC6yncXHUwMDFjnvjrY9aw7W9fji727ra64ZfLw9557+Wk0blNXHUwMDFltub7Xi/vuFeHl93eMdk5OfapufeMXHUwMDFlXHUwMDEx2WFcdTAwMGJcdTAwMThcdTAwMTddmVx1MDAwN/uVVv1ElFx0vGg7Z7vut8ZcdTAwMDLGLUi96zXs8ePOPpDXXHUwMDA3L0934qhSMZ9uat3eXHUwMDBm5S5m2H1rXHUwMDBmXHUwMDA33bpdvtm+XHUwMDBi7ytcdTAwMDe75fOr47mUO1wixce8N5JT3Obxdlx1MDAwZmBcdTAwMGK07zudg6Pj7svN+U6YT9zXv6JQ+Ngxa4OspYIsJJBcIiZcdTAwMDSPwq1juy110n10nOiYV29Fie5DTOCJXHUwMDE0O3L/I9lcdTAwMTWK8cNv2ZVzKiHjMMpZ7yXXbLNY2eQqspKrIFx1MDAwNl1OcqVcdMmV8vHkilR2RVx1MDAxY9LosS0sty7SXHUwMDFho4fuuWHVfunDNTZytFJr287zyHPrm6mSdFudrtmu5f/83dUnbPP375umXXO8xvfN7+6/41pcdTAwMGUsJY1cdTAwMWWeopFxyo7d0Ca+6Vh3o7ZcdTAwMWbaXG6oXHUwMDBlT7dt04xDz/rbtVx1MDAwZvJcdTAwMDBGz7dcdTAwMWK2W3Mu8kqe6abZSJhClOarkCtcdTAwMTAsOVx1MDAxMjy3s55fPDjWS3nr8XHr5MA5Obt2ryv3f6+z0nd9lVx1MDAwMYNCXHRcYodcdL5KMTOkgJwqV9W+SlN9XHUwMDE19F9z+KpcdTAwMDSTvirYhK9cdTAwMDKAMYNAXHUwMDE24KxZiem+etjYvfDc7Va9xfyn1sG31kNKYvpcdTAwMDGEp1x1MDAxZrcg9a7XsEVcdTAwMDHh9dLChd/c6d12rYtcdTAwMTBcdTAwMWY6XHUwMDEwiZfWeWc+xLqOWlg8blx1MDAxZoyLn1x1MDAxZkpfkajUQ9JcdKrW887Z9lx1MDAwM1/AuGZcdTAwMWJcXG57h597leujbafb8XdvXHUwMDFiuFCekaz4aNjXvzJcdTAwMTFcdTAwMThmJMr/RfFcdTAwMDGKU/lcdTAwMDCkilx1MDAwYlxiRkGUV9/DXHUwMDE42fa2qlx1MDAxOINmYVxmXG5ccrJcdTAwMWOMIVx1MDAxMjDGJFx1MDAxZpCAXHUwMDEzIFxiL2CubZHWOFx1MDAxZlx1MDAxZthXMPtFI2vnZ318XHUwMDAwretOLVxirEDh69vHMPTcYNnU4Fx1MDAxZFx1MDAwND1OXHKmuYlM581mXHRcXKbOlzPMXHUwMDA1Izj/bPmX8PzCe3q+2eptnZ1+uXj5/PKpzFL8d8xcdTAwMGb/LjpPODGApFxiXG7JuXJfQkb8lyNqKCpcdTAwMGJcdFx1MDAxM8rJXHRcdTAwMTfz+O9PXHUwMDEw3VxuoajxzFxmXHUwMDAxXHUwMDEyrKRcdTAwMDScocW7b1ZcdTAwMDZkrOneyy77elolnYY8wkfHj5X5gcBcdTAwMGaGUKh612vYolx1MDAxOMJ6aaEohrBeWnDk1ra43dsm24xZ1Yujp0alssK2sHyCkHwjOcWtXHUwMDFmnV6QzrF/+tXfvjWt44483vuyuELEUoiHiNWWx4lcdTAwMDeTXHUwMDEwXCKJRX7ikW1cdTAwMTcrWokgXFxmQlx1MDAxNypccrwo6DI99YBcdTAwMTFsfJvepLpARPCyS1x1MDAxMUulXHUwMDFlW31Y/vP3za+WwubJ9Fx1MDAwMpKRj1xy+UNd3Y7lz04w3oHf41x1MDAwNGNcXNRMR8wmXHUwMDExXCK2pGbMXHUwMDFiXHUwMDExXHUwMDA0XGZDOUVZkMPKWYvsud3mXvUlaODnXHUwMDEz379ebVx1MDAxNsEkMqBSXHUwMDAye3XF0UlcdTAwMDBNXCKo1GW41SBcdTAwMTFAeyElYMkk4iGQlaP2WZVcdTAwMWOet2jFNe/RzqfLXHUwMDFmJGJRJKIg9a7XsEWRiPXSQlEkYr20UFx1MDAxNIlYLy0svijyXHUwMDFlN0m+kWjY17/+flx1MDAwZSFlOodAurTBXHUwMDEwyT/5mf38VpRDMMmzgFx1MDAwYtVcZmNBwGVcdTAwMTFcdTAwMTSCS0VcdTAwMWZcYohhmv9nXG5x6i2bQbxcdTAwMDO9U1x1MDAxOcRA0kwvXHUwMDFjXHUwMDA0l1x1MDAwNDdkIL2EXGIh1oYp83thdm15NUuITFx1MDAxN1x0XHRjXHUwMDE0YFwigEQjPkhcdTAwMThVJ1x1MDAxMZVcdTAwMTJDKinjhfkgMDBcdTAwMDZUcixcdTAwMTCmXGIozdNJn2TSgMopKIVCxVx1MDAwZS7wuItcIlx1MDAwMDliKFZVyu2ifVlndVHC4UxcdTAwMGJcdTAwMGWDsOaHW7Zr2m5DnYwy3VsnSp51fX2nrj9cdTAwMDZ9LVx1MDAwMkY5xVxcqVx1MDAxMFx0XHUwMDE5X1x1MDAwM6p1UetoTmZgSVxiXHUwMDAxQFExiFV4fX3DMONuWq75vkzZXHUwMDA1xZhMJSVcdTAwMTTmXHUwMDAwqStcIlxuoZIv8rmhUMjAnDMpKEVcdTAwMTLolDAhlFNcdTAwMGLCba/dtkOl+0+e7YbjOu4rs6w9vWnVzPGz6qbi58ZDQkePOMpcIqO/Nlwin+n/M/z7j4/J7043Zv2aMONovFx1MDAwZvHfU0czXGJcdTAwMDFcdTAwMWU/PKynXHUwMDAySFx1MDAwNIQyesN70SxcdTAwMWK8rSqmYMTgXHUwMDAyMimFJJLEVozrzzNMXGYqMUOIUPVcdTAwMDY2LtdcdTAwMDIxXHUwMDA1xFx1MDAwNkZQXHUwMDFiPEbKolGk92h2XHUwMDA0XHUwMDE4XGZzhFx0xFx1MDAwNKugXHUwMDE1W58xXGJnXHUwMDAyS6DcgswwVzJXOJtcdTAwMTVx5FxmZ7lDXHUwMDA3MFxiplx1MDAxOCCgMCDAkkNcdTAwMWPzo9fIXHUwMDAxocH0Ulx1MDAxZklcdTAwMDBcdTAwMDE6XHUwMDE0z1x1MDAxNs6ywceoTFxiS8l1L1x1MDAxZmBcdTAwMDKDJJlUXHUwMDAwoFKrknKOXHUwMDEw4utcdTAwMWTO0m1ZvyaseFHRLD5tO4HNJCfKmfNcdTAwMDez7CrZqlx1MDAwNjMsXGaKXHUwMDEwg0JiXHUwMDE1zMZjXHUwMDE5M1x1MDAwMMVcdTAwMWF8MJX1i1x1MDAwYmVSKmvGkimszFWWTmr9XHUwMDEwKuhcIqFbXFxcdTAwMTGEKn5MrPyCXG6VIKpS/z81lJU0JiCSqidJmYLZQrCYXHUwMDE3vcVccmyoUKf8jFx1MDAwM4XFlabfkMGUwSy7XHUwMDE2MyqVTodSuVx1MDAxM0QqpElcdTAwMDQnhKJcdTAwMDZcdTAwMDRUhVfOgZZrUqR1XG5lpVRj1q9cdDOeMpRl1qlcdTAwMTQySVxyZyq39d08f9HYadbA2XFnL7Shf0rK7Z3wyjxdcapJdEfMSFxmQ0hcdTAwMThQQlx1MDAwMlx1MDAxOCpwcSpNoJKCXHUwMDFiiKtcdTAwMDTCJ0BcdTAwMTfGkGDFhJfcXHUwMDEw7jlP1XLrwobynpZcdTAwMWN0ePxcdTAwMTLUq/PPwJ6cXHUwMDFmvHytPF2H/o5ValaDa47I3T+wQFWQelx1MDAwYlx1MDAxYZZ37f3Kwy5s+7s35k7YRd0zZ1x1MDAxMVogwO9At1s/vapcdTAwMWOdiqudK+8kPFpd7d7et8ufnC6DNCxtmbJxdKqyWqHlg+RcdTAwMWLJKe5cdTAwMWM91pnjfjmzzmXpwWqJ3a8v95/cs8vSXU5reEtb70CkXGJHXHUwMDE3VO6gNH2elWFFXHUwMDFmXHUwMDE0ssjfXHUwMDBlmm1uq5r86Hjyo9JcdTAwMTBcbm5cdTAwMTSZ+khC6kNcdTAwMTPTpopaKFx1MDAxNlFExluk4UXPNypsIDByNKOwUa37luX+nFLS4CPvX1hJ41x1MDAxZJQ2XtJcdTAwMTjKmOljqYSZp1ZcdTAwMTSFXHUwMDAy+EDPUud2sexYtpouRlx0MahmWFx1MDAxYzLEYUxcdTAwMWT93Vx1MDAxMaA0KKSFXCJNXGJcciggZlSRKSooTZj0IypcZqQgT6hcdTAwMWVRX+5cdTAwMTkqjPOWL/gsjpiTJGd7wUZ8bo1KrtNcdTAwMDElkjCMXHUwMDEyKDIxXHUwMDAwXHUwMDA1XFxcdTAwMDfN1zNTUuMpSilMPVx1MDAwZu04XHUwMDA0ci6jRqS4KEA339F+c1x1MDAxZJZyQqJ1YsbptqtfMauNXHUwMDA2+lx1MDAxMP8929pNlE6KXHTlXGIjPsVcdTAwMWPf+YV74MnHo93OTfPo7vzm0uJcdTAwMGbWiscsjJShgdGwNNgoTShwQFx1MDAxMFx1MDAxNJz2o1x1MDAxMlx1MDAxYpNogVEr305pSGFcdTAwMDTlnXyG2upcXFx1MDAxYqV99tF1q/n5oXlwWeru032zu3OfXGZ+fyzcnH7cgtS7XsNcdTAwMTa2UdpaaaGgYYvacKEgcVx1MDAxN0/i393XLfFGcor7cIvLVVRmdyc7XHUwMDE31WvvM7l7MlvrNTeAkEhdtVx1MDAwMLFUp5mg+SdcdTAwMDey7WJVUVx1MDAwME1GXHUwMDAxglx1MDAxOHhJKCDflm5QXGJcdTAwMDSgXHUwMDEyqID+jaInXHUwMDBi5tzTLbBN67bmL3/nhkxUm2tTt1x1MDAxMdHngOtZJXmqWFx1MDAxOVx1MDAwNGCKXd26veolsvDR4fVpq1x0fdn4dmydpHhq3feCoNSshfXm3++tXHUwMDEwXHUwMDE5irlob1x1MDAwNWNe2f88YobgXHUwMDA1zjJwPumpXHSdVlxiXCLMgIRL3tCtVVx1MDAwNtf7N4dP5llcdTAwMGaf7l1cdTAwMTDzmJXdXHUwMDFmgH1RgL0g9a7XsEVcdTAwMDH29dJCUZ1W66WFojqtXG5cdTAwMTJ38ds1XHUwMDE0JO57tCX5gjmlnYO2ZI5bpdVnSlx1MDAwZt2DXHUwMDFitoPuXHUwMDFliLtXPkwhhStLh6RM7WdXUiBAOEf5XHUwMDBiOdl2saJ0XGLyTIBFoIGLXHUwMDA0WCxcdTAwMDFgTVIhLplUkFx1MDAxN69h2XTqfjB9bMAnvm9cdTAwMWW4QVhznGXzoHfYQkp7WKrgmb6Z3i+W3rQpOVx1MDAwMphROMWOdZnrOlbTNSlcdTAwMTaGXHUwMDE4W7XwVl3tN1x1MDAxY1x1MDAxNemXRK/PXHUwMDFjxIVJXHUwMDA3JdKgjI0tJVx1MDAxY+5Dj1VUXHUwMDE1s1CheVdcdTAwMWbP5KqLLqyWgMGERIxJXHUwMDE1WpmEOKr3bkTlTIIxlVGFL6WyOir/XHUwMDFhVThLSfajXzHLicb4XHUwMDEw/z11nIgtZlx1MDAxYe/DYlxcb6RI85c1s7HSaoZcdFx1MDAwMqhBqMBcdTAwMTKqZIpZbHnBoK2UXHUwMDE4sexe5LQm0F83JaTuXHUwMDFml0xFZ0FcdTAwMTJyO4OGnmIl8vVcdTAwMTX74ozXuVx1MDAxNEgp5ZLPsjn+Klx1MDAwN5DsucXRXHUwMDAwotuwXHUwMDAwJZjplUSQxL4oYFx1MDAxOEK4IbBKglx1MDAxMFxyNJnQK5BrjUZ2pt9cdTAwMThpd1VcdTAwMDJJSlx0XHUwMDEyglx1MDAwYoZcdTAwMTPaXSfbW9cpZmWYr35NXHUwMDE47pTxK70kk1qRwSp5UEbzdypcdTAwMWP4n+Wlc/FcYus9XHUwMDFmXHUwMDA1l1x1MDAwN95J2fs8+ywvXHUwMDFh97VcdTAwMWMhLIpN03RfIVx1MDAwMlxmoWyLxSdz+ztTXGJmXGIwWIU0T+D66a5GXHUwMDExRYm7aaHoPqOFm5MrxpBe76ZcdTAwMDRZ8qKM4jdxXFzewtC2traNsGlcdTAwMDVWXCKbiYHGadhM6HXSqMzIrYzzlrg4s2FcdTAwMGYq0796R1LdcVx1MDAxZW98fs99s1x1MDAxZvVS3He25knMXHUwMDEzvLTvvlx1MDAxMlx1MDAxOFhcbiwwXHUwMDAzXHUwMDFjXHUwMDAzmr6pRY6vycrwYa5cdTAwMThcdTAwMTFcdTAwMWWjRNH2MsRQaVx1MDAxMlx1MDAwMVxyyrnKp5N7WWi6XCJcdTAwMTBe9lZcdTAwMTZcdTAwMDUjjuxsMJLbXHUwMDExXHUwMDAwXHUwMDEyXHUwMDEzhiBVhEVSjmLvXHUwMDFh9ktKQvHMi0Fz90lqYaR6XoLrfnxAJUlo3uRcbvFcdTAwMTJFoF43ROVcdTAwMTMyrVx1MDAxM/BIMF79Kk3a7YIgXHUwMDA3lOlcdTAwMWRcIpwjpU+FP3NHLeubXHUwMDBm7ZOtW8e8XHUwMDExqGe13Fx1MDAxNv+yv1x1MDAxNqBcdTAwMDNcdTAwMTNcdTAwMTW1JjGHOmAw2bf1oiCHiEJNXHUwMDA25GBSXHUwMDExOimW/SVcdTAwMDCrNbE/XHUwMDFm4vjF9Nx/hb9svKV6O1hcdTAwMDXgkSDVbPhcdTAwMDPFZsTH9+NcdTAwMDVUXHUwMDA1T8R5fk/OfvCrjD9cdTAwMTAxMMaC6u2zIIjvXHUwMDEw1XdoXHUwMDE1YYnsr+wqXGJ/MGlwyoWKXHUwMDE5XHUwMDEyYUXdo4npqKjBjMGU31x1MDAwNLFcdTAwMTCMU5XIwLJbUVxuhlx1MDAxZtl5YWNkwlx1MDAwMynlYaQ3RVx1MDAxMIIgNDndIYz+ZMeM4CP3LIdcdTAwMTJF51lcYlx1MDAwMVwiXGbr73lcdTAwMTJsQlx1MDAxNjiQN0madYJcdTAwMWSpNqtfpaG5pmGOXHUwMDBmr1x1MDAwM2/WOp1qqGxrqH9lvrb5XHUwMDFhqaO72+zaVm8ryav6L1x1MDAxZFx1MDAwMPt61GHG0vf4519cdTAwMWb++lx1MDAxZow/wb0ifQ== Container( id=\"dialog\")Horizontal( classes=\"buttons\")Button(\"Yes\")Button(\"No\")Screen()Container( id=\"sidebar\")Button( \"Install\")match these*don't* match this

Let's say we want to make the text of the buttons in the dialog bold, but we don't want to change the Button in the sidebar. We can do this with the following rule:

#dialog Button {\ntext-style: bold;\n}\n

The #dialog Button selector matches all buttons that are below the widget with an ID of \"dialog\". No other buttons will be matched.

As with all selectors, you can combine as many as you wish. The following will match a Button that is under a Horizontal widget and under a widget with an id of \"dialog\":

#dialog Horizontal Button {\ntext-style: bold;\n}\n
"},{"location":"guide/CSS/#child-combinator","title":"Child combinator","text":"

The child combinator is similar to the descendant combinator but will only match an immediate child. To create a child combinator, separate two selectors with a greater than symbol (>). Any whitespace around the > will be ignored.

Let's use this to match the Button in the sidebar given the following DOM:

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1daXPayFx1MDAxNv2eX+HyfMmrXHUwMDFhNL0vUzX1ylx1MDAxYl6It1x1MDAxOMdxXqamXHUwMDA0yFi2QFjIYDw1//3dxlx1MDAxOFx0kFx1MDAwNDiIQCaaqlx1MDAxOCTRurp9l3P6dvf8/W5jYzPstZzN3zc2naeq7bm1wO5u/mrOd5yg7fpNuET639v+Y1Dt33lcdTAwMWKGrfbvv/3WsIN7J2x5dtWxOm770fba4WPN9a2q3/jNXHKdRvu/5t9cdTAwMTO74fzR8lx1MDAxYrUwsKKHXHUwMDE0nJpcdTAwMWL6wcuzXHUwMDFjz2k4zbBccq3/XHUwMDBmvm9s/N3/NyZd4FRDu1n3nP5cdTAwMGb6l2JcdTAwMDJcdTAwMTLKx0+f+M2+tJgxxKhiWFx1MDAwZe9w27vwwNCpweVcdTAwMWJcdTAwMTDaia6YU5unT1x1MDAwZr3CcfehXFyq1iv+x6B81Ph4XHUwMDE1PffG9byLsOe96MKu3j5cdTAwMDYxqdph4N87V24tvDVPXHUwMDFmOz/8XdtcdTAwMDc1RL9cbvzH+m3TabdHfuO37KpcdTAwMWL24Fx1MDAxY0fDky9a+H0jOvNkblDIYkQxoZSgSpPhRfNrirhFJOiAIcSU0HxMqlx1MDAxZN+Dnlx1MDAwMKl+Qf0jkqtiV+/rIFxcsza8J1xm7Ga7ZVx1MDAwN9Bf0X3d1/dcdTAwMTVieO7Wceu3IZxUKnqe09e6VlJcdTAwMTKCWNQn5iGtw1rfXHUwMDAy/lx1MDAxY1farVx1MDAxZLRcdTAwMDbK2WybLzFcdTAwMDGNbHvj5lx1MDAxMzehWMdcdTAwMWU3gkcquttb+1x1MDAwNz1153/aqYf+zrCtXHUwMDExe1x1MDAwYp2ncHN44Z9fs5ptfLkslfdvtjvh5aej7nn3+bjeqiQ3a1x1MDAwN4HfnbXdq6NPne5cdTAwMDe2e/wh4LX9XHUwMDFleSRsVyygXXJVOzwo3leP1Vx1MDAxNsPlhne61/xSX0C7Oal3vZr98Lh7gPTnw+enXHUwMDFiVSpcdTAwMTZrT3/Zne5P5S6m2eYneVx1MDAxM1xcf9zzd0J9dl1raX5Fb1ZXucGjLu70xNlflza9Pqtf1vjV0dk3iTtcIsWvs75I1OzgU1x1MDAxNGFcdTAwMWZbNfslXHUwMDEzQuiG5KBcdTAwMDX8p/Twuuc27+Fi89HzonN+9T5Knu9i8k6k7Vx1MDAxMTlHMjZT46eHXHUwMDE5XHUwMDFicoMkSNCZXHUwMDEzdnb3rWrC5lx1MDAxOVx0myBLLidh84SEzaO8PEjYkKtcdTAwMDVcdTAwMTOYxSxjYVx1MDAxOXuRxlx1MDAxOPW531xmL9znvj2JkbNFu+F6vZFu61spSLpcdTAwMDOXbbfpXHUwMDA07782zVx1MDAwNbf2x9fNmmt7fv3r5tfmf+JqbjsgjWmek5F2tjy3bix803NuRk0/dFx1MDAwMftcdTAwMGUvN9xaLY5mq6/PPpxcdTAwMDWD+oFbd5u2V55V8kwvzVx1MDAwNtec41RX5eDHWjOGZ/bVx+1tzfZ6qHhyt9XcVzLsnlx1MDAwNEer7qtCWkQhRDGd9FUmqUVcdTAwMTlTXHUwMDA0XHUwMDEx3vfVXHUwMDFjnVWjSWdVYtxZmWZcdTAwMTBBOc7BV7Oy3e5D8S44Oz54OC/uljpN++7y41x1MDAxN/cnul5cdTAwMTS6zkm969VsXuh6vbRQXHUwMDBlbne7lY5TXHUwMDBl6ZGHiXq+P299+NdpIS8ycNCz5e1e62z3ktxXXHUwMDFmcIufXHUwMDFmXHUwMDFl11x1MDAxNtBuVVx1MDAxY5/xc9ooXHUwMDFl1IJS4aSwfVK8JavYa9NIRvJcdTAwMDOjZlx1MDAwN5++P8ngUoyfXHUwMDFlXCJcdTAwMTdcIjHVUszBMrL1vKLIRZJcZuSipKWWhFxcVFx1MDAwMnKZpFx1MDAxOVpSjaj6sVnGXHUwMDAxgPdng9e99+b8XHUwMDBiYK96drvttFx1MDAwMbVXXHUwMDFlw9BvtpdNOKbg8nHCMc9LZDpvNvdQKHVgXzGwXHUwMDEzhYme2YErXb3fKNRDXSn0XHUwMDBlnXapt394WllxXHUwMDA3ZmZcXJ9LKbg0fkFGPVhcYmJhLFx1MDAxOcKcI+M835t6SI6QknHGuFx1MDAxNOqx5fTY/vmhf1cv71x1MDAxNLYudz63y92Ugbaf1GP+dnNS73o1m1x1MDAxN/VYLy3kRT3WS1x1MDAwYp7e3lGV/Vx1MDAxZLYjhHNRLj3Vi8VcdTAwMTW2hbyYx8LFncY8klx1MDAxZlx1MDAxODU7+PT9mYfSLJV5IFwiIFczOfuYabaeV1x1MDAxNbjwLOCiuSWWXHUwMDAzXFySmEdsaPSVeSBcZlxmXGKxXHUwMDFjgMtcdTAwMTRrjFx1MDAwMau8mcd2XHUwMDFmlb//unntXHUwMDAwNE9mXHUwMDE3mI38bEhcdTAwMWaq8DpO8HZ+MVx1MDAwNXyP84txUTPdMJtDQNem+lwiVlxuXHUwMDBijsXsJOLx+aDTPH1+vmS03PN4VTS/PFx1MDAxN1fcXHUwMDE3wcgspFx1MDAxNXDrXHUwMDE3X1x1MDAxY+NcdTAwMTDYklpcIqXRanBcYkqxXHUwMDE2XFzFRiOWwiGuXHUwMDBlXHUwMDBmzz5cXD8/bFx1MDAxZlx1MDAxNC78QtdcdTAwMGWfPznHPznEojhETupdr2bz4lx1MDAxMOulhbw4xHppIS9cdTAwMGWxXlrIq9iycHGnUZPkXHUwMDA3Rs1cdTAwMGU+LVx1MDAxMlxmZmKiNGpCXHUwMDExkuOnI2pcIjlSfJ6iSLaeV1x1MDAxM1x1MDAwZUnEMuBcdTAwMTAwXHUwMDEzslx1MDAxYzg0XHUwMDFiM8GaKVx1MDAwMmLKXHUwMDFjJkuvIDU58ZfNTKYg+lRm8lwiaaZcdTAwMTO+hKxcdTAwMDQvlFx1MDAwNKU6IcdSKCb57E6YXVx0X00nXHUwMDA0hG9cdTAwMTFwQLAyjlx1MDAxMJd8xFx1MDAwYlx1MDAxOcKWkERx+MC0XCKxQu6i3Vx1MDAxMFlAOFx1MDAxMMZcXEumheI0NnQzdEuhISgwXCKgZ1x1MDAwNHiimvRSjLjCwHDo/F7aXHUwMDE3dtle2lx1MDAwZe0g3HabNbdZh4tRrntdjDPLPMS+X1dcdTAwMWbbfTVcIlx1MDAwMb1IJSeIKOgyLWN31e2W4XpcdTAwMTYolzGEjL4pkXhwwzDnbjrN2nSZskuVMZlcbiBcdTAwMTSViMBcdTAwMTNcdMdA9LlSfEIqYlEphVx1MDAwNsJJNIJAKyak8ux2uOM3XHUwMDFhblxiyj/z3WY4ruS+NreMt986dm38KrxV/Np4WGiZXHUwMDE2R/lp9Gkj8pv+l+HnP39NvjvdnM0xYchRe+/if+eOaJjg1MlcdTAwMTbgXHUwMDE1nDGsZ1x1MDAxZvHMhoUrXHUwMDFh0SS2OIGghSRYN4uiSP/XXHUwMDEyWVx1MDAxYZCVMkNcdTAwMWKaxIZDXHUwMDE3XHUwMDBlKzC1qFCKQWDlbGTSRzTogiymXHUwMDAwXHUwMDAyMZCTgj3EJn5cZuq4mijMwWt+rGg2c+RcdTAwMDD9UI6FMj5EXHUwMDA0pFx1MDAxZlx1MDAxNvOiQdzAkKKwXHUwMDE5NWaIIU5cdTAwMDR7YzTLhFx1MDAxZqMyXHUwMDExqjX4rEJIKFxuXHUwMDFkPSlcdTAwMTO4P9dcZnPCzVxuO1x1MDAxMHytg1m6LZtjwopcdTAwMTdcdTAwMTbLIGWkwjOALIRcbj7HvJPs8tuKXHUwMDA2M8aBIyHMTWKkMjZA/lx1MDAxMs2IxbAwI7VcXFx1MDAwYinHxVpcXDTTXHUwMDFh+phA0NRcdTAwMWNDOoutXHUwMDFhioJcdTAwMTmz4CqVcFx1MDAxZsZE0IlZZVx1MDAxOEJcdTAwMTlcdTAwMDNcdTAwMGbG/9ZoVjCggHFMXHUwMDAwalx1MDAxM1xiaYyihHBGLUhcXFx1MDAwMJOAXHUwMDFlQ1xu44K+LZ5lXHUwMDE3ekal4lx1MDAxYfA/1lx1MDAxMlx1MDAxM4hqmuBcdKG4XHUwMDA1uFx1MDAxYVwirJTIyDUp0jpFs0KqMZtjwoznjGaZRTBcdTAwMTnjJWNcdTAwMDGNaCyQmme1nfZKl6Wb+6fS9dVFrfaJ2Fx1MDAxN1x1MDAxZj6ueDhjZlx1MDAwNY8wuYJAzkYyXG4j/eFcYlx1MDAwMbpcdTAwMDeEKlx1MDAwMbVpXHUwMDA0d+RcdTAwMDfOYlWtKICBbPBoKidgXHUwMDE4QGZcdLQknmyWUlx1MDAwYrvrlu9cXO+ycdTuak4qXi0sep++fbD3+Pzw+br49DlcZnadwu1F+7MkbFx1MDAxMTP2161cdTAwMTaWk3pzalZ23IPiw1x1MDAxZW5cdTAwMDR7f9V2w1x1MDAwZemceovQXHUwMDAyQ0FcdTAwMGI3O9WTq2LpRF3tXvnHYWl1tVu5a2ydeVx1MDAxZIF5WNiu6XrpxH/qra64i197PlDD58NcdTAwMTLx2lx1MDAxN6p1etxmndZt6ZBfflO70yoryVxuipp9zY5cdTAwMGJEYpmJNq2yXHUwMDAySDiVNCDgYkQxOnuWzTaLXHUwMDE1zbJmtUl6ltXCgoSGXHUwMDAx5+mcsyxLyLIk0v1rdqVUamqw1uKza96VlVj9YEpl5aJcdTAwMWE4TvN9Sk1Fjty/sJrKXHUwMDE0jDheU1x1MDAxOcqY6XjpfF2lbyZBgD4gTdjs2z9lR87V9DzOsVx1MDAwNWRcdTAwMWNcXItcdTAwMTGqXGJcdTAwMWatpsB3S0klluB5XHUwMDE4XHUwMDAzi1SUaOCZSopYrXnoiIybMlx1MDAwMFx1MDAwN8ebwLtcdTAwMDQhRpjW4lxyeHeVmXq2O2zEx/i4llx1MDAwMuJcdTAwMTJcdTAwMDeWLijhMYo4oMTMQlx1MDAxY1RcZtpcdTAwMWJcXJmTn89R0Vx1MDAxMVx1MDAxOFx0XHUwMDA19JxhXHSBMqGgXHUwMDAzsiCgsohzbpKb1lx1MDAxM1wirVx1MDAxMz9PN15zxMw2auhd/O/bpqfSdGpcdTAwMGVsUYE7izm217huPV2497RyfcnK4fn+XHUwMDE26dyffl7x4EWJtKggXHUwMDFjSHpcdTAwMDJsMJvXYWVqwfqFnH/33eugQ1x1MDAxOFn+XCK34z1WPFdq9+amVun2yrf+VlhOISA/J6jO325O6l2vZnPbvW6ttJBTs7ntXpePuHmNIOQk7nn9oX7uXFw9XHUwMDEwLb94lyeNtt1yUsZRXHUwMDE2tdle4otEzb6Cg+89MEEoSV1cdTAwMDFDMCFCKI5nh1x1MDAxONn9t6JcdTAwMTCDkiyIQVx1MDAwMOsuXHRizLbfXHUwMDFlxpIjSlx1MDAwMX2v39DEN+6313ZrTsVcdTAwMGWWvf/FXHUwMDE04DzThnsjome66pRcdTAwMTVr6ZtjMsaAiDE++3DG9lHTx1xi6JdDjypP5Vx1MDAxMr7URSfFXcfcbtRZx6csvdVZMZrqrVhbXGZpRvCLt45OPmCCWIJcbo6Y+PbRjF8wqSgllEpwVZkweJGw5Vx1MDAwNSZcdTAwMTKx+Fx1MDAwNNalsIHzvaND9CRLN6dXLr66PdNfetf7P9nAothATupdr2bzYlx1MDAwM+ulhbyWq62XXHUwMDE28lqulpO4eW15sV6dtnxOlPxcIjOKe1rd3T65bqn60Ze7Y7vje0/bjM0m7uDTd+daXHUwMDE00dTldUT3t9mLIYVp2C3bLL5cdTAwMGLVmlx1MDAwMb1cdTAwMTGWhd6ktPCi0NuU0dxcdTAwMDT8lrC1uWRMXHUwMDAzXHUwMDA0zFx1MDAwMb+tXHUwMDBl01x1MDAxYSxaM+de+MrXzcNmO7Q9b9k8a1xuXHUwMDFkSVltlyp4ppOmXHUwMDE3jFx1MDAxOUl1Ulx1MDAwNCleUDVcdTAwMDfDyp5cdTAwMTKzmlx1MDAwM1winHFLMCp14oBcYuHCMsMgS3BSxixJiOCCJMzZYNriQjAkJkvFVCBwJPKWXUJ+iEpxwVxmWWlcIoSmXHUwMDEwZzWmOKk8yyjlmsoppeJR+deoYltIMlx1MDAxZnPEXGYnauNd/O/8MYOn1mk55cpUjGfP65m4bDUjXHUwMDA2w8Si4OuvQ6hcdCt2XHUwMDExNsaY91x1MDAxY1x1MDAxM2RcdTAwMTFuZvQogYmAXGJcdTAwMTCb2Fx1MDAxNa3XNZNcdTAwMTckVlx0U6qppGZcdTAwMDe2t+T7VVx1MDAwZVx1MDAxY9njl6OBXHUwMDAzXHUwMDExJFx1MDAxMWdUSCRcdTAwMTiOZlx1MDAxN1xmXHUwMDAzh7RcdTAwMTQliGIzXHUwMDE3XHUwMDA3joR1XHUwMDE3M001yc71XHUwMDFiI4uHQVx1MDAxY7PGiygzUYlINLlOd3K18DqFqnSrNUdkr3OGq3RcdTAwMWWSselcdTAwMTnHRFx1MDAxMqlm5yFS2yd35O7sYLdXrX/0xd72WUWkxKtcdTAwMTVcdTAwMTlDJlx1MDAxNFx1MDAwMI7Go9jhZb8z9mrd31TmyVx1MDAxODkmSdPeJiFcZlx1MDAwNtJcdTAwMDH2IN8y2+1bho5Xi2YnXHUwMDExm9lnt142a05gXGLLRnjrtjde9lx1MDAxZE+e6qpGfjz7VNfQb6WxmZHXXHUwMDFhpy7Jor1ccnxA0EhcdTAwMDVcdTAwMWbIJGQ5x+L67P5fSW+mWFlcdTAwMTC2zNbiXHUwMDE0Y6FGx1x1MDAxNFx1MDAwNMA9zNToXG6qMY92hJRv92hNLU0pXHUwMDE1ZjNcdTAwMWUmaGxCXnxcdTAwMTVX4lx1MDAxYS6zmo/QkZLdXHUwMDBmXHUwMDAxOLJzwijggNxcdTAwMGXAXHUwMDExXHUwMDExXGKMWkNATli4XHUwMDBl0VrNwlW+XHUwMDE5a1x1MDAxOHGkJmbSqlZcdTAwMDSeiVTiYliwMqzM/zvDbFx1MDAwMrXe2CPVes1ReDXcNOTxbtDupt1qXYRgZcNeXHUwMDAwQ3Zrg5BcdTAwMWS93GbHdbrbXHTuddM/TFx1MDAwNOyr0Vx1MDAwNFx1MDAxYce84t//vPvn/yxzRNUifQ== Container( id=\"dialog\")Horizontal( classes=\"buttons\")Button(\"Yes\")Button(\"No\")Screen()Container( id=\"sidebar\")Button( \"Install\")Underline this button

We can use the following CSS to style all buttons which have a parent with an ID of sidebar:

#sidebar > Button {\ntext-style: underline;\n}\n
"},{"location":"guide/CSS/#specificity","title":"Specificity","text":"

It is possible that several selectors match a given widget. If the same style is applied by more than one selector then Textual needs a way to decide which rule wins. It does this by following these rules:

  • The selector with the most IDs wins. For instance #next beats .button and #dialog #next beats #next. If the selectors have the same number of IDs then move to the next rule.

  • The selector with the most class names wins. For instance .button.success beats .success. For the purposes of specificity, pseudo classes are treated the same as regular class names, so .button:hover counts as 2 class names. If the selectors have the same number of class names then move to the next rule.

  • The selector with the most types wins. For instance Container Button beats Button.

"},{"location":"guide/CSS/#important-rules","title":"Important rules","text":"

The specificity rules are usually enough to fix any conflicts in your stylesheets. There is one last way of resolving conflicting selectors which applies to individual rules. If you add the text !important to the end of a rule then it will \"win\" regardless of the specificity.

Warning

Use !important sparingly (if at all) as it can make it difficult to modify your CSS in the future.

Here's an example that makes buttons blue when hovered over with the mouse, regardless of any other selectors that match Buttons:

Button:hover {\nbackground: blue !important;\n}\n
"},{"location":"guide/CSS/#css-variables","title":"CSS Variables","text":"

You can define variables to reduce repetition and encourage consistency in your CSS. Variables in Textual CSS are prefixed with $. Here's an example of how you might define a variable called $border:

$border: wide green;\n

With our variable assigned, we can write $border and it will be substituted with wide green. Consider the following snippet:

#foo {\nborder: $border;\n}\n

This will be translated into:

#foo {\nborder: wide green;\n}\n

Variables allow us to define reusable styling in a single place. If we decide we want to change some aspect of our design in the future, we only have to update a single variable.

Note

Variables can only be used in the values of a CSS declaration. You cannot, for example, refer to a variable inside a selector.

Variables can refer to other variables. Let's say we define a variable $success: lime;. Our $border variable could then be updated to $border: wide $success;, which will be translated to $border: wide lime;.

"},{"location":"guide/actions/","title":"Actions","text":"

Actions are allow-listed functions with a string syntax you can embed in links and bind to keys. In this chapter we will discuss how to create actions and how to run them.

"},{"location":"guide/actions/#action-methods","title":"Action methods","text":"

Action methods are methods on your app or widgets prefixed with action_. Aside from the prefix these are regular methods which you could call directly if you wished.

Information

Action methods may be coroutines (defined with the async keyword).

Let's write an app with a simple action.

actions01.py
from textual.app import App\nfrom textual import events\nclass ActionsApp(App):\ndef action_set_background(self, color: str) -> None:\nself.screen.styles.background = color\ndef on_key(self, event: events.Key) -> None:\nif event.key == \"r\":\nself.action_set_background(\"red\")\nif __name__ == \"__main__\":\napp = ActionsApp()\napp.run()\n

The action_set_background method is an action which sets the background of the screen. The key handler above will call this action if you press the R key.

Although it is possible (and occasionally useful) to call action methods in this way, they are intended to be parsed from an action string. For instance, the string \"set_background('red')\" is an action string which would call self.action_set_background('red').

The following example replaces the immediate call with a call to run_action() which parses an action string and dispatches it to the appropriate method.

actions02.py
from textual import events\nfrom textual.app import App\nclass ActionsApp(App):\ndef action_set_background(self, color: str) -> None:\nself.screen.styles.background = color\nasync def on_key(self, event: events.Key) -> None:\nif event.key == \"r\":\nawait self.run_action(\"set_background('red')\")\nif __name__ == \"__main__\":\napp = ActionsApp()\napp.run()\n

Note that the run_action() method is a coroutine so on_key needs to be prefixed with the async keyword.

You will not typically need this in a real app as Textual will run actions in links or key bindings. Before we discuss these, let's have a closer look at the syntax for action strings.

"},{"location":"guide/actions/#syntax","title":"Syntax","text":"

Action strings have a simple syntax, which for the most part replicates Python's function call syntax.

Important

As much as they look like Python code, Textual does not call Python's eval function to compile action strings.

Action strings have the following format:

  • The name of an action on is own will call the action method with no parameters. For example, an action string of \"bell\" will call action_bell().
  • Actions may be followed by braces containing Python objects. For example, the action string set_background(\"red\") will call action_set_background(\"red\").
  • Actions may be prefixed with a namespace (see below) followed by a dot.
eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nNVaW2/bNlx1MDAxNH7vr1xivIduQK3y8M5cdTAwMDLDkNuKtFl6SdJ0XHUwMDFkhkGV6Fi1LGmScluR/75DJbFkxXZsx8kyIXAsklx1MDAxMlx1MDAwZlx1MDAwZs/3nVx1MDAwYv392dpap7zIbOfVWseeXHUwMDA3flx1MDAxY4W5f9Z54dpPbV5EaYJdtLov0pM8qEb2yzIrXr18OfTzgS2z2Fx1MDAwZqx3XHUwMDFhXHUwMDE1J35cXJQnYZR6QTp8XHUwMDE5lXZY/OI+9/yh/TlLh2GZe/UkXVx1MDAxYkZlml/NZWM7tElZ4Nv/wPu1te/VZ0O63Fx1MDAwNqWfXHUwMDFjx7Z6oOqqXHUwMDA1VES0W/fSpFx1MDAxMlx1MDAxNoQm1ChQajRcIiq2cL7ShtjdQ5lt3eOaOlx1MDAxZr6c9T92//HXeSo/fzvc3Oz725/qaXtRXHUwMDFj75dcdTAwMTdxJVaR4mrqvqLM04E9isKy7+ZutY+eXG79om9cdTAwMWKP5enJcT+xhVs/XHUwMDE5taaZXHUwMDFmROWFe1x1MDAxMalbr5TQXHUwMDFjd+62iHBPUy5cdTAwMDSVhlx1MDAxOcpGne5xajyptDJGa8XAXGLgLcE201x1MDAxOHdcdTAwMDJcdTAwMDX7gVRXLdlXP1x1MDAxOFx1MDAxY6N4SViP6fmC4jyyXHUwMDFldXa9YGk80CC4oVxcSqJlPU/fRsf9XHUwMDEyh3DuXHRmiKT1jlx1MDAxNbbaXG5DgSlupFx1MDAxOXW4ibOdsLKKP9u67Pt5dq2yTiVgQ2h3u902qaZZNXY7se+2j3bPw3LvaHt3a7d7MVx1MDAxNFx1MDAwN2b0rjFcdTAwMWL08zw964x6Lq+/1aKdZKF/ZVcgJeeodC6YqC0vjpJcdTAwMDF2JidxXFy3pcGgNsWq9fLFXHUwMDEyXHUwMDE4XHUwMDAwwshUXHUwMDEwXHUwMDE4YJQoQ+dcdTAwMDfBodj66O++zz92L96dXHL6XyCM5ZenXHUwMDBlXHUwMDAypT3FXHRcdTAwMTdcdTAwMWGhQCVt2FhcdTAwMDVcdTAwMDPtYVx1MDAxYpVMXHUwMDEypcBokPeCXHUwMDAx0K9ay0kwoFxiOM6N4kpcYqNcdTAwMTUsglx1MDAwMmCEK6VcdTAwMDU8Mlxm9lx1MDAwNjD4evr69MPml/M9+lv5XHUwMDFl5M5fK4OBxMU+XHUwMDEyXGaAT4dcdTAwMDE3XHUwMDA0jVx1MDAwMlx1MDAwMOaGXHUwMDAx/7xcdTAwMGXs8+DtRry1l4b7mdVvksMnXHUwMDBlXHUwMDAzXFygp5HrNdHGLVaNo0ChM+CK4ntcdTAwMTRChZt7gYBcdTAwMDfS9sQkXHUwMDEwXHUwMDAwUE9cdTAwMDNjaOSaKYq+aSFcdTAwMThcYsm4RCHF48KgSzdcdTAwMDb7hkT5/plMdXFA5MHu69XBQKNXXFxcdTAwMTVcZkp7Xk5EXHUwMDAwct9UXHUwMDA0XHUwMDEwyVx1MDAxOFx1MDAxN1x1MDAwNOZ3XHUwMDA04dveutjaevO7POSDzUMt4r9cdTAwMGbTlVwioPVUXHUwMDEzXHUwMDAwsFx1MDAxNFx1MDAwMFDdnpTCXHUwMDAwRdNcdTAwMTOKyjFcdTAwMDCAXHUwMDE2XHUwMDFlQ3rmoFxiekUj7+dcdTAwMDZmIMBMYP7bpq4lcIrRXHUwMDE5LG7phbt5knFcdTAwMGYzyixC+LU9pUm5XHUwMDFm/WOrmHas9Vd/XHUwMDE4xVx1MDAxN2NGUUFcdTAwMDBcdTAwMDV8l5Vo5H68lmCqUaCl2OaeXHUwMDE1XHUwMDE256+sX489uVx1MDAxZUfHXHUwMDBlMJ3Y9saRVEaYpYy6y7Sh41x1MDAwMCXx8XX5TtheUZpHx1x1MDAxMUpxMF2qpVx1MDAwMI1x+3Q8XHUwMDBizkBqMT+eTVx1MDAxMoP/Nt/RKflwXlx1MDAxY+yGcL71+mnjmVx04UmmXGIlwNBXqPHsXHUwMDA2uPKAMkFcZsewj90zqpvl0Golz4AzQ1JBbmH14EeB88PGb4rrhlN5aDivXHUwMDA3XHUwMDBlOFx1MDAxNWxcdTAwMTbCcYCKsvlcdTAwMDMguSnQUlx1MDAxMNbCtFtHXHUwMDEwXHUwMDA2iVlcdTAwMDJIPn9QSvzuMP37nLPy2+5OfiZcdTAwMDby8Nvm04Ywl8Zj2lx1MDAwNXVcdTAwMWHjTpef3fLJklx1MDAxOFx1MDAwNDFaXHUwMDFjqGZcdTAwMDSzWlx1MDAxMCOFzFx1MDAwM2JcdTAwMDOCXG7M3Fx1MDAxZdknP2z0+Vx1MDAxZvnkzM9cdTAwMTE3XGLM4mmAeZJgM0F9pepJkTafXHUwMDFlaWP2olx1MDAxNFVcctzfherZXHUwMDAx2Vx1MDAwMqhuY2dZVKs76y1cZlFcdTAwMGKMYqLJpFa6sZWVTSjmgeKCSiUx4aQzXHUwMDAy7Vx1MDAxMH036S1cdTAwMGJq5lx0TlxmwYlcYlx1MDAwN8x4xVx1MDAwNEetjUdccnZLKVx0XHUwMDEx0IghriFPMSXmmKku4bfvSjhXWSBsyOHn5UaUhFFyjJ01m9xcdTAwMTTTd+ZI39wq/axKXHUwMDFhcatQPdwwzbVq9PfS4MStoks86spdUlx1MDAxMalQlVxmdXk96nIklE3Cu0WaXV9cdTAwMWaJZDCHk5QrhZsloHaP4zJcdTAwMTFqXHUwMDE44J/AUNhVttktmWK/KDfT4TAqUfPv0ygp21x1MDAxYa5Uue5Q3rf+Lf7ANTX72nSQuTeOM3v9ba1GTHUz+v7ni4mjp1uyu7q3jLh+37Pm/8WZzGjWbr5hMlxumJ1yVPP8OcbsYPQpUlx1MDAxOTPGXHUwMDAzV1x1MDAxYmGaUIG23YpPmPKEQtunklx1MDAxM4lmJlqC1TQlXHUwMDAyw0m4dHziUUE4ZnWSMCMwXHUwMDAwmVA1XHUwMDEzzNNcdTAwMThHoaTKXGKuTENJV1SmMCNkgKB5XFwqWzpJmJPKZmeuXHLeQO05h8RBacxcdTAwMTh5Y0STzKRcdTAwMTGoYoJcdTAwMDRCXHUwMDE1Q+e0XHUwMDFjmc0+J6n5lXhCXHUwMDEzQVx1MDAxY98z9DL1vrboXGZcdTAwMDdhmFx1MDAwNoxJVCdy3/+azqZbs7u6t1xmeVV0hvQp280jOmNcZlWMucjcbDY7Kv9cdTAwMGbYTN95XHUwMDAy4Fx1MDAwZWJcdEZeLrdslz+V9ihSXHUwMDE51UZcdTAwMDOlzeJSm8qY5L1ALUtlxFx1MDAwM4NuTDrPjJmf1mzCcbBhXHUwMDFlikmFoe40gjeLN9dcdTAwMDdcdTAwMDGG4FuU4Fx1MDAwZnBcdTAwMTCwylL9omQ2O4dcdTAwMWbxhvJcdTAwMThzkSlobjjlnDVGNGmDaIyQjNCIMkOlXHUwMDA2s1x1MDAxY5vNPu5qRovS7arE0Fx1MDAxZThhdIpUKFx1MDAxMVx1MDAwM7fxXHUwMDFhjVFh9v+/ZrNcdTAwMTlcdTAwMDbtru4tW16QzqZcdTAwMTWP2PRcdTAwMTNNzShcdTAwMTdEwvxkZuS23GbBh4NPnzc+XHUwMDFkvTmKN/OMTCGzvlx1MDAxZvRPcjuNzlZVPTJ35pmAcbGghGFcdTAwMTiKITEh4+f6XGY8XHUwMDA21J1rOVx1MDAwZqu0udfPW8rcT4rMz1x1MDAxMVx1MDAxM7c5jcOE8lGD125Ii1OqMVA0S5DW0z3TXHUwMDExtFlcXF+yfqTHWkf1ozr7uKlcdTAwMWb5WeZcdTAwMTW2/Kveolx1MDAxZp/nNnz+08QqUuOXLY9xtDNduGc32qw06Vx1MDAwNu6XqMdcdTAwMTHnoiFE4bUy6ik6p5E925j0W6vqcm+tWMPh0zoz+H757PJfPFx1MDAxMrdyIn0= Optional namespaceAction nameOptional parametersapp.set_background('red')"},{"location":"guide/actions/#parameters","title":"Parameters","text":"

If the action string contains parameters, these must be valid Python literals. Which means you can include numbers, strings, dicts, lists etc. but you can't include variables or references to any other Python symbols.

Consequently \"set_background('blue')\" is a valid action string, but \"set_background(new_color)\" is not \u2014 because new_color is a variable and not a literal.

"},{"location":"guide/actions/#links","title":"Links","text":"

Actions may be embedded as links within console markup. You can create such links with a @click tag.

The following example mounts simple static text with embedded action links.

actions03.pyOutput actions03.py
from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nTEXT = \"\"\"\n[b]Set your background[/b]\n[@click=set_background('red')]Red[/]\n[@click=set_background('green')]Green[/]\n[@click=set_background('blue')]Blue[/]\n\"\"\"\nclass ActionsApp(App):\ndef compose(self) -> ComposeResult:\nyield Static(TEXT)\ndef action_set_background(self, color: str) -> None:\nself.screen.styles.background = color\nif __name__ == \"__main__\":\napp = ActionsApp()\napp.run()\n

ActionsApp Set\u00a0your\u00a0background Red Green Blue

When you click any of the links, Textual runs the \"set_background\" action to change the background to the given color.

"},{"location":"guide/actions/#bindings","title":"Bindings","text":"

Textual will run actions bound to keys. The following example adds key bindings for the R, G, and B keys which call the \"set_background\" action.

actions04.pyOutput actions04.py
from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nTEXT = \"\"\"\n[b]Set your background[/b]\n[@click=set_background('red')]Red[/]\n[@click=set_background('green')]Green[/]\n[@click=set_background('blue')]Blue[/]\n\"\"\"\nclass ActionsApp(App):\nBINDINGS = [\n(\"r\", \"set_background('red')\", \"Red\"),\n(\"g\", \"set_background('green')\", \"Green\"),\n(\"b\", \"set_background('blue')\", \"Blue\"),\n]\ndef compose(self) -> ComposeResult:\nyield Static(TEXT)\ndef action_set_background(self, color: str) -> None:\nself.screen.styles.background = color\nif __name__ == \"__main__\":\napp = ActionsApp()\napp.run()\n

ActionsApp Set\u00a0your\u00a0background Red Green Blue

If you run this example, you can change the background by pressing keys in addition to clicking links.

See the previous section on input for more information on bindings.

"},{"location":"guide/actions/#namespaces","title":"Namespaces","text":"

Textual will look for action methods in the class where they are defined (App, Screen, or Widget). If we were to create a custom widget it can have its own set of actions.

The following example defines a custom widget with its own set_background action.

actions05.pyactions05.tcss actions05.py
from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nTEXT = \"\"\"\n[b]Set your background[/b]\n[@click=set_background('cyan')]Cyan[/]\n[@click=set_background('magenta')]Magenta[/]\n[@click=set_background('yellow')]Yellow[/]\n\"\"\"\nclass ColorSwitcher(Static):\ndef action_set_background(self, color: str) -> None:\nself.styles.background = color\nclass ActionsApp(App):\nCSS_PATH = \"actions05.tcss\"\nBINDINGS = [\n(\"r\", \"set_background('red')\", \"Red\"),\n(\"g\", \"set_background('green')\", \"Green\"),\n(\"b\", \"set_background('blue')\", \"Blue\"),\n]\ndef compose(self) -> ComposeResult:\nyield ColorSwitcher(TEXT)\nyield ColorSwitcher(TEXT)\ndef action_set_background(self, color: str) -> None:\nself.screen.styles.background = color\nif __name__ == \"__main__\":\napp = ActionsApp()\napp.run()\n
actions05.tcss
Screen {\nlayout: grid;\ngrid-size: 1;\ngrid-gutter: 2 4;\ngrid-rows: 1fr;\n}\nColorSwitcher {\nheight: 100%;\nmargin: 2 4;\n}\n

There are two instances of the custom widget mounted. If you click the links in either of them it will changed the background for that widget only. The R, G, and B key bindings are set on the App so will set the background for the screen.

You can optionally prefix an action with a namespace, which tells Textual to run actions for a different object.

Textual supports the following action namespaces:

  • app invokes actions on the App.
  • screen invokes actions on the screen.

In the previous example if you wanted a link to set the background on the app rather than the widget, we could set a link to app.set_background('red').

"},{"location":"guide/actions/#builtin-actions","title":"Builtin actions","text":"

Textual supports the following builtin actions which are defined on the app.

  • action_add_class
  • action_back
  • action_bell
  • action_check_bindings
  • action_focus
  • action_focus_next
  • action_focus_previous
  • action_pop_screen
  • action_push_screen
  • action_quit
  • action_remove_class
  • action_screenshot
  • action_switch_screen
  • action_toggle_class
  • action_toggle_dark
"},{"location":"guide/animation/","title":"Animation","text":"

Ths chapter discusses how to use Textual's animation system to create visual effects such as movement, blending, and fading.

"},{"location":"guide/animation/#animating-styles","title":"Animating styles","text":"

Textual's animator can change an attribute from one value to another in fixed increments over a period of time. You can apply animations to styles such as offset to move widgets around the screen, and opacity to create fading effects.

Apps and widgets both have an animate method which will animate properties on those objects. Additionally, styles objects have an identical animate method which will animate styles.

Let's look at an example of how we can animate the opacity of a widget to make it fade out. The following example app contains a single Static widget which is immediately animated to an opacity of 0.0 (making it invisible) over a duration of two seconds.

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass AnimationApp(App):\ndef compose(self) -> ComposeResult:\nself.box = Static(\"Hello, World!\")\nself.box.styles.background = \"red\"\nself.box.styles.color = \"black\"\nself.box.styles.padding = (1, 2)\nyield self.box\ndef on_mount(self):\nself.box.styles.animate(\"opacity\", value=0.0, duration=2.0)\nif __name__ == \"__main__\":\napp = AnimationApp()\napp.run()\n

The animator updates the value of the opacity attribute on the styles object in small increments over two seconds. Here's what the output will look like after each half a second.

After 0sAfter 0.5sAfter 1sAfter 1.5sAfter 2s

AnimationApp Hello,\u00a0World!

AnimationApp Hello,\u00a0World!

AnimationApp Hello,\u00a0World!

AnimationApp Hello,\u00a0World!

AnimationApp Hello,\u00a0World!

"},{"location":"guide/animation/#duration-and-speed","title":"Duration and Speed","text":"

When requesting an animation you can specify a duration or speed. The duration is how long the animation should take in seconds. The speed is how many units a value should change in one second. For instance, if you animate a value at 0 to 10 with a speed of 2, it will complete in 5 seconds.

"},{"location":"guide/animation/#easing-functions","title":"Easing functions","text":"

The easing function determines the journey a value takes on its way to the target value. It could move at a constant pace, or it might start off slow then accelerate towards its final value. Textual supports a number of easing functions.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1XWVPjRlx1MDAxMH7nV1DeVyzmPrYqlYJcdTAwMDVcdTAwMTJcdTAwMDJLXHUwMDBlczib2ofBXHUwMDFh21x1MDAxM8uSkMZcdTAwMDaW4r+nJVx1MDAxM48vwrnUblX0YGumZ3q+6e6vu3Wztr7e8Ne5bbxfb9irjklcXFxcmMvGRjU/tkXpslx1MDAxNESkXHUwMDFll9mo6NQr+97n5fvNzaEpXHUwMDA21ueJ6dho7MqRSUo/il1cdTAwMTZ1suGm83ZY/lj9XHUwMDFlmaH9Ic+GsS+icEjTxs5nxeQsm9ihTX1cdNr/gvH6+k39O4POXHUwMDE0RTZcdTAwMDFWT1x1MDAwN3BKL05cdTAwMWVlaY1cdTAwMTMjrjSjWuDpXG5X7sBR3sYg7lx1MDAwMlxcXHUwMDFiJNVU42Tvl7TZ/nX76rh1fDDO+4j+dNVcdKd2XZK0/HUysYLp9EeFXHLS0lx1MDAxN9nAnrnY9/812sz8dF9syj5cdTAwMDCYiots1OuntqwuXHUwMDFmkGa56Th/XV9cdTAwMDNNZ03aq5WEmStcdTAwMThRwSMqXHUwMDE0XHUwMDE3XGZRQZhUU2m1n2lcdTAwMWNxXHUwMDAxcsyRRIxcdTAwMTK5gOxDloBcdTAwMWZcdTAwMDDZO1Q/XHUwMDAx2rnpXGZ6gC+Np2t8YdIyN1x1MDAwNXgrrLu8uzOONGZIXHUwMDEwPpX0rev1fWVcdTAwMGXCXCIlg6C0tVx1MDAxM7DCmiuhUZBUXHUwMDA35vtxXHUwMDFkXHUwMDBin1x1MDAxN43YN0V+Z6tGXHJsXHUwMDA2bDXcnVx0pLB5lMdm4nQsXHUwMDA0x1xcXHUwMDEzWtlqKk9cXDpcdTAwMDBhOkqSMJd1XHUwMDA2K+Kk9Kbw2y6NXdpb3GLT+Fx1MDAxZUliSv8hXHUwMDFiXHUwMDBlnVx1MDAwN1x1MDAxOL9lLvWLK2q9W1WQ962JV2ielS2yIa80XHUwMDA28lRPeFtcdTAwMGbRUlx1MDAwZqbvnzdWrm4uebGevXNg2L02+3+78UTa4pkwXFzkraCaIaxkYPZDvKUnh1x1MDAwN7y9d/7n3uFheaJHgra3+t9cdTAwMDFvRUQgXHUwMDE2pdRaXHUwMDAwbdlcdTAwMTJvMaFIYES1XHUwMDE0mi9cdTAwMDB7PdpShFwiKSmjclx1MDAwNXFxJCFtUEyWuEs5Ykpr9sbUhdAgwY7/U3dmwVxuR1ZPM/jwifRcdTAwMDXr2pXsRfL+skuYpkpjXHUwMDFj8utD9G3r38nu2cfty7//6F1cZoa7R1x1MDAwM3aQvDJ9y1xm+o7Xr7pUXHUwMDAwNSWjeJagNX2VjJCQXFxTRojmSC9cdTAwMDBcdTAwMGL0VYJY3XlcdH1pJKSCUsqpRFqH8lx1MDAxZqovXHUwMDAyMFx1MDAwNHMhJWGKXHUwMDEyvURmgpnAXHUwMDAyIf6mbFZcbmCrXHUwMDAw+Htj85zsVamMMYso4lpqSoRcdTAwMTYqNLHV06QogsZcdTAwMTZxJjWD5lx0yVx1MDAwN/VpXHUwMDEyKVwiJHTDoJJzzOb0YYVcIkisWFIou1x1MDAxMlx1MDAwYvVwqrkn6Gp1y/H2xMTj7ZVflXjEfVmnSjpcdTAwMDRcbkPoKlx1MDAxZUo6O+3Wyadd0vz482D3tDX+1L44z/Hzklx1MDAwZV5kx1dLOuDsiDBcdTAwMDZdPuJcdTAwMDRcbvB8q89cdHiRay5cdTAwMTTl0PD/R855acvAXHUwMDAyrJBklrt7wYggirFw2UcnlbJcdTAwMWE8M6lQKlmIlCcklW6W+pb7Ulx1MDAwN1x1MDAxNJqb3TNDl1xcz/mtjtHKUG5oZy1Z2rpsVlx1MDAxZjdza7dcdTAwMTLXS+uqarvzwe1cdTAwMWR8XHUwMDE1T8U+m7l3XHUwMDA3zjagrthfyjdZ4XouNcnxLI5nsYrcW8zhq1x1MDAxMVPIXG6Pp9Vps9y/lGdcdTAwMTe90+ukvfOlO+52bfNbp1x1MDAxNYH0x1x1MDAxNaZcXCqFkObznTiQKVx1MDAxMlxcK2jEXHUwMDE5NFLQkn81XnHyKF5BktaKMvyMYv1cdTAwMTJesSrLvlx1MDAxOa/GJlx1MDAxOX1cdTAwMTPEmlx1MDAwMJkwa+2uXCI2TJ63PNhcdTAwMDdcdTAwMTZMeFx1MDAwNi5w8d0lg7rG2NnL7eUoeNetn0przdaKXHUwMDE4tnLAze3a7T8uXFzYXHUwMDFjIn0= timevalue

Run the following from the command prompt to preview them.

textual easing\n

You can specify which easing method to use via the easing parameter on the animate method. The default easing method is \"in_out_cubic\" which accelerates and then decelerates to produce a pleasing organic motion.

Note

The textual easing preview requires the textual-dev package to be installed (using pip install textual-dev).

"},{"location":"guide/animation/#completion-callbacks","title":"Completion callbacks","text":"

You can pass a callable to the animator via the on_complete parameter. Textual will run the callable when the animation has completed.

"},{"location":"guide/animation/#delaying-animations","title":"Delaying animations","text":"

You can delay the start of an animation with the delay parameter of the animate method. This parameter accepts a float value representing the number of seconds to delay the animation by. For example, self.box.styles.animate(\"opacity\", value=0.0, duration=2.0, delay=5.0) delays the start of the animation by five seconds, meaning the animation will start after 5 seconds and complete 2 seconds after that.

"},{"location":"guide/app/","title":"App Basics","text":"

In this chapter we will cover how to use Textual's App class to create an application. Just enough to get you up to speed. We will go in to more detail in the following chapters.

"},{"location":"guide/app/#the-app-class","title":"The App class","text":"

The first step in building a Textual app is to import the App class and create a subclass. Let's look at the simplest app class:

from textual.app import App\nclass MyApp(App):\npass\n
"},{"location":"guide/app/#the-run-method","title":"The run method","text":"

To run an app we create an instance and call run().

simple02.py
from textual.app import App\nclass MyApp(App):\npass\nif __name__ == \"__main__\":\napp = MyApp()\napp.run()\n

Apps don't get much simpler than this\u2014don't expect it to do much.

Tip

The __name__ == \"__main__\": condition is true only if you run the file with python command. This allows us to import app without running the app immediately. It also allows the devtools run command to run the app in development mode. See the Python docs for more information.

If we run this app with python simple02.py you will see a blank terminal, something like the following:

MyApp

When you call App.run() Textual puts the terminal in to a special state called application mode. When in application mode the terminal will no longer echo what you type. Textual will take over responding to user input (keyboard and mouse) and will update the visible portion of the terminal (i.e. the screen).

If you hit Ctrl+C Textual will exit application mode and return you to the command prompt. Any content you had in the terminal prior to application mode will be restored.

Tip

A side effect of application mode is that you may no longer be able to select and copy text in the usual way. Terminals typically offer a way to bypass this limit with a key modifier. On iTerm you can select text if you hold the Option key. See the documentation for your terminal software for how to select text in application mode.

"},{"location":"guide/app/#events","title":"Events","text":"

Textual has an event system you can use to respond to key presses, mouse actions, and internal state changes. Event handlers are methods prefixed with on_ followed by the name of the event.

One such event is the mount event which is sent to an application after it enters application mode. You can respond to this event by defining a method called on_mount.

Info

You may have noticed we use the term \"send\" and \"sent\" in relation to event handler methods in preference to \"calling\". This is because Textual uses a message passing system where events are passed (or sent) between components. See events for details.

Another such event is the key event which is sent when the user presses a key. The following example contains handlers for both those events:

event01.py
from textual.app import App\nfrom textual import events\nclass EventApp(App):\nCOLORS = [\n\"white\",\n\"maroon\",\n\"red\",\n\"purple\",\n\"fuchsia\",\n\"olive\",\n\"yellow\",\n\"navy\",\n\"teal\",\n\"aqua\",\n]\ndef on_mount(self) -> None:\nself.screen.styles.background = \"darkblue\"\ndef on_key(self, event: events.Key) -> None:\nif event.key.isdecimal():\nself.screen.styles.background = self.COLORS[int(event.key)]\nif __name__ == \"__main__\":\napp = EventApp()\napp.run()\n

The on_mount handler sets the self.screen.styles.background attribute to \"darkblue\" which (as you can probably guess) turns the background blue. Since the mount event is sent immediately after entering application mode, you will see a blue screen when you run this code.

EventApp

The key event handler (on_key) has an event parameter which will receive a Key instance. Every event has an associated event object which will be passed to the handler method if it is present in the method's parameter list.

Note

It is unusual (but not unprecedented) for a method's parameters to affect how it is called. Textual accomplishes this by inspecting the method prior to calling it.

Some events contain additional information you can inspect in the handler. The Key event has a key attribute which is the name of the key that was pressed. The on_key method above uses this attribute to change the background color if any of the keys from 0 to 9 are pressed.

"},{"location":"guide/app/#async-events","title":"Async events","text":"

Textual is powered by Python's asyncio framework which uses the async and await keywords.

Textual knows to await your event handlers if they are coroutines (i.e. prefixed with the async keyword). Regular functions are generally fine unless you plan on integrating other async libraries (such as httpx for reading data from the internet).

Tip

For a friendly introduction to async programming in Python, see FastAPI's concurrent burgers article.

"},{"location":"guide/app/#widgets","title":"Widgets","text":"

Widgets are self-contained components responsible for generating the output for a portion of the screen. Widgets respond to events in much the same way as the App. Most apps that do anything interesting will contain at least one (and probably many) widgets which together form a User Interface.

Widgets can be as simple as a piece of text, a button, or a fully-fledged component like a text editor or file browser (which may contain widgets of their own).

"},{"location":"guide/app/#composing","title":"Composing","text":"

To add widgets to your app implement a compose() method which should return an iterable of Widget instances. A list would work, but it is convenient to yield widgets, making the method a generator.

The following example imports a builtin Welcome widget and yields it from App.compose().

widgets01.py
from textual.app import App, ComposeResult\nfrom textual.widgets import Welcome\nclass WelcomeApp(App):\ndef compose(self) -> ComposeResult:\nyield Welcome()\ndef on_button_pressed(self) -> None:\nself.exit()\nif __name__ == \"__main__\":\napp = WelcomeApp()\napp.run()\n

When you run this code, Textual will mount the Welcome widget which contains Markdown content and a button:

WelcomeApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Welcome!\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b Textual\u00a0is\u00a0a\u00a0TUI,\u00a0or\u00a0Text\u00a0User\u00a0Interface,\u00a0framework\u00a0for\u00a0Python\u00a0inspired\u00a0by\u00a0\u00a0 modern\u00a0web\u00a0development.\u00a0We\u00a0hope\u00a0you\u00a0enjoy\u00a0using\u00a0Textual! Dune\u00a0quote \u258c\u00a0\"I\u00a0must\u00a0not\u00a0fear.Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0little-death\u00a0that \u258c\u00a0brings\u00a0total\u00a0obliteration.I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass \u258c\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner \u258c\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only \u258c\u00a0I\u00a0will\u00a0remain.\" \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0OK\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

Notice the on_button_pressed method which handles the Button.Pressed event sent by a button contained in the Welcome widget. The handler calls App.exit() to exit the app.

"},{"location":"guide/app/#mounting","title":"Mounting","text":"

While composing is the preferred way of adding widgets when your app starts it is sometimes necessary to add new widget(s) in response to events. You can do this by calling mount() which will add a new widget to the UI.

Here's an app which adds a welcome widget in response to any key press:

widgets02.py
from textual.app import App\nfrom textual.widgets import Welcome\nclass WelcomeApp(App):\ndef on_key(self) -> None:\nself.mount(Welcome())\ndef on_button_pressed(self) -> None:\nself.exit()\nif __name__ == \"__main__\":\napp = WelcomeApp()\napp.run()\n

When you first run this you will get a blank screen. Press any key to add the welcome widget. You can even press a key multiple times to add several widgets.

WelcomeApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Welcome!\u2503\u2582\u2582 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b Textual\u00a0is\u00a0a\u00a0TUI,\u00a0or\u00a0Text\u00a0User\u00a0Interface,\u00a0framework\u00a0for\u00a0Python\u00a0inspired\u00a0by modern\u00a0web\u00a0development.\u00a0We\u00a0hope\u00a0you\u00a0enjoy\u00a0using\u00a0Textual! Dune\u00a0quote \u258c\u00a0\"I\u00a0must\u00a0not\u00a0fear.Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0little-death\u00a0 \u258c\u00a0that\u00a0brings\u00a0total\u00a0obliteration.I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0permit\u00a0it\u00a0 \u258c\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn \u258c\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0 \u258c\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\" \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0OK\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

"},{"location":"guide/app/#awaiting-mount","title":"Awaiting mount","text":"

When you mount a widget, Textual will mount everything the widget composes. Textual guarantees that the mounting will be complete by the next message handler, but not immediately after the call to mount(). This may be a problem if you want to make any changes to the widget in the same message handler.

Let's first illustrate the problem with an example. The following code will mount the Welcome widget in response to a key press. It will also attempt to modify the Button in the Welcome widget by changing its label from \"OK\" to \"YES!\".

from textual.app import App\nfrom textual.widgets import Button, Welcome\nclass WelcomeApp(App):\ndef on_key(self) -> None:\nself.mount(Welcome())\nself.query_one(Button).label = \"YES!\" # (1)!\nif __name__ == \"__main__\":\napp = WelcomeApp()\napp.run()\n
  1. See queries for more information on the query_one method.

If you run this example, you will find that Textual raises a NoMatches exception when you press a key. This is because the mount process has not yet completed when we attempt to change the button.

To solve this we can optionally await the result of mount(), which requires we make the function async. This guarantees that by the following line, the Button has been mounted, and we can change its label.

from textual.app import App\nfrom textual.widgets import Button, Welcome\nclass WelcomeApp(App):\nasync def on_key(self) -> None:\nawait self.mount(Welcome())\nself.query_one(Button).label = \"YES!\"\nif __name__ == \"__main__\":\napp = WelcomeApp()\napp.run()\n

Here's the output. Note the changed button text:

WelcomeApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Welcome!\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b Textual\u00a0is\u00a0a\u00a0TUI,\u00a0or\u00a0Text\u00a0User\u00a0Interface,\u00a0framework\u00a0for\u00a0Python\u00a0inspired\u00a0by\u00a0\u00a0 modern\u00a0web\u00a0development.\u00a0We\u00a0hope\u00a0you\u00a0enjoy\u00a0using\u00a0Textual! Dune\u00a0quote \u258c\u00a0\"I\u00a0must\u00a0not\u00a0fear.Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0little-death\u00a0that \u258c\u00a0brings\u00a0total\u00a0obliteration.I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass \u258c\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner \u258c\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only \u258c\u00a0I\u00a0will\u00a0remain.\" \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0YES!\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

"},{"location":"guide/app/#exiting","title":"Exiting","text":"

An app will run until you call App.exit() which will exit application mode and the run method will return. If this is the last line in your code you will return to the command prompt.

The exit method will also accept an optional positional value to be returned by run(). The following example uses this to return the id (identifier) of a clicked button.

question01.py
from textual.app import App, ComposeResult\nfrom textual.widgets import Label, Button\nclass QuestionApp(App[str]):\ndef compose(self) -> ComposeResult:\nyield Label(\"Do you love Textual?\")\nyield Button(\"Yes\", id=\"yes\", variant=\"primary\")\nyield Button(\"No\", id=\"no\", variant=\"error\")\ndef on_button_pressed(self, event: Button.Pressed) -> None:\nself.exit(event.button.id)\nif __name__ == \"__main__\":\napp = QuestionApp()\nreply = app.run()\nprint(reply)\n

Running this app will give you the following:

QuestionApp Do\u00a0you\u00a0love\u00a0Textual? \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Yes\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0No\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

Clicking either of those buttons will exit the app, and the run() method will return either \"yes\" or \"no\" depending on button clicked.

"},{"location":"guide/app/#return-type","title":"Return type","text":"

You may have noticed that we subclassed App[str] rather than the usual App.

question01.py
from textual.app import App, ComposeResult\nfrom textual.widgets import Label, Button\nclass QuestionApp(App[str]):\ndef compose(self) -> ComposeResult:\nyield Label(\"Do you love Textual?\")\nyield Button(\"Yes\", id=\"yes\", variant=\"primary\")\nyield Button(\"No\", id=\"no\", variant=\"error\")\ndef on_button_pressed(self, event: Button.Pressed) -> None:\nself.exit(event.button.id)\nif __name__ == \"__main__\":\napp = QuestionApp()\nreply = app.run()\nprint(reply)\n

The addition of [str] tells mypy that run() is expected to return a string. It may also return None if App.exit() is called without a return value, so the return type of run will be str | None. Replace the str in [str] with the type of the value you intend to call the exit method with.

Note

Type annotations are entirely optional (but recommended) with Textual.

"},{"location":"guide/app/#return-code","title":"Return code","text":"

When you exit a Textual app with App.exit(), you can optionally specify a return code with the return_code parameter.

What are return codes?

Returns codes are a standard feature provided by your operating system. When any application exits it can return an integer to indicate if it was successful or not. A return code of 0 indicates success, any other value indicates that an error occurred. The exact meaning of a non-zero return code is application-dependant.

When a Textual app exits normally, the return code will be 0. If there is an unhandled exception, Textual will set a return code of 1. You may want to set a different value for the return code if there is error condition that you want to differentiate from an unhandled exception.

Here's an example of setting a return code for an error condition:

if critical_error:\nself.exit(return_code=4, message=\"Critical error occurred\")\n

The app's return code can be queried with app.return_code, which will be None if it hasn't been set, or an integer.

Textual won't explicitly exit the process. To exit the app with a return code, you should call sys.exit. Here's how you might do that:

if __name__ == \"__main__\"\napp = MyApp()\napp.run()\nimport sys\nsys.exit(app.return_code or 0)\n
"},{"location":"guide/app/#css","title":"CSS","text":"

Textual apps can reference CSS files which define how your app and widgets will look, while keeping your Python code free of display related code (which tends to be messy).

Info

Textual apps typically use the extension .tcss for external CSS files to differentiate them from browser (.css) files.

The chapter on Textual CSS describes how to use CSS in detail. For now let's look at how your app references external CSS files.

The following example enables loading of CSS by adding a CSS_PATH class variable:

question02.py
from textual.app import App, ComposeResult\nfrom textual.widgets import Button, Label\nclass QuestionApp(App[str]):\nCSS_PATH = \"question02.tcss\"\ndef compose(self) -> ComposeResult:\nyield Label(\"Do you love Textual?\", id=\"question\")\nyield Button(\"Yes\", id=\"yes\", variant=\"primary\")\nyield Button(\"No\", id=\"no\", variant=\"error\")\ndef on_button_pressed(self, event: Button.Pressed) -> None:\nself.exit(event.button.id)\nif __name__ == \"__main__\":\napp = QuestionApp()\nreply = app.run()\nprint(reply)\n

Note

We also added an id to the Label, because we want to style it in the CSS.

If the path is relative (as it is above) then it is taken as relative to where the app is defined. Hence this example references \"question01.tcss\" in the same directory as the Python code. Here is that CSS file:

question02.tcss
Screen {\nlayout: grid;\ngrid-size: 2;\ngrid-gutter: 2;\npadding: 2;\n}\n#question {\nwidth: 100%;\nheight: 100%;\ncolumn-span: 2;\ncontent-align: center bottom;\ntext-style: bold;\n}\nButton {\nwidth: 100%;\n}\n

When \"question02.py\" runs it will load \"question02.tcss\" and update the app and widgets accordingly. Even though the code is almost identical to the previous sample, the app now looks quite different:

QuestionApp Do\u00a0you\u00a0love\u00a0Textual? \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Yes\u00a0\u00a0No\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

"},{"location":"guide/app/#classvar-css","title":"Classvar CSS","text":"

While external CSS files are recommended for most applications, and enable some cool features like live editing, you can also specify the CSS directly within the Python code.

To do this set a CSS class variable on the app to a string containing your CSS.

Here's the question app with classvar CSS:

question03.py
from textual.app import App, ComposeResult\nfrom textual.widgets import Label, Button\nclass QuestionApp(App[str]):\nCSS = \"\"\"\n    Screen {\n        layout: grid;\n        grid-size: 2;\n        grid-gutter: 2;\n        padding: 2;\n    }\n    #question {\n        width: 100%;\n        height: 100%;\n        column-span: 2;\n        content-align: center bottom;\n        text-style: bold;\n    }\n    Button {\n        width: 100%;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Label(\"Do you love Textual?\", id=\"question\")\nyield Button(\"Yes\", id=\"yes\", variant=\"primary\")\nyield Button(\"No\", id=\"no\", variant=\"error\")\ndef on_button_pressed(self, event: Button.Pressed) -> None:\nself.exit(event.button.id)\nif __name__ == \"__main__\":\napp = QuestionApp()\nreply = app.run()\nprint(reply)\n
"},{"location":"guide/app/#title-and-subtitle","title":"Title and subtitle","text":"

Textual apps have a title attribute which is typically the name of your application, and an optional sub_title attribute which adds additional context (such as the file your are working on). By default, title will be set to the name of your App class, and sub_title is empty. You can change these defaults by defining TITLE and SUB_TITLE class variables. Here's an example of that:

question_title01.py
from textual.app import App, ComposeResult\nfrom textual.widgets import Button, Header, Label\nclass MyApp(App[str]):\nCSS_PATH = \"question02.tcss\"\nTITLE = \"A Question App\"\nSUB_TITLE = \"The most important question\"\ndef compose(self) -> ComposeResult:\nyield Header()\nyield Label(\"Do you love Textual?\", id=\"question\")\nyield Button(\"Yes\", id=\"yes\", variant=\"primary\")\nyield Button(\"No\", id=\"no\", variant=\"error\")\ndef on_button_pressed(self, event: Button.Pressed) -> None:\nself.exit(event.button.id)\nif __name__ == \"__main__\":\napp = MyApp()\nreply = app.run()\nprint(reply)\n

Note that the title and subtitle are displayed by the builtin Header widget at the top of the screen:

A\u00a0Question\u00a0App \u2b58A\u00a0Question\u00a0App\u00a0\u2014\u00a0The\u00a0most\u00a0important\u00a0question Do\u00a0you\u00a0love\u00a0Textual? \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Yes\u00a0\u00a0No\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

You can also set the title attributes dynamically within a method of your app. The following example sets the title and subtitle in response to a key press:

question_title02.py
from textual.app import App, ComposeResult\nfrom textual.events import Key\nfrom textual.widgets import Button, Header, Label\nclass MyApp(App[str]):\nCSS_PATH = \"question02.tcss\"\nTITLE = \"A Question App\"\nSUB_TITLE = \"The most important question\"\ndef compose(self) -> ComposeResult:\nyield Header()\nyield Label(\"Do you love Textual?\", id=\"question\")\nyield Button(\"Yes\", id=\"yes\", variant=\"primary\")\nyield Button(\"No\", id=\"no\", variant=\"error\")\ndef on_button_pressed(self, event: Button.Pressed) -> None:\nself.exit(event.button.id)\ndef on_key(self, event: Key):\nself.title = event.key\nself.sub_title = f\"You just pressed {event.key}!\"\nif __name__ == \"__main__\":\napp = MyApp()\nreply = app.run()\nprint(reply)\n

If you run this app and press the T key, you should see the header update accordingly:

A\u00a0Question\u00a0App \u2b58t\u00a0\u2014\u00a0You\u00a0just\u00a0pressed\u00a0t! Do\u00a0you\u00a0love\u00a0Textual? \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Yes\u00a0\u00a0No\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

Info

Note that there is no need to explicitly refresh the screen when setting the title attributes. This is an example of reactivity, which we will cover later in the guide.

"},{"location":"guide/app/#whats-next","title":"What's next","text":"

In the following chapter we will learn more about how to apply styles to your widgets and app.

"},{"location":"guide/command_palette/","title":"Command Palette","text":"

Textual apps have a built-in command palette, which gives users a quick way to access certain functionality within your app.

In this chapter we will explain what a command palette is, how to use it, and how you can add your own commands.

"},{"location":"guide/command_palette/#launching-the-command-palette","title":"Launching the command palette","text":"

Press Ctrl + \\ (ctrl and backslash) to invoke the command palette screen, which contains of a single input widget. Textual will suggest commands as you type in that input. Press Up or Down to select a command from the list, and Enter to invoke it.

Commands are looked up via a fuzzy search, which means Textual will show commands that match the keys you type in the same order, but not necessarily at the start of the command. For instance the \"Toggle light/dark mode\" command will be shown if you type \"to\" (for toggle), but you could also type \"dm\" (to match dark mode). This scheme allows the user to quickly get to a particular command with a minimum of key-presses.

Command PaletteCommand Palette after 't'Command Palette after 'td'

ViewerApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \ud83d\udd0eCommand\u00a0Palette\u00a0Search... \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

ViewerApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \ud83d\udd0et Toggle\u00a0light/dark\u00a0mode Toggle\u00a0the\u00a0application\u00a0between\u00a0light\u00a0and\u00a0dark\u00a0mode Quit\u00a0the\u00a0application Quit\u00a0the\u00a0application\u00a0as\u00a0soon\u00a0as\u00a0possible Ring\u00a0the\u00a0bell Ring\u00a0the\u00a0terminal's\u00a0'bell' \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

ViewerApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \ud83d\udd0etd Toggle\u00a0light/dark\u00a0mode Toggle\u00a0the\u00a0application\u00a0between\u00a0light\u00a0and\u00a0dark\u00a0mode \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

"},{"location":"guide/command_palette/#default-commands","title":"Default commands","text":"

Textual apps have the following commands enabled by default:

  • \"Toggle light/dark mode\" This will toggle between light and dark mode, by setting App.dark to either True or False.
  • \"Quit the application\" Quits the application. The equivalent of pressing ++ctrl+C++.
  • \"Play the bell\" Plays the terminal bell, by calling App.bell.
"},{"location":"guide/command_palette/#command-providers","title":"Command providers","text":"

To add your own command(s) to the command palette, define a command.Provider class then add it to the COMMANDS class var on your App class.

Let's look at a simple example which adds the ability to open Python files via the command palette.

The following example will display a blank screen initially, but if you bring up the command palette and start typing the name of a Python file, it will show the command to open it.

Tip

If you are running that example from the repository, you may want to add some additional Python files to see how the examples works with multiple files.

command01.py
from __future__ import annotations\nfrom functools import partial\nfrom pathlib import Path\nfrom rich.syntax import Syntax\nfrom textual.app import App, ComposeResult\nfrom textual.command import Hit, Hits, Provider\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Static\nclass PythonFileCommands(Provider):\n\"\"\"A command provider to open a Python file in the current working directory.\"\"\"\ndef read_files(self) -> list[Path]:\n\"\"\"Get a list of Python files in the current working directory.\"\"\"\nreturn list(Path(\"./\").glob(\"*.py\"))\nasync def startup(self) -> None:  # (1)!\n\"\"\"Called once when the command palette is opened, prior to searching.\"\"\"\nworker = self.app.run_worker(self.read_files, thread=True)\nself.python_paths = await worker.wait()\nasync def search(self, query: str) -> Hits:  # (2)!\n\"\"\"Search for Python files.\"\"\"\nmatcher = self.matcher(query)  # (3)!\napp = self.app\nassert isinstance(app, ViewerApp)\nfor path in self.python_paths:\ncommand = f\"open {str(path)}\"\nscore = matcher.match(command)  # (4)!\nif score > 0:\nyield Hit(\nscore,\nmatcher.highlight(command),  # (5)!\npartial(app.open_file, path),\nhelp=\"Open this file in the viewer\",\n)\nclass ViewerApp(App):\n\"\"\"Demonstrate a command source.\"\"\"\nCOMMANDS = App.COMMANDS | {PythonFileCommands}  # (6)!\ndef compose(self) -> ComposeResult:\nwith VerticalScroll():\nyield Static(id=\"code\", expand=True)\ndef open_file(self, path: Path) -> None:\n\"\"\"Open and display a file with syntax highlighting.\"\"\"\nsyntax = Syntax.from_path(\nstr(path),\nline_numbers=True,\nword_wrap=False,\nindent_guides=True,\ntheme=\"github-dark\",\n)\nself.query_one(\"#code\", Static).update(syntax)\nif __name__ == \"__main__\":\napp = ViewerApp()\napp.run()\n
  1. This method is called when the command palette is first opened.
  2. Called on each key-press.
  3. Get a Matcher instance to compare against hits.
  4. Use the matcher to get a score.
  5. Highlights matching letters in the search.
  6. Adds our custom command provider and the default command provider.

There are three methods you can override in a command provider: startup, search, and shutdown. All of these methods should be coroutines (async def). Only search is required, the other methods are optional. Let's explore those methods in detail.

"},{"location":"guide/command_palette/#startup-method","title":"startup method","text":"

The startup method is called when the command palette is opened. You can use this method as way of performing work that needs to be done prior to searching. In the example, we use this method to get the Python (.py) files in the current working directory.

"},{"location":"guide/command_palette/#search-method","title":"search method","text":"

The search method is responsible for finding results (or hits) that match the user's input. This method should yield Hit objects for any command that matches the query argument.

Exactly how the matching is implemented is up to the author of the command provider, but we recommend using the builtin fuzzy matcher object, which you can get by calling matcher. This object has a match() method which compares the user's search term against the potential command and returns a score. A score of zero means no hit, and you can discard the potential command. A score of above zero indicates the confidence in the result, where 1 is an exact match, and anything lower indicates a less confident match.

The Hit contains information about the score (used in ordering) and how the hit should be displayed, and an optional help string. It also contains a callback, which will be run if the user selects that command.

In the example above, the callback is a lambda which calls the open_file method in the example app.

Note

Unlike most other places in Textual, errors in command provider will not exit the app. This is a deliberate design decision taken to prevent a single broken Provider class from making the command palette unusable. Errors in command providers will be logged to the console.

"},{"location":"guide/command_palette/#shutdown-method","title":"Shutdown method","text":"

The shutdown method is called when the command palette is closed. You can use this as a hook to gracefully close any objects you created in startup.

"},{"location":"guide/command_palette/#screen-commands","title":"Screen commands","text":"

You can also associate commands with a screen by adding a COMMANDS class var to your Screen class.

Commands defined on a screen are only considered when that screen is active. You can use this to implement commands that are specific to a particular screen, that wouldn't be applicable everywhere in the app.

"},{"location":"guide/command_palette/#disabling-the-command-palette","title":"Disabling the command palette","text":"

The command palette is enabled by default. If you would prefer not to have the command palette, you can set ENABLE_COMMAND_PALETTE = False on your app class.

Here's an app class with no command palette:

class NoPaletteApp(App):\nENABLE_COMMAND_PALETTE = False\n
"},{"location":"guide/design/","title":"Design System","text":"

Textual's design system consists of a number of predefined colors and guidelines for how to use them in your app.

You don't have to follow these guidelines, but if you do, you will be able to mix builtin widgets with third party widgets and your own creations, without worrying about clashing colors.

Information

Textual's color system is based on Google's Material design system, modified to suit the terminal.

"},{"location":"guide/design/#designing-with-colors","title":"Designing with Colors","text":"

Textual pre-defines a number of colors as CSS variables. For instance, the CSS variable $primary is set to #004578 (the blue used in headers). You can use $primary in place of the color in the background and color rules, or other any other rule that accepts a color.

Here's an example of CSS that uses color variables:

MyWidget {\nbackground: $primary;\ncolor: $text;\n}\n

Using variables rather than explicit colors allows Textual to apply color themes. Textual supplies a default light and dark theme, but in the future many more themes will be available.

"},{"location":"guide/design/#base-colors","title":"Base Colors","text":"

There are 12 base colors defined in the color scheme. The following table lists each of the color names (as used in CSS) and a description of where to use them.

Color Description $primary The primary color, can be considered the branding color. Typically used for titles, and backgrounds for strong emphasis. $secondary An alternative branding color, used for similar purposes as $primary, where an app needs to differentiate something from the primary color. $primary-background The primary color applied to a background. On light mode this is the same as $primary. In dark mode this is a dimmed version of $primary. $secondary-background The secondary color applied to a background. On light mode this is the same as $secondary. In dark mode this is a dimmed version of $secondary. $background A color used for the background, where there is no content. $surface The color underneath text. $panel A color used to differentiate a part of the UI form the main content. Typically used for dialogs or sidebars. $boost A color with alpha that can be used to create layers on a background. $warning Indicates a warning. Text or background. $error Indicates an error. Text or background. $success Used to indicate success. Text or background. $accent Used sparingly to draw attention to a part of the UI (typically borders around focused widgets)."},{"location":"guide/design/#shades","title":"Shades","text":"

For every color, Textual generates 3 dark shades and 3 light shades.

  • Add -lighten-1, -lighten-2, or -lighten-3 to the color's variable name to get lighter shades (3 is the lightest).
  • Add -darken-1, -darken-2, and -darken-3 to a color to get the darker shades (3 is the darkest).

For example, $secondary-darken-1 is a slightly darkened $secondary, and $error-lighten-3 is a very light version of the $error color.

"},{"location":"guide/design/#dark-mode","title":"Dark mode","text":"

There are two color themes in Textual, a light mode and dark mode. You can switch between them by toggling the dark attribute on the App class.

In dark mode $background and $surface are off-black. Dark mode also set $primary-background and $secondary-background to dark versions of $primary and $secondary.

"},{"location":"guide/design/#text-color","title":"Text color","text":"

The design system defines three CSS variables you should use for text color.

  • $text sets the color of text in your app. Most text in your app should have this color.
  • $text-muted sets a slightly faded text color. Use this for text which has lower importance. For instance a sub-title or supplementary information.
  • $text-disabled sets faded out text which indicates it has been disabled. For instance, menu items which are not applicable and can't be clicked.

You can set these colors via the color property. The design system uses auto colors for text, which means that Textual will pick either white or black (whichever has better contrast).

Information

These text colors all have some alpha applied, so that even $text isn't pure white or pure black. This is done because blending in a little of the background color produces text that is not so harsh on the eyes.

"},{"location":"guide/design/#theming","title":"Theming","text":"

In a future version of Textual you will be able to modify theme colors directly, and allow users to configure preferred themes.

"},{"location":"guide/design/#color-preview","title":"Color Preview","text":"

Run the following from the command line to preview the colors defined in the color system:

textual colors\n
"},{"location":"guide/design/#theme-reference","title":"Theme Reference","text":"

Here's a list of the colors defined in the default light and dark themes.

Textual\u00a0Theme\u00a0Colors \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Light\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Dark\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$background-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0$primary-background-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-background-darken-3\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0$primary-background-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-background-darken-2\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0$primary-background-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-background-darken-1\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-background\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-background\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0$primary-background-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-background-lighten-1\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0$primary-background-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-background-lighten-2\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0$primary-background-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$primary-background-lighten-3\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0$secondary-background-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-background-darken-3\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0$secondary-background-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-background-darken-2\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0$secondary-background-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-background-darken-1\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-background\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-background\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0$secondary-background-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-background-lighten-1\u00a0\u00a0\u00a0 \u00a0\u00a0$secondary-background-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-background-lighten-2\u00a0\u00a0\u00a0 \u00a0\u00a0$secondary-background-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0$secondary-background-lighten-3\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$surface-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$panel-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$boost-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$warning-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$error-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$success-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-darken-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-darken-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-darken-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-lighten-1\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-lighten-2\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$accent-lighten-3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0

"},{"location":"guide/devtools/","title":"Devtools","text":"

Note

If you don't have the textual command on your path, you may have forgotten to install the textual-dev package.

See getting started for details.

Textual comes with a command line application of the same name. The textual command is a super useful tool that will help you to build apps.

Take a moment to look through the available subcommands. There will be even more helpful tools here in the future.

textual --help\n
"},{"location":"guide/devtools/#run","title":"Run","text":"

The run sub-command runs Textual apps. If you supply a path to a Python file it will load and run the app.

textual run my_app.py\n

This is equivalent to running python my_app.py from the command prompt, but will allow you to set various switches which can help you debug, such as --dev which enable the Console.

See the run subcommand's help for details:

textual run --help\n

You can also run Textual apps from a python import. The following command would import music.play and run a Textual app in that module:

textual run music.play\n

This assumes you have a Textual app instance called app in music.play. If your app has a different name, you can append it after a colon:

textual run music.play:MusicPlayerApp\n

Note

This works for both Textual app instances and classes.

"},{"location":"guide/devtools/#running-from-commands","title":"Running from commands","text":"

If your app is installed as a command line script, you can use the -c switch to run it. For instance, the following will run the textual colors command:

textual run -c textual colors\n
"},{"location":"guide/devtools/#live-editing","title":"Live editing","text":"

If you combine the run command with the --dev switch your app will run in development mode.

textual run --dev my_app.py\n

One of the features of dev mode is live editing of CSS files: any changes to your CSS will be reflected in the terminal a few milliseconds later.

This is a great feature for iterating on your app's look and feel. Open the CSS in your editor and have your app running in a terminal. Edits to your CSS will appear almost immediately after you save.

"},{"location":"guide/devtools/#console","title":"Console","text":"

When building a typical terminal application you are generally unable to use print when debugging (or log to the console). This is because anything you write to standard output will overwrite application content. Textual has a solution to this in the form of a debug console which restores print and adds a few additional features to help you debug.

To use the console, open up two terminal emulators. Run the following in one of the terminals:

textual console\n

You should see the Textual devtools welcome message:

textual\u00a0console \u258cTextual\u00a0Development\u00a0Console\u00a0v0.36.0 \u258cRun\u00a0a\u00a0Textual\u00a0app\u00a0with\u00a0textual\u00a0run\u00a0--dev\u00a0my_app.py\u00a0to\u00a0connect. \u258cPress\u00a0Ctrl+C\u00a0to\u00a0quit.

In the other console, run your application with textual run and the --dev switch:

textual run --dev my_app.py\n

Anything you print from your application will be displayed in the console window. Textual will also write log messages to this window which may be helpful when debugging your application.

"},{"location":"guide/devtools/#increasing-verbosity","title":"Increasing verbosity","text":"

Textual writes log messages to inform you about certain events, such as when the user presses a key or clicks on the terminal. To avoid swamping you with too much information, some events are marked as \"verbose\" and will be excluded from the logs. If you want to see these log messages, you can add the -v switch.

textual console -v\n
"},{"location":"guide/devtools/#decreasing-verbosity","title":"Decreasing verbosity","text":"

Log messages are classififed in to groups, and the -x flag can be used to exclude all message from a group. The groups are: EVENT, DEBUG, INFO, WARNING, ERROR, PRINT, SYSTEM, and LOGGING. The group a message belongs to is printed after its timestamp.

Multiple groups may be excluded, for example to exclude everything except warning, errors, and print statements:

textual console -x SYSTEM -x EVENT -x DEBUG -x INFO\n
"},{"location":"guide/devtools/#custom-port","title":"Custom port","text":"

You can use the option --port to specify a custom port to run the console on, which comes in handy if you have other software running on the port that Textual uses by default:

textual console --port 7342\n

Then, use the command run with the same --port option:

textual run --dev --port 7342 my_app.py\n
"},{"location":"guide/devtools/#textual-log","title":"Textual log","text":"

Use the log function to pretty-print data structures and anything that Rich can display.

You can import the log function as follows:

from textual import log\n

Here's a few examples of writing to the console, with log:

def on_mount(self) -> None:\nlog(\"Hello, World\")  # simple string\nlog(locals())  # Log local variables\nlog(children=self.children, pi=3.141592)  # key/values\nlog(self.tree)  # Rich renderables\n
"},{"location":"guide/devtools/#log-method","title":"Log method","text":"

There's a convenient shortcut to log on the App and Widget objects. This is useful in event handlers. Here's an example:

from textual.app import App\nclass LogApp(App):\ndef on_load(self):\nself.log(\"In the log handler!\", pi=3.141529)\ndef on_mount(self):\nself.log(self.tree)\nif __name__ == \"__main__\":\nLogApp().run()\n
"},{"location":"guide/devtools/#logging-handler","title":"Logging handler","text":"

Textual has a logging handler which will write anything logged via the builtin logging library to the devtools. This may be useful if you have a third-party library that uses the logging module, and you want to see those logs with Textual logs.

Note

The logging library works with strings only, so you won't be able to log Rich renderables such as self.tree with the logging handler.

Here's an example of configuring logging to use the TextualHandler.

import logging\nfrom textual.app import App\nfrom textual.logging import TextualHandler\nlogging.basicConfig(\nlevel=\"NOTSET\",\nhandlers=[TextualHandler()],\n)\nclass LogApp(App):\n\"\"\"Using logging with Textual.\"\"\"\ndef on_mount(self) -> None:\nlogging.debug(\"Logged via TextualHandler\")\nif __name__ == \"__main__\":\nLogApp().run()\n
"},{"location":"guide/events/","title":"Events and Messages","text":"

We've used event handler methods in many of the examples in this guide. This chapter explores events and messages (see below) in more detail.

"},{"location":"guide/events/#messages","title":"Messages","text":"

Events are a particular kind of message sent by Textual in response to input and other state changes. Events are reserved for use by Textual, but you can also create custom messages for the purpose of coordinating between widgets in your app.

More on that later, but for now keep in mind that events are also messages, and anything that is true of messages is true of events.

"},{"location":"guide/events/#message-queue","title":"Message Queue","text":"

Every App and Widget object contains a message queue. You can think of a message queue as orders at a restaurant. The chef takes an order and makes the dish. Orders that arrive while the chef is cooking are placed in a line. When the chef has finished a dish they pick up the next order in the line.

Textual processes messages in the same way. Messages are picked off a queue and processed (cooked) by a handler method. This guarantees messages and events are processed even if your code can not handle them right away.

This processing of messages is done within an asyncio Task which is started when you mount the widget. The task monitors a queue for new messages and dispatches them to the appropriate handler when they arrive.

Tip

The FastAPI docs have an excellent introduction to Python async programming.

By way of an example, let's consider what happens if you were to type \"Text\" in to a Input widget. When you hit the T key, Textual creates a key event and sends it to the widget's message queue. Ditto for E, X, and T.

The widget's task will pick the first message from the queue (a key event for the T key) and call the on_key method with the event as the first argument. In other words it will call Input.on_key(event), which updates the display to show the new letter.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXGtT28hcdTAwMTL9zq+g2C97q4Iyj57XVm3dXG6Eh8NcdTAwMWJCSPbuXHUwMDE2JWzZViw/sGRcZknlv99cdTAwMWVcdTAwMDGWLEu2McaYutdcdFx1MDAxOEujUVvTp/ucnpF+rqyurkV3XHUwMDFkb+2P1TXvtuxcdTAwMDZ+pev2197Z7TdeN/TbLdzF4s9hu9ctxy3rUdRcdP94/77pdlx1MDAxYl7UXHTcsufc+GHPXHLCqFfx20653XzvR14z/Lf9feg2vT877WYl6jrJSda9ilx1MDAxZrW79+fyXHUwMDAyr+m1olx1MDAxMHv/XHUwMDBmfl5d/Vx1MDAxOf9OWVx1MDAxN/gtL25cdTAwMWJvTWzjhGW3XHUwMDFltluxnYxpJYFcbjlo4IdcdTAwMWbxTJFXwb1VtNZL9thNa3ew7a9v1GuNw6P92uXn453d/r5Mzlr1g+Asulx1MDAwYu4vgluu97opm8Ko2254XHUwMDE3fiWqP16z1PbBcVx1MDAxNTeso1x1MDAwMYPd3XavVm95of3udLC13XHLfnRnt1x1MDAxMTLY6rZqcSfJllv8XHUwMDA0TDlcXFFiuFx1MDAxMoNcdTAwMWT2UC7AoZRRZncxo1x1MDAxNM9cdTAwMTi12Vx1MDAwZXBcdTAwMDTQqN9I/EqsunLLjVx1MDAxYZrWqlxm2kRdt1x1MDAxNXbcLo5T0q7/+HVcdTAwMWRJQSpcbmBwREApXHUwMDE4NKl7fq1cdTAwMWVZczRzXHUwMDE4lZRcbs2pXHUwMDAyloxL6MVDXCK0XHUwMDA2LiE51FrQKVVit/gne0HrbrfzcN3WYktT1tuPWymfSlx1MDAwZe51Ku69XHUwMDAzUCk5XHUwMDEzaCmXLLmg6GdcctzZ6lx1MDAwNUGyrV1u5PhMXHUwMDE4ud1ow29V/FYte4jXqlx1MDAxNOxcdNww2mw3m36EZlx1MDAxY7f9VpRtXHUwMDEx9/uh2233655byem5cF/HdpeAyL6Sv1ZcdTAwMTO3iT9cZv7+511u6+Ihta+RwUy6W0m//3r3RDxLkt36iGdcdTAwMDVUXHUwMDEyTVJccibhueyT7cMvnXWz8dm/8b5cdTAwMDU7p+ftT0uPZ0Ul4llcdTAwMWJcIllcdTAwMTbP3MGQxlx0XHUwMDA3/Mf0y8HZOIRxjfiQglBcbjRcdTAwMGbNyjhMMcE5XHUwMDA1wVjyVVx1MDAxZsBMNTWMS6lcdTAwMTKY/1x1MDAxZs6vXGLnwiG1r+xgPlx1MDAxMcxdr1x1MDAxY937clx1MDAwZaKFVEWIplxuXHUwMDA3jFx1MDAwYlx1MDAwNXpqSJ9/b1xis8v5XHUwMDA1bFx1MDAxZVx1MDAxZV1Hd5dHSjZngzTNuuDjcWFcdTAwMWIpylxcMzRwR2mBVCSDaClExognYfg3KEuvKlx1MDAwMHJcdTAwMTKyTK7pXHUwMDAwszpcdTAwMTXFXHUwMDFlUMqBMMFcdTAwMDQkXHUwMDA2z1xypVx1MDAwMyf6mfK0gc9E3m1cdTAwMTJ4Ulx1MDAwM1xcPWq4ncjb0Xe1T0dbknxcdTAwMDH6w19cdTAwMWK0+/Uuv9v7gzfV0Y3r9Xav3fN+dHHEXHUwMDBmw8Nob/gsj+d3LepS/T46+lx1MDAxY0PLWMxcZn3/NFxcaCFcXDDVcmGQ1k6NlvyLufRo0Vx1MDAwNWjR4DxcdTAwMGIv4ymsyEFcZuNZxFx1MDAwMKXKMMlnSGuh/bDotFZtt6Iz/0csiMjQ1m236Vx1MDAwNzGvXHUwMDE4bI6d0mrBXHUwMDFia5Oz59393vDu/vx77fPfa/9KX9rQi1x0XHUwMDFj2meGXHUwMDBl/lx1MDAxMPi1Vky9sFx1MDAwM6875OCRj9pv0KDpVyrphFFGi1xc7LNbmibOt7t+zW+5weexXHUwMDA2z560kPNcdTAwMTYmLcGUXCKYJNnUMCTVjdb2xkWTXHUwMDFk7PbPvm9dnVx1MDAxZJWbfPlhSFx1MDAxZKo5Q6bJjaaSXHUwMDBmYVFcYuEg61dcdTAwMDb5qFx1MDAwNinUc3A5hzymKCVcblxy0U+H5Wx57H5kw89cdTAwMDcnXHUwMDFmLjpy0z24U7J1XHUwMDFhfe3fVvNcdTAwMTNOjK2JeWxSesw/4fKlMfSeQlx1MDAwMCHTXHUwMDA0LZWeXsiNv8xLXHUwMDBiIDlcdTAwMGVAmMzMvFx1MDAwMDSPxEa1llx1MDAwNNW+nIFcbr7hzOYtOrNNSFx1MDAwNlx1MDAxMzObNzGz3VPbXHUwMDFjUFwiqSpcdTAwMDIlXHUwMDAwXHUwMDAzxSVMXHLJ8VR7SSEpNHU4IZwrKrVG2TNcdTAwMDRJhTnN4GZNWTHNxGtULavx6azqoqRiUo5iXHUwMDExUypcdTAwMDDmKq2I1Pa/XHUwMDE4hSaAY00kWnMlXHUwMDA1g5HSikBcdTAwMWLwXHUwMDFi0DdaWUny3WPdf1x1MDAxYcpcdTAwMTdDu9yzVq5cdTAwMTNcdTAwMDdcdTAwMDNcdTAwMTWOlK3uXHUwMDBiW1xiXHUwMDAzwVPtam4njmeDwXzYNci5w/WcXCJ7dvrkR61xcvDtU/PM6NBcdTAwMWNdnlx1MDAxZbM8e9BcdTAwMWPCLP1AUqRMbFx1MDAxNchcdTAwMTF7XHUwMDE4sVU9Jlx1MDAwNGZcdTAwMDKQXHUwMDFj4/6IWXMuJmVcdTAwMDPBXFzrScWebF8jPpx0t5J+n4mcayOzW1x1MDAxM26BmVRSdIqpXHUwMDAz2aHZuNBcdTAwMDY2TrpcdTAwMDem8uV6//r6Q+922Vx1MDAwM1x1MDAxOVx1MDAwMHFQgiB70KhGbVx1MDAxMW0okiFcdTAwMWVcdTAwMWNFiFx1MDAxMdxcdTAwMDBcdTAwMGVcdTAwMDFJz7W8XHUwMDBlPVx1MDAxN1x1MDAxOHpcdTAwMTFcdTAwMWJcdTAwMGIrM92Prdzp175931xc3708//bps/6q6mqDPYeev1C3k1h//lx0p7T2W2f/ekdvVIOvXHUwMDE3f7ntvV23tHnWfltFMVxyxTVkQe0sr2HTU5fxw7e0iFx1MDAxN2NcdTAwMTGvucPmhvh56Fx0rVx1MDAwNDpH+ov9L8iJ20XLiVx06WuinLidKCdcbivVKchlQGlcYqXUwPRcdTAwMDJfbrPaycc72DpcdTAwMTW9nql978nLzt2yQ1JcdTAwMTnioGRcdTAwMWWpU1x1MDAwYtxupFYvNbeD3DdcdTAwMDeAXCJcdTAwMGJANEzb4MhcdTAwMTZcXFx1MDAxM1uufPNcdTAwMTR80yGIWlx1MDAwM1x1MDAwZvB6uDVv9brn9bx8XFzroYNcdTAwMDawXHK8ajRcdTAwMDbVUbtTXHUwMDA06aEvk8XvsEFjcVtYXHUwMDA2oHRMOqXcIJtcdTAwMTdPwO748V5S7GqgXHUwMDBlXHUwMDAwXHUwMDE4SbmmXFyZXGaCNThcZlx1MDAxOH1BXGZT7lCFSoZcYsZcdTAwMDQjyYBcZlx1MDAxMG2UwzhcdTAwMDGJTYhSSo+sl1x1MDAwMkzzgmj2XHUwMDAypPo161x1MDAwMONzwWq2XHUwMDBlgNFXgWGUaaF5stZg9VF3S0ehKJSzVlx1MDAwMcbn12FrcDhccjNKcCEwL+SWXHUwMDAwXGJcdTAwMDAj2MagXHUwMDFlXHUwMDAypkdsekslgPVCJ473jvhv0t9K+r0oflV8t9lOe2lqdkFcdTAwMTSuXHUwMDEyY4Rb5snp9Ms+x1x1MDAxN3qWNIBh8HK0XHUwMDE0dnEnXHUwMDE1SpiMXHUwMDFlwKjgMGUwglx1MDAwMTJcdTAwMTHgJmPY0+LYfUEzt1x1MDAwMKByuFxiZcaRoCG9IPUhZiFOXHUwMDE5oF2zyILnkJJnrFxmeZff7yTNLi5PXCLYgNOrvd3u7dFx+aJeOjmeVrP/pc7Pt672zlx1MDAwZvC6d3+cd8PN2+rB/DiUVjzh7i+k2XnxQlx1MDAxNiaBaOtPU0M0/2IuPUTNWIgqjvKBXHUwMDAzkZZoPFx1MDAxN6LjanR5cmF0/lx1MDAwZrRWXHUwMDEyg/lbWX89k2Bvty5R9/5cdTAwMWXL4MVcbvVcdFkmS/SHXHKdXHSBjFx1MDAxNM/BI/9QWpjpk+TJlemTm6PNXHUwMDEyLdPti7vatquDcNlcdTAwMTGokIMgK2RcXFKquExcdTAwMTWm4/k+TVx1MDAxZGOsXG5cdTAwMDDQ3OiXypGU58zyjep1ZVxiVUy/XHUwMDAw/F4x0yxUrW9ZsKzW3Vx1MDAxNuKwm1x1MDAwZu7FqvVhg8aCuFCtKz6O6zKM2IRNL9bHXHUwMDBm97LCWHNcdTAwMDcpP1hcdTAwMWOPwlhcdTAwMTPqXGKhha1+XHUwMDEzolPMZr4wNlxmo1x1MDAwNWpNlN2a4agksXNcdTAwMDBqQVx1MDAxZFwiXHUwMDA0xWCj8Vx1MDAwN1LceLDIRtgpU/1cdTAwMDIludeU7OOTw2p6qlxcoTI2wOyEoFxyeCzV6GHeXHUwMDFlXHUwMDFjylx1MDAxNzNtz/GyKM5cdGpx1KlKiFFjhMM1XHUwMDAwupZUWinJzYhRb0qxXHUwMDE3+rB9jXhv0t1K+n2mSXtKivVcdTAwMDBcdTAwMDVcdTAwMTTtXHUwMDA0zZk+jqldz5elXm1cdTAwMDM2LkNZ+Ytz9/Ro2eNcdTAwMThyfMdII6XAK0+0XHUwMDFjjmNcXChcdTAwMDe4jitIhEry2ktqhTBcdTAwMDQlXHUwMDAxWfD0Qalx2bioXHUwMDA2/CvtnqhcdTAwMGa3lZ0621x1MDAwZZ4/Z/9Wup1UVsg/4YuSsrGgL1x1MDAxMlx1MDAxZkZcdTAwMTXSXHUwMDE2VJqc2rtKp7+RZfxlXla4XHUwMDAzjIO7lo6ZXHUwMDE33OcxYc+IXHUwMDE0KJhmwftcdTAwMWKesI9cdTAwMTY9YT8hc02csI+ed2eLLr6zhVx1MDAxOUzDVlx1MDAwZk9ccstcdTAwMDNSql5cdTAwMWNdRKWQ7ZQvvG+eOP9SKoBludtcdTAwMGXD9bpcdTAwMWKV669cdTAwMGZNpHNcdTAwMGUyPs24Xb+YWmJcdTAwMWJ7XHJoh1x1MDAxMW0pIVx1MDAxNUSl65TzhqZQXGbZruFcdTAwMDaZJSbr1PrsoVx1MDAxYq1cdMdcdTAwMTbI2URcXLdcdTAwMTlcdTAwMDUuoFx1MDAwZeTmXHUwMDE1bkmjT1x1MDAwMO7sPitk4UM+7O3wXHUwMDFhuez0qaTSb7tl3W5tXHUwMDA3VyGr1Fp998dOUSrJ+N2Uj1x1MDAwNHhcdTAwMTFvxcG3t0pcdTAwMWJOhcy6q33+XHUwMDA3XHUwMDExhkrQRKpcdTAwMTdcXPolXHUwMDE0cVx1MDAwMKhRQJhGRZ6TVzhx7FJoSbhBXHUwMDExh/pqhFcqXHUwMDAzXHUwMDAyMM+8wsKwubmrXHUwMDE3XHUwMDA0fifMd1ZerHNcYlx1MDAxN1ZdmelXJ1+VTndcci25n8pcdTAwMDErl1x1MDAwM6M/qkZvXHUwMDE2b11gbMWAhY5cbkQwpDU8VVa9VzlcdTAwMWElNXqytM8jXHUwMDAwMY9qTd5cdTAwMTIphyiD0TteXHUwMDFmQSCnVkPtgklbXHUwMDE2tus8XHUwMDE4KtBRXHUwMDA1JCVSXCLD2VxmJdk34ammcH5AKo5xRD1hXHUwMDEyvXlrbrauv3/8us+jq1x1MDAxYrfqXHUwMDFk7+1cdTAwMTXd47okjootlIP5X2AsI4qAyD5uRWEoI1xc2ck5JVKqeTZPvVwiRLyQp1x1MDAwMrpcdTAwMGXG2te4dW9cdTAwMTGOKkFcdTAwMTQ5KqPx04fQW6f21HOXlEo/tpplJk5O9XW9XFzz5e6yeyrHXHUwMDFjT1x1MDAxOUFMYlx1MDAwZeFcIsNcdTAwMDCEdOzDkYjUklx1MDAxMmDPm8ii7ErrvHvX5lx1MDAxMlNcdTAwMDHiqDpLXHUwMDA1fGlcXHX8o1x1MDAwM3Rx4UNrJGmM8enDqiq3dvfDm1x1MDAxZF1vymr3MvhQqvpmtsLH4vgqRUnlUKGZNMo+XHUwMDA3avg2S8GIozVFSYNcdTAwMTRcdTAwMDFkXHUwMDE2RfNjq9xcdTAwMTBHWSdcdTAwMTSA0VvTvFx0XHUwMDFi7iBcdTAwMTNcdTAwMDVqn+oh7TrQrL9yaiTmhtd43Fx1MDAwN1+Iu45Zxc9cdTAwMDE4t3Cd2lt31zuNnb1bV1x1MDAxZl/pfr9zXHUwMDFjnFx1MDAxZpr9ZVx1MDAwZq2KO0rZKVx1MDAxMeuvOlWHi52VY9RcdTAwMDP0JFx1MDAwZZpcdTAwMTIqnuWtXHUwMDBmRfmc0KpcdTAwMWNcdTAwMTR3qGONQHVl1ySPuirTjp2HQkuJoExcdTAwMTgyqqwk2FuDXHUwMDA0vEJsfbqzrjxcdTAwMTSp19xO5yzCLtdcdTAwMWXn9NBqv/JQ2Uu6Wbvxvf5GXHUwMDFls4pftiRcdTAwMTZcdTAwMDPAeplnbf75a+XXf1x1MDAwMfpa2G0ifQ== events.Key(key=\"T\")events.Key(key=\"e\")events.Key(key=\"x\")Message queueon_key(event)Event handlerevents.Key(key=\"t\")

When the on_key method returns, Textual will get the next event from the queue and repeat the process for the remaining keys. At some point the queue will be empty and the widget is said to be in an idle state.

Note

This example illustrates a point, but a typical app will be fast enough to have processed a key before the next event arrives. So it is unlikely you will have so many key events in the message queue.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1daXPbOLb93r9cIpX+8l5Vi1xy3Fx1MDAwYlxcXHUwMDAwUzX1yrvlxI5cdTAwMTfF25splyxRiy1LskSvXf3f50JxLGohJVlL5ExUldgmaVx1MDAxMCbP3Vx1MDAwZi7++u3Dh4/RUzP8+I9cdTAwMGZcdTAwMWbDx0K+Vi228lx1MDAwZlx1MDAxZv/wx+/DVrvaqPMp6Pzcbty1XG6dKytR1Gz/488/b/Kt6zBq1vKFMLivtu/ytXZ0V6w2gkLj5s9qXHUwMDE03rT/z/+/l79cdP/ZbNxcdTAwMTSjVtC9SSYsVqNG69u9wlp4XHUwMDEz1qM2j/7//POHXHUwMDBmf3X+j82uVq2HnWs7R7tzU6D7j+416p15XCKR4tOye0G1vc53isJcIp8t8WzD7lx1MDAxOX/oY+FY1pu721x1MDAwN42709rGTqV1+bTXyHTvWqrWakfRU+3bQ8hcdTAwMTcqd63YnNpRq3FcdTAwMWSeVItR5fszi1x1MDAxZH/9vWK+XeFcdLyebjXuypV62PZ/u3w92mjmXHUwMDBi1ejJXHUwMDFmXHUwMDEz4vVovl7uXGbSPfLIP2VcdTAwMDBFYFx1MDAxZFx1MDAxOaW1QjSWXk/7XHUwMDAxMnxSKyc0WWONUYawb25rjVx1MDAxYb9cYp7b76Lz6U7uMl+4LvNcZuvF12uiVr7ebuZb/Lq61z18/6tcdTAwMDOSioxUyqFcdTAwMDDFd3u9pFx1MDAxMlbLlci/XHUwMDE2XHUwMDBiXHUwMDAxSJJSW5RGQXe27bDzZow2xlxup8zrXHQ/hWa22IHHv/tcdTAwMWZsJd9qvjy/j52pxqbvf9yIYav7y3fNYv5cdTAwMWJcdTAwMTAkXHUwMDExgjZcdTAwMWFcdTAwMTGlfT3PeLvmk/W7Wq17rFG4XHUwMDFlgp12lG9Fq9V6sVov9/9KWC8mnKnl29Fa4+amXHUwMDFh8TT2XHUwMDFi1XrUf0Vn3JVWq/FQXHTzxSEjJ55r+uG6wuQ/3e8+dOHT+eH1+3//MfTq5HfqP1x1MDAwM2+zO9xv8a9//zGZXFzHxbZPrqU1XHUwMDEy/VvrXHUwMDAyZJRgXHUwMDE3XHUwMDFmt+uHNbm/cqLXilLKp1P8urX0gk1cdTAwMTiAQaeVllx1MDAwNqxm+e5cdTAwMTVsXHUwMDE5KFx1MDAwM1JoaYGUk6J/arOTa1xmLFx0x4IpXHUwMDE0KiMsdoW2K9fGXHUwMDA1pNFJLdFcYsDYbL7LtZaSQHfVzy+5/oFynfxOO2f73+aEct1cblx1MDAwYtE3VFx1MDAwZlx1MDAxMW5cdTAwMGKq/+h34TYgyFkruiBcdTAwMTkl23pcdTAwMTez6vhuf0tcdTAwMWWXXHUwMDBln45WN6urXHUwMDBme2+TbdmPwe+/126w0zJbmy1VQGCsklpb/qe74uJHXHUwMDAwZ1x1MDAwMudcdTAwMDRcdTAwMTFYh85p0zeziUT7d1WgsKSVXHUwMDFhYrCpK1x1MDAxYa+SbFx1MDAwN0TXXHUwMDE5XCJWMNrNXnRfcfVXXGZ9L6/2+jS7cnZcdTAwMWGFZ1x1MDAwZW9Xwp2zbPSw1vxcdTAwMTiH6SveovAx+vh64u8/foZhe67+Y9xcdTAwMWJ2h/0uqDNUjqky3zPPmLhcdTAwMWIjksRdXHUwMDFhUsiuKcmx5T39MS+tvFx1MDAwYpcm71xiJlCzkvd0XHUwMDE3XVx1MDAwZpF4wH6JZ/3EXHUwMDEzVSrmc4wt8m3/w6KtdalRj46qz/7Rg+g5upm/qdY6XHUwMDBm+fVwXHUwMDA3qT7mvfdzXG4+hU//c1x1MDAxZD79819cdTAwMWbDf3383/izbYdcdTAwMWRcdTAwMDeV5+d6fnmlVi3XOyEjXHUwMDBmXHUwMDEwtnpQXHUwMDFmVTnGfb3gplosxq1ggWeU5zFb2XGsV6NVLVfr+VoudcJvt8RSOpckmyDY9+T34cZcdTAwMGagm0BQyX6y95VTKp5+NjuGtnaWXzalXHUwMDBljJLkUKFjV1x1MDAwN/pssVCBs8ZZJVxmsG+ippHN6W0xsVtG1mo5uWBOY4tLXHUwMDE2Tyh7sEVCXHUwMDFlZTZcdTAwMGLbUT17tT290fwvXHUwMDFmdpSJXHUwMDFmfsPlM/FSxtI1/TZeXHUwMDE44jBC4vg+ffpzXl49XCJS9VxiqkDNSo/MxMZrhWTi7+W/wcQ/LtrEjzCKI03845QmXHUwMDFlY2++XzSVcFx1MDAxY+lrM777fWLv7k5LVXyqf3Lbd+XqMUVN/Vx1MDAwZURTXHUwMDA11pJBpYXSZGLa6ttcYi4wXHUwMDE2vOdttXFcdTAwMTLtNLI5vY2X6KQlqWBcdTAwMGU58DT7tpYt3lZutrPQus3kSoXyVrNcdTAwMDJcdTAwMGbTm81fw85j2FG+w/BcdTAwMWIuoe9cdTAwMDAmMVx1MDAxZFxiXHUwMDAyyLDcqq5uXHUwMDFlpaDSn/PSKijWQGlcblxuXVx1MDAwMDNTULNwXHUwMDFlkDg45Hm8ISX4jp2HaNHOw1xiczvSeYimdFx1MDAxZYSEJNmUvqpsSajx81x1MDAwM6JcXCg+fd543nzOrZ6a1np0YrYuXHUwMDEzZLPQarTbmUo+KlQmrsXNXFw+Ob5cdKRcdTAwMDGSTrGEOid7xVNcdTAwMDdWXHUwMDBiw+60clx1MDAwNlx1MDAwNcr5pe/Yq1xiWFx1MDAxMtBnTaWMXHUwMDE1Sbt1OFx1MDAxMoFlh0YpQuzUXHUwMDA2+0WXWMU4K/Fcclx1MDAxOYRpRJescjSB6L5cdTAwMWS1XHUwMDA2XHUwMDEyXHUwMDEzzmA58lwimKB2/PDl7Ph+t+lobbN+vXffzl2VaDdcdTAwMDGzfbj7cWjFwFxuzViUgpW0xD6wXHUwMDAyaTYhPi5nXHUwMDE42XmCXHUwMDE1XHUwMDAzIUn74rU0zlxmMyw2IFx1MDAxMqhYj1x1MDAwMCFcdTAwMDNlXHUwMDAwrdJcIlnygcpyW5pUuIa1WrXZXHUwMDFlXG5W0olMXHUwMDA3p4W39+P7Puu3bq0sXHUwMDFh1fUoV6GV+pdqWTxfvVx1MDAwNauL83wk6MBcYumENlx1MDAwNlx1MDAxND952Vx1MDAwM1apXHUwMDAzjUaQY8xqXHUwMDA2K01Fcvi9lNegYVx1MDAxMKksMEpcdTAwMDBoR+xxgoonZ16hKiHQRMqxUlx1MDAxNYBcYjFcdTAwMTftO1RcdTAwMWRJY1x1MDAxNDtoPydUjUgsXHUwMDE2+EJcdTAwMDJpJeT4Sb6TxtZOY2vvYHf/5jh7lFx1MDAxN7lneX605GDV4K1vJzRX6ND2eekqQKk1Y4RcdTAwMDVcdTAwMTdcZsXM3dvAeimEnlx1MDAxYljRSkOCZe8nXHUwMDA1KyWnvdjYgEOYgGWSoULrRu+F1fPTs8v80VWrjodPS1x1MDAwZVYrXHUwMDAy0pbh4Yz3XHUwMDA2bS9WkVxyL1tV7SyHcYyT6VJeXHUwMDEyLtmvmlx1MDAxN1ZZliSHxPie9Wqqx0o6UbNqUFx1MDAxMlmnjFx1MDAxZmWd5MzVYSV/U9k938k837ab1y6iXHUwMDA0rL6V7ThztIKgXHUwMDAwpFJAXHUwMDA2vVbqJTFLclx1MDAwMTgvs6y1PMV5jlxcRyNcdTAwMDPHYZZX4GRUTD2+XHUwMDAyVstcdTAwMDDNNzWPXG6MjnHQvytX45xcdTAwMDWcS23l5cTQJOP9cVbVVm+fNr82M/fb59tfT7ZKZ/NMMlx1MDAwZb9hd9iX7354klx1MDAxMZO9bClYN1x1MDAxMlx1MDAxODF+TJj+mJc0x1xiUqZJmKFAWWG0L1xisveC81x1MDAwYlx1MDAwYpFGSlx1MDAxOFx1MDAwZZhcdTAwMDBcdTAwMDaLs07rd8hH8sszXHUwMDE4Y2jIXHUwMDAya1x1MDAwYqN7LkpJP+aGplx1MDAxYVH0XFz4mkmshaVe6E+WaEw3XHUwMDFh/YnG3Fx1MDAxNElFJWWSLJJcdTAwMDEhrFx1MDAxNuOHvMeZ6PK2urlXOz0pn12VVk4zN/nscjtmLFuBJasth6LaOWN7ef3SYoDs7CiUgFx1MDAxY/xOtVxc56VcdTAwMTY5xDEzgWGnT7BMKVx1MDAxZrfi8OSMXHUwMDEywnFIKzTH6U5cZjpmyidtOEpZbqlMxWryXHUwMDEylORo11x1MDAxYZRKqFx0XHUwMDAyiIddsV68P195PG89Xrebh1FUeWjN2Cmby9oy1tVWO4UgjZZcdTAwMWPi92BVkVx1MDAwZZBDXGb5Ld6FfiFasqVlUmhfTsS3xLy/1qB0zv1cdTAwMDRryyhZsI2w1lx1MDAxOFx1MDAwYuPzYS42L53UT7BfrFx1MDAxZcv1c7neLlx1MDAxZp4svVxcXHUwMDEz+Fx1MDAxNaPgWIU5xyFMb9LVSzU/dDZR5Dx+xfyoalx1MDAxOEjl2CfloE5rXHUwMDAwXHUwMDE4YoX8UiQgskpY9lx1MDAxNy1cckg1O1fOL2Waw+qUX0LtP5MuLEt4pZ2T/S9zQplcdTAwMWVBdVOJnqVcdTAwMTRoXHUwMDA1mHh9YZRor+7fXVx1MDAxY8r26flDTWy3rlx1MDAwZlx0sVx1MDAwMctcdTAwMWXlZVhjXHUwMDA2noPKVpmkYC+lN84z2lx1MDAwNb6K7UChXHUwMDE22s3Cu5yG6SZYs/ty5IKZboXWYeZi5XRtu1x1MDAwNNW7jWN+zc/H+9OTvH5ccjuPYUcloYbfsDvsd82yqIhcIpnpZm3/4S6bXHUwMDA2lNPG2lx02lWkPufl1U8yVT9ZXHUwMDFi0Kz00yyIbpI9VH415i39KH505uk90eRHWNu50+S1SF7B4pRcdTAwMTakXHUwMDEwx09LRe58y17WWpXN58+5q0+5i82tKImJsUyyyY4/kSSQVkiUvYkpPlx1MDAxMXSWt4DlmExN10dmetfBXGKNPnhcXLDnXHUwMDEwfn740l5dfb4o7aysRcdcdTAwMWKHub3bcHqj+WvYdzTsKIdk+Fxyl9AhUTpR6ZGViJMszE9/ysur8kSaynNcInCzUnkz8Ua01Vx1MDAwNMLJJc+4z9hcdTAwMWJZOO9+hP2eO+9cdTAwMWUokXfv0Fx1MDAxOHJajC+ZT1dP5d3jXG6gK2Tz+0dcctkw2fz7oN3rXHUwMDAwkP1cdTAwMDF2v1x1MDAxZItnf57SYEBWKOUkkmDJmF+eUmtcdTAwMTlcYoNgXHUwMDEwWVx1MDAxM0DM6YhcdTAwMTHvNV9iXHUwMDFjkTZcdTAwMWPokVx1MDAxOFJ+UKCs027BoYRcdTAwMDGn1Fx1MDAwNML7dty6WGewfthKUlx1MDAxYeI07pFd29a/NL9cdTAwMWWph3ZOZa5lTX6+XFy/uFh25r3HXHUwMDAwaWLAcrTvpO1cdTAwMDMsXHUwMDA0YKywXGZcdTAwMDGNen7VMq1cXKDQovJcdTAwMGLGKL6ONF7cdc5cdTAwMDFbXHUwMDEyy3hGUv1oRY7OeziSXHUwMDBiwapcdTAwMDJQM1slkkJcdTAwMGa1NpFcdTAwMWVcboJDPclcbmV8XHL7aaWZrZePi5vnj7uVtdrGp690t+QsXHUwMDA0NjGBdVx1MDAxNlx1MDAwMY10IJXp5TIri1x1MDAwMVx1MDAwZqJQa4NcdTAwMWNo4bRcXOZE4r0mzzpSgr84I4d0XHUwMDE2lFx1MDAxMFx1MDAxOKklq1/tXHUwMDFjuXjrk1dcdTAwMWFcdTAwMDKAdKhcdTAwMTesV1x1MDAxN4VVh8mpXHRcdTAwMTZRYmtcdTAwMDPjpybOro4q2Vx1MDAxM1co1Vx1MDAwYitcdTAwMDeZ3U27+1x1MDAxNZeed69cdTAwMDKPUCNAsONDivqwKlx1MDAwM9ZyymotSKrp8lx1MDAxMims+1x1MDAxOSCVpc5cdTAwMTH/XHUwMDE1uGD/fWFQdYlcdTAwMTU4VEJIXHUwMDAxMD7N8su5OVxcKa1cdTAwMWPU90pfi632Sq7cLN4tOVItXHUwMDA0WmhALaxfx1xyXHUwMDAzQFx1MDAwNb8kz1x1MDAxYlxcS1O2Yk4j3c9cdTAwMDCqRiqrLL2BKrM0QFx1MDAxZOGrJrZcdTAwMWSQmpBgolx1MDAxOOvgYCVcdTAwMTNlbr/uPt6b/U+nlXpNbL+xXHLpXCJJ9zbo8Pek0X49f1/HXCJCXHUwMDFikFGsblx1MDAxOcxg5Pz8VTRcdTAwMTSwl+GZhOBLwkNcdTAwMDCrPVx1MDAxMY1naDtsIVx1MDAxMFx1MDAwM/6qNJZxTKRcdTAwMTbMub+9P6jIkthcdTAwMTD7N7vV0+yXyu7Jp+5b6sHjJEnLmVx1MDAwZjsqaTn8ht1hX75cdTAwMWIlvHNPWpJJ5mVcdTAwMDJcdTAwMTkt0NnxbUz6Y17SrCWbj1S5VTogXCIgwYrMoJnjYlx1MDAxOVx1MDAxYSm38VYlL4LK8bFmV1xyXHUwMDE3767Hqkhv5PLLQFx1MDAxYcdxXHUwMDExsNUkNp+q56o0Mn9cdTAwMTh/zHNm84+wRlx1MDAwM2z+cIpcXKWRid1cdTAwMTaMXHUwMDA1/uBcdTAwMDTiWL86XHUwMDE3XHUwMDBlz8zuZeuomTvcKOWz0cZyu3ygfdHU8lx1MDAxN5IgXHUwMDA0qD5Z9JlcdTAwMThAJdnP8vHJVFx1MDAxZV9cbptcdTAwMWZcYtHPQrGDXHUwMDE505DxfI/hXHUwMDBiNMuqRf5ucNFcdTAwMWFcYt/01yyc6eCthptALlOhmsjmd4lcdTAwMGLAQLKDXHUwMDAziGp8mJrjzY2mOP1aKT2stHdcdTAwMTGuXHUwMDFmSup+6Vm/nZ1iiP/zvTbQ8S/0dlx1MDAwNnHCXHUwMDA1gFKC8ett3Vx1MDAxYzeUmFxym99cdTAwMGbgJP5cIv4uXHUwMDA38fdcdTAwMDex+V1ipYyjXG5jhJ2gJ8Xh5Vx1MDAxNT5cItjbp88n7UyYscfl+vrSyzVh4Fh/WVx1MDAwZehZtEH0eoNeqn3mRYBcdTAwMDZcdTAwMTNfXHUwMDA3O3suv7K+14+z7Fx1MDAxNzlcdTAwMTVPxCWS+c1ghVtpYJy8qS/FL6nunJsxnT/prXbOXHUwMDBlvM9cdMU6vVxmTsmdvKTfzcw5sFx1MDAxM3ScuXiWpcszudW42C192s/WcpWd99G7Vvt9kzjqY1x1MDAwZlxydN/CbemMX4PnwDdQ0qzyZuBhTkHLXHUwMDAz4djnXHUwMDA1WjAtb1/kLteal+p0o3L7eFx1MDAxODZcdTAwMGJPutTtstxcdTAwMDO5SbIwv4b9NeyoxNnwXHUwMDFidof9rlx1MDAwN2dofFJValwi209TYubMt4OUksZffJD+lJdWl7IvlKZLwVx1MDAxN8VnpUtnwvdcdTAwMTNcdTAwMWOzcyxml7xLV/fFv0++31xi12BcdTAwMDF8v+RilFG+i9dcdTAwMDR+XHUwMDBlbd5dXHUwMDE3zvbWqfglX4n29s/3dlx1MDAxM1uWLlx1MDAxN99cdTAwMGZE4FxmXGKjhe9MKmNdsTqRjPeE/Fx1MDAxZbdW+35mMD82rla+RYe0VnC8KmxM+mJ0P1xiXHUwMDFjz5M0XHREgYP1KIlcbv062Fx1MDAwNYuu8f06ZmVL0iuoyV3LJOtQ313WjV9B3dws2Mdy9sneX8rdtdXSefnpxC053Vx1MDAwZnyvZd+e1oKxMr4v6je8YmB8/zZcdTAwMGXBnW9cZjlHvFJcdTAwMDDI01CgXHUwMDFjuWHL6MFcdTAwMDZcdTAwMTZcdTAwMDU4w+FcdTAwMWNcdTAwMDdxclx1MDAxMK+ao1xiJFx0i6amWICZ4TWNmpKywMuT01x1MDAxY6hcdJrsrWx9Pmt8LbSL5nzncdvIjcuzXFz0XHUwMDE2tC6Qm4K+0Vwi61dcdTAwMDPot1x1MDAxMOjrXsp/f1x1MDAwMM5cbuf33o5b/1k32mV/XHUwMDA2pee/alx1MDAxYq9cdTAwMDHGqSmkpG88pFl76njm/rUntFwiYlxys3By6qKgSoksKraBrN7dXHUwMDA0u9qf4Vnt071dWa3uX1x1MDAxZdRcdTAwMWWbd4XjZpKnvixI1a7TXHUwMDE23PBLVtag6stoXHUwMDAyXHUwMDA2orPNXHUwMDE2P1x0RtN0O/KltdmdXHUwMDAxVFx02Vwiszgtmke1IKhKkbJTu2CsXHUwMDAyXHUwMDFim/FcdTAwMGJra7dnj2H2aKNw0FxcvTtcbqN2e+UmKVx1MDAwMb8sYLXsK7K2s8Yho9U521x1MDAwN1ZcYpRU7Eii9WCdikSV2md3XHUwMDA2YDVglVx1MDAxNvGC/ftcdTAwMDNrusuarFn5PVx1MDAwMitcdTAwMWRN41x1MDAwN1rR7v6FuSiXTu7yrvX5oZy5XFw3XHUwMDA3S0/665CHtGVb70j5/d178CqFsYHjqMY3aFx1MDAxMHK69oMjSH8qkMR4XHUwMDA08MyJYYtUtPRcdTAwMGK8wCm/XHUwMDE3sVSDi1RYu0jPXHUwMDFmgDm0hn45MTTJuHef39v7slx1MDAwZke3mYeHwu1D+2v1tjF97vK9XGY7KiU6/IbdYV++XHUwMDFipVx1MDAxM3BWOiFxa3KV2JBcdTAwMDVcdTAwMTjbXHUwMDFjSdP4XHUwMDBiK9Kf8pKmREHadG1gtd9Lxlx1MDAwN4ZcdTAwMWHQzrNcdTAwMTVcdTAwMWONVFx1MDAwN8O4hFx1MDAxY15r86adXHUwMDEyp7NYXHUwMDE4S5a/uS+wY1fW51x1MDAwZXhAXHUwMDE3V7UjuYSPXHUwMDBiJFx1MDAxM46wcoNkwsdp2IRp7F70q7xgXHUwMDAyPkflrPJ8eYqVK5cp5FpHn3dL9XJ5ud1JXHUwMDA2XHUwMDAzXHUwMDA3N563Yp3xLYJVv0D6XHUwMDEyhjWecKuJY/qp/MlcdTAwMTRCIYuWIa2VUsbK2HL0eEZJd1x1MDAxYTmBYdUh41x1MDAxYkl9zyhJQyB/SOwzs/XOibyj5Iy95qjUM9HHhmmzoj+3r0ru6GrroJJTh3hw/by29LSjXGaA8cuZlFx1MDAxM35xiO03XHUwMDFk6L03v+LL75dn58c7mlx0mZA63YTe5EdOyzqahPtcdTAwMWGbx0/NOvoxXFxCflx1MDAxZonuIN/MeUU3vlRHVysnbnPzZrN+9OX86Gz1dOP8aHXppdrIQHg2l187KGz/JvZcdTAwMWShdlxuXHRcdTAwMWRcdTAwMTA//DluxFwi/G5cdTAwMTVcdTAwMWOFalwi8LtFqGE2XGI5WGXH1SpU7Fx1MDAxYmhcdTAwMWOQa09qd7pni/Nfgv1cdTAwMDNcdTAwMDU7k/Ja/WfghU4o2iPK7GmbLfL9/Fx1MDAxMo2x5Xun0ag8rjdk5n5/7aZyer5xXHUwMDFh3W++jzK7XHRYvK1Fllx1MDAwZYfxXHUwMDFkKF9k3O/J6Jv+k1x1MDAxMobN99xk3PcpXHUwMDAxLYxfy2ZcdTAwMDTqYYxhwsDybKzwsZ52XHUwMDAzIaBf1sUgolx1MDAwNW9cdTAwMGVmJbpJXCLAt6PWqcSQXGL4Vfq93cavsl9X7E21JU736qtHxbtnVXu8v3xTgn2xcFx1MDAwNe3/Ut3pKY5cdTAwMDMmSTFcXK3SSrJTXHUwMDAwen6kLU9cdTAwMGLhgFx1MDAwNpTPXHUwMDEzW6GG0EJ8W1x1MDAxZOFcdTAwMTkqwneCwngg8lx1MDAxYVx1MDAxNfl9j1x1MDAwNIpcdTAwMDWnLCa1SKmATeus41x1MDAxMuvsfomCJ++MX1x1MDAwZrpoP118udtsVWBlz62V98rm4K603Fx1MDAwMTzrhYBcdTAwMTHg9SqrWCn7o1wi0lx1MDAwMVjR2fC4t0nYjNvqsL8qXHUwMDFjKE/NxyHrXHUwMDAxfTXIoNDaoensXHQ2WFxyQmfYu47npX4qoDqdXFxcdTAwMGLyPVx1MDAxNk18O4GRXHUwMDFkoGzt881u5ejE1Y+b+TD6VMmJJc808fNcdTAwMGV8Po3YzKPyPn0/Ulx1MDAxNYdcdTAwMDLWOWPQTFu4TGurMz1SwTdcdTAwMDNmQ/iTXCJVxnfY6ocqR8BW9qzaXHUwMDFkzbTbyVx1MDAxZEa3ollcXL9ccq9BrV/Jh+aSY9WpQGnyvo6RxJI7oFUx4JDUNyxjZ9HAdGo1rbHO1GBcdTAwMDVgpLr4XHUwMDFlQ+9cdTAwMGarI2ihKVxcZn5cIuArumOD9TjKStrP5chd01x1MDAxOYX526eVqkhcdTAwMDDrXHUwMDEy1dhd4DpcdTAwMTU1wSGnjL3rb5vag1xiUDFI+Jx05KZSrSNq7DqQ7GyA51OzN+KGRFhaXHUwMDA27KZ4rq5veyjt4EJr3+uCT8/DY305MbRsfV3BXHUwMDFjfcpcdTAwMWTa7fVjzK5uhZtccnszfTX8v3zYUbX74TfsXHUwMDBl+/LdwnRNYlx1MDAxZiCZzOf1IZiRQo+vZtJcdTAwMWbzslx1MDAxNu9BpatcdTAwMTlcdTAwMGWcXHUwMDE1XHUwMDA3rEJcdTAwMTjfXHUwMDFicY5qhkarmSHFe3ROk1Y/YvvQN+Vme1x1MDAxYlx1MDAwMVx0tvNGKfZcdTAwMDaI/1xi6rkqvXhcdTAwMWbFn/Scq/cj7OeQ6n00RfneQvJcdTAwMGVHXHUwMDE2pfKZj/EzrNnzfFttnn2l/f1wd0OYnZvPoV1uR1x1MDAxNYVcbsjvU+1cdTAwMTe/KG1s31xuQ41s+i1pY3zLrFx1MDAxZVx1MDAwZeaM+1x1MDAwMVx0XHUwMDAyo3zlnuLNe+OJKl+0l4o/woe6XHUwMDAzjqpB1Fx1MDAxNlx1MDAxMN5FUPXbi0X6mG82j1wiXHUwMDFlkk9/gy7Pulp8kdruMFx1MDAxZu+r4cPqsEC18/FcItBcdTAwMTFcdTAwMDBcdTAwMGay0M/5r79/+/s/t9XlXHUwMDAwIn0= events.Key(key=\"e\")events.Key(key=\"x\")events.Key(key=\"t\")Tevents.Key(key=\"x\")events.Key(key=\"t\")Teevents.Key(key=\"t\")TexText"},{"location":"guide/events/#default-behaviors","title":"Default behaviors","text":"

You may be familiar with Python's super function to call a function defined in a base class. You will not have to use this in event handlers as Textual will automatically call handler methods defined in a widget's base class(es).

For instance, let's say we are building the classic game of Pong and we have written a Paddle widget which extends Static. When a Key event arrives, Textual calls Paddle.on_key (to respond to Left and Right keys), then Static.on_key, and finally Widget.on_key.

"},{"location":"guide/events/#preventing-default-behaviors","title":"Preventing default behaviors","text":"

If you don't want this behavior you can call prevent_default() on the event object. This tells Textual not to call any more handlers on base classes.

Warning

You won't need prevent_default very often. Be sure to know what your base classes do before calling it, or you risk disabling some core features builtin to Textual.

"},{"location":"guide/events/#bubbling","title":"Bubbling","text":"

Messages have a bubble attribute. If this is set to True then events will be sent to a widget's parent after processing. Input events typically bubble so that a widget will have the opportunity to respond to input events if they aren't handled by their children.

The following diagram shows an (abbreviated) DOM for a UI with a container and two buttons. With the \"No\" button focused, it will receive the key event first.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1bbVPa2lx1MDAxNv7ur3C4X3pnarrfX85M54yioFSxVk+tPZ5xYlx1MDAxMiElJDRcdCDt9L/fXHUwMDE1UFx1MDAxMt5cdTAwMDIqcHBu80Eh2eysvfZ6nv2slZ2fW9vbhbjXclxuf2xcdTAwMTece8v0XFw7NLuFt8n5jlx1MDAxM0Zu4MMl0v9cdTAwMWVcdTAwMDXt0Oq3rMdxK/rj3bumXHUwMDE5Npy45ZmWY3TcqG16Udy23cCwguY7N3aa0Z/J36rZdN63gqZcdTAwMWSHRnqTXHUwMDFkx3bjIFx1MDAxY9zL8Zym48dcdTAwMTH0/jd8397+2f+bsc52zWbg2/3m/Vx1MDAwYql5nODxs9XA75uKOUKcMCT1sIVcdTAwMWLtw91ix4bLd2Cxk15JTlx1MDAxNXrRt6vuXHUwMDE34pu8yI/l/snt58reSXrbO9fzzuOeN3CEadXboZNejeIwaDiXrlx1MDAxZNeTu4+dXHUwMDFm/i5cbsBcdTAwMDfpr8KgXav7Tlx1MDAxNI38JmiZllx1MDAxYveSc1xiXHLPmn6t30d65j6ZIcVcciQ0JZprLDVVw6vJ77UhMeJcdTAwMTQrjSTmmo3bVVxmPJhcYrDrP6h/pJbdmlajXHUwMDA25vl22kZcdTAwMTFL48yYu4+jVdRgjFx1MDAxMlwiwVxmqlx1MDAxMeHDJnXHrdXjpFxyIYZCTCjJXHUwMDA3t8qY4vSnhGtFqVx1MDAxNul8JbdvXHUwMDFk2f3Q+GfcoXUzbD04rlx1MDAxMCVfMqYnVlx1MDAxZozHVTa2MpNOxFWl7Fx1MDAxZlQ+fyzvX1rfj3ul8/tw2NdIIMbOfVxcXHUwMDE4Xvj19ne3M7tcdTAwMWRp/XbRXHUwMDFiLmituL+6b1x1MDAxZvPDkPpnJdwrnXlfet3p1pphXHUwMDE4dDP9PnxKo6ndss1cdTAwMDEjYCEoSyiDS8SG1z3Xb8BFv+156bnAaqQkspUxeIK7RsafIS5G0fjZR+JcIohSXHUwMDA0WOApiOZcdTAwMTFX/vRtKnFplENcXFx1MDAwMlx1MDAxOVx1MDAwMlx1MDAxMyBcdTAwMGLBXHUwMDA2zPVcdTAwMTLiikPTj1pmXGJ8MIW85HzyXCJcdTAwMTNkRVx1MDAxMJFMJ3S2fLrKj05O5Vx1MDAxM6IzXHKCwI/P3Vx1MDAxZv2lUVx1MDAxOFx1MDAxYyuGiIBBaI24XHUwMDFjaVUym67XXHUwMDFimdd+XHUwMDE4g+W7rdab/2ZdXHUwMDFkOWDCYLlcdTAwMWRpvOu5tSTOXHUwMDBiXHUwMDE2XGbKXHRHIFx1MDAxMLsgXHUwMDA0hlxymq5te5lwtMBcdTAwMDJcdTAwMTP6XGaPXHUwMDE2WZOD0K25vuldjFx1MDAxOJhcdTAwMGLJXHUwMDAxJUzBpFwiszGJwfGYscyiNVx1MDAwZpP5JLWhmKRSXHUwMDFhhDIsZKJcdTAwMTVENjL6XHUwMDFkMGJwwCsgUilBJScrQyU1XHUwMDE0cCSTQlxugShXelxuKqk2XHUwMDE0xVopLjSGcJ5cdTAwMDAp5lxcwigofjpG+6Y+XHUwMDFio09bQTJ2mGG85/q269fgYrr0ParkRTDRR7HVTqzcXHUwMDAxhlx1MDAwNYBzjlxiU8BcdTAwMWNEZFx1MDAxYdXMVlx1MDAxMvRcdTAwMDZcdTAwMTeKSFx1MDAwNbHNlCZYPDRcdTAwMTiuwFx1MDAwNce355tUvPlcdTAwMDbS8NI8s8t2T/rt3a87++VZJjGsMdZIYyqJXHUwMDEyRLFcdKswg+mHmcNEXHUwMDEzglx1MDAwNPydMMszo7hcdTAwMTg0m25cZs7/XHUwMDE4uH487uS+N3dcdTAwMTO011x1MDAxZNNcdTAwMWW/XG7Dyl5cdTAwMWKnhVbS46h4TD9tp7Dpf1x1MDAxOX7+5+3U1juzozk5JuI47W8r+39cdTAwMTajhY5cdTAwMTVcdTAwMGbwPIXVXGK4eFx1MDAxNq1cdTAwMDGSXHRcdTAwMDaxsbjSyJ/nXHJlNaKUQSVcdTAwMTJIJ1x1MDAwYp5iZIzVNJBcdTAwMWWiSFx1MDAwM2q1TtbF1WlccpHOxZDGVKp8XHUwMDFleFx1MDAwYphVipHVZi2pkGp8iM5kpV5FOLBkLL+dWl9PXp5cXHjNXHUwMDFmn2/+wlx1MDAxZr2DYlxye3Xkur1ytJhcXM/t97LyudM9ZvsnxyG3yz3SJmxfLKFfcmlcdTAwMWZcdTAwMWSWXHUwMDFh1onaZfii6Z1cdTAwMWX4X2tL6HdF7n1d3TbE5VGn1PqEb9pRvdHhJXRXtf/vnPuCXGZ2vebOy+On33BBa0vlxoUon3+7u6yfdSp+3Tv+XHUwMDE27CzBXHUwMDBi91fki/zavjmpoHL5XHUwMDE2k2bN6Z0tqT7AJFx1MDAxMZCOrro+QIhW46dcdTAwMWZXbYq51ELRxXOR/LDY1FVb07xVXHUwMDFiUjJDrmnV5lNW7Uxq9LBqKyk4XHUwMDE4K+g6K1x1MDAwMkxohTR6QjxOr1xiLFpcdTAwMDEoPqbnb6795IJrv79OKvReULsuXFz704tcdTAwMDOZPHGkOOA5d6PR/6TSwFx1MDAxYy06Xlx1MDAxYZhr+fM1NkWKzkIrRlxcI0GzYTFcdTAwMGaunVx1MDAxYoJlLf7c2bPo0W3ZP2qflXv/Llxc+Ty0YkFcZqIxpDmSUsGoXHUwMDFhRSucMrhmSlx1MDAwMVx1MDAxMFx1MDAwNWFMrlx1MDAwZaxcdTAwMTkwpFx1MDAxMluMg5VgjDBkY5lq31o09sVheFOMnfaN3K1Wrq7qny5+XHUwMDFjfvitsZelsVfk3tfV7ao09uvywqo09uvygqf3iuq2XFxkRSGc84tcdTAwMGb3tVJpg72w/JRgXlx1MDAwNjN9IGm3XHUwMDBmn3JcdTAwMTSYwpqwpyiwXFyhMSsjoCSjcMc1XHUwMDA2wVRRgejiXHUwMDFhI3/+NlVjyHyNodelMdRcdTAwMTSNISc0htKYScroXG52NMxcdTAwMGJH/IRwfFlCsNeO48B/k5xcdTAwMWLo6uvClVx1MDAxM11cdTAwMTfeXHUwMDBlvnXM0DX9XHUwMDE4pHbUtixcdTAwMTjd7CxBjna+pCxhjphcdTAwMWXPXHUwMDEynjecXFxE56dcdTAwMGV49lNHjFx1MDAwNYQ8hPvimf7ljYjrxdZt1W+ffqrY9ztcdTAwMDeXZ/VNz/SpVlx1MDAwNoaoXHUwMDE1QkHSz4SczFx1MDAxZJCAbjjTjJOV7lx1MDAwNVgsecCISkQ5ZWtOXHUwMDFl0MfK4Un7qnL48axcdTAwMTL39lx1MDAwZXo8OPF/J1x1MDAwZstKXHUwMDFlVuTe19XtqpKH1+WFVSVcdTAwMGavy1x1MDAwYqtKXHUwMDFlXpdcdTAwMTde8DjhmTnJ9IGk3T58yntKwUEkp09cdTAwMTBWlpPgmTlcdFGCXHUwMDEzQcjie1x1MDAwYvKnb0O1XHUwMDBiQzRfu+i1aZdcdTAwMDWTXHUwMDEypSmiXHUwMDAyr0C6LDNcdTAwMWWXnpRUgylcIt5cdTAwMDG8huvOSOZo9Fx1MDAwNTKSeWPJXHUwMDA188z9j1jJmc9cdTAwMWMx0kJLxjPhPVx1MDAwZs75tLmhcKaKXHUwMDE5QiCtklx1MDAwMoLi2YpK/6Gjklx1MDAwNlx1MDAxNZJizYHeOFErrDFgXGaWXGJGXHUwMDE4p5RQwtkkulx1MDAwNTe45lopRGjyglx1MDAwN1x1MDAxZFx1MDAwNztnyXNi8ZyNRM/fXHUwMDAw+Vx1MDAwMrAvuFx1MDAwMXLh3YbIYJgqrFx1MDAwNeVIJvOVaTPYaUhcZlxmPoZlilxuLlx1MDAxNOTakztcclx1MDAxN9pcdTAwMDCZXHUwMDBm6lx1MDAxMZO4XHUwMDEwyVx1MDAwZUBcdTAwMDSrXHUwMDA1pJFcbk/YXHUwMDA0M4+0XHUwMDA0vIE9SMBcZuNcdJte0+7H2ZGcXHUwMDFjXHUwMDEzMZx2t5X9/1xmOpstTmBcdTAwMDKoktn8fVx1MDAxZZvlXHUwMDE3pjeVzSQxINDAXHUwMDEzXGYpxHXqj1x1MDAwMZkpQzCMOaeKXHUwMDBi/qJXw+ZQXHUwMDE5NVx1MDAxOMijxFx1MDAwNM4kKKUpVEZcZlx1MDAwMcJcdTAwMDR0iVJcdTAwMTIxltnv/Vh0oUpcYs1AvKyXzJgmXHUwMDE5sfQvktlcdTAwMGVQXHUwMDA3lVx1MDAwMCTMQEpKJsgkdYCjKbhZUVxuLCM0aM7n0Vl+1XTMqMQmXHUwMDA07UEkSEz0hFEw/UQgcCeiXHUwMDE45CdWr5rOdmaHc3JMXHUwMDA28lx1MDAxM1x0LbdcXCwzr7OOcVx1MDAxYeeYwaLCXHUwMDE3L1x1MDAxNvNcdTAwMWatfetL7XDvwlx1MDAxM7FC3S46vmttOqdcdEZcckWJgHDjXHUwMDEyKzZZK9ZUKFhQYGXNvlT2nPddmSWcO87YJKVcdTAwMTGR9pxcdTAwMTaKM1vQXHUwMDFlOEtCqqVhTp7xXGJoXHUwMDFlZ1xyw2pKyaLt69L9j6/V71cq+u53j3ixcVJ9eSWkKE87ptM+/G7+1Y0vT2k1qsYzXHUwMDFl+i6pXHUwMDEyMn0gabePiJrN36BcdTAwMDKUeMrjsFxccM6qhEiSkzrBIVx1MDAwNVx1MDAxNotcdTAwMDMzf/o2XHUwMDE2mCpcdTAwMGaYmsHStCRg5qpccsKnQJNMpEZcdTAwMThcdTAwMDNXcs7Uup/OPjFcdTAwMWPTWU9cdTAwMGIhmUeGI4WQdJCPhVx1MDAxMKeT2GR8cHpvXHUwMDFhTu/9deHiujDjXHUwMDA1Tj3y46W9wDlnkVx1MDAxOa92TDd4gMmtXHUwMDA3oFx1MDAxN8xW6zxcdTAwMDa/XHUwMDBlXHUwMDA1XGZMnWs/OCf1ZaHjOt29Kbx+1z+SXvs4T1x1MDAwMOUkXHUwMDEz9/PX1q//XHUwMDAxXHUwMDA3vCMgIn0= App()Container( id=\"dialog\")Button( \"Yes\", variant=\"success\")Button( \"No\", variant=\"error\")events.Key(key=\"T\")

After Textual calls Button.on_key the event bubbles to the button's parent and will call Container.on_key (if it exists).

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXG1T4spcdTAwMTL+7q+wOF/2Vq3ZeX85VVu3XHUwMDE01PXdVVddr6e2YohcdTAwMTBcdFx0Jlx1MDAwMcSt/e+ngyxJeFx0iODi3U1ZXHUwMDAymaHT09P9zNOdXHTfV1ZXXHUwMDBiUadhXHUwMDE3/l4t2Fx1MDAwZpbpOuXAbFx1MDAxN97H51t2XHUwMDEwOr5cdTAwMDdNpPs59JuB1e1ZjaJG+PeHXHUwMDBmdTOo2VHDNS3baDlh03TDqFl2fMPy61x1MDAxZpzIrof/jf9cdTAwMWaadftjw6+Xo8BILrJml53IXHUwMDBmnq5lu3bd9qJcdTAwMTCk/1x1MDAwZj6vrn7v/k9pV3bMuu+Vu927XHKJekKiwbOHvtdVVWpMJNOc9js4YVx0Llx1MDAxNtllaL1cdTAwMDWF7aQlPlVwS+dccr0vNre3qvdRdHrepHf1teSqt47rnkZcdTAwMWT3yVx1MDAwZaZVbVx1MDAwNnbSXHUwMDFhRoFfsy+cclSFdjxwvv+90Fx1MDAwN1x1MDAxMyTfXG78ZqXq2WGY+Y7fMC0n6sTnUDI+06t0ZSRnXHUwMDFl4lx1MDAxZVx1MDAxOFx1MDAxYlgozjiTQlLBkvF2es1cXCiKNSWSQC86oFnRd2EmQLO/UPdIdLsxrVpcdTAwMDVcdTAwMTT0yklcdTAwMWZFLI1To27/XHUwMDFjr6JcdTAwMDZjlFx1MDAxMEmJplx1MDAxYVx1MDAxMd7vUrWdSjWK+1x1MDAxMGIoxISS/OlSKSPZ3UlRQlDCiEh0jK/f2Cl3neOfQZtWzaDRs10hjD+kdI/V3lx1MDAxY/SstHel5t1sX3ok2tza31x1MDAxMZfhiSutVl3gvqyMK0b2Q1ToN/x4nyf24XBcdTAwMTe7Zpl9uSi192osujQv9uhosWZcdTAwMTD47WnlLkjd31xcbKb3+2kvmIjtvUuctNkom09Yg8GtXHUwMDE54kRwOPrtruPVoNFrum5yzrdqXHQ8raT0XHUwMDFkXHUwMDAyxYyeKUTkaixcIjKlkYbwTHSYhIj5Vl5aRFx1MDAxNLmIKIjBJII5QfLliFx1MDAxOFx1MDAwNaZcdTAwMTc2zFx1MDAwMHBmXHUwMDA0KsrJqEiGUJAqzVxilkLPXHUwMDFmXHUwMDA15+mdiVx1MDAxN/hedOo82l1ZXHUwMDA2x4ohXCJcdTAwMTCRWiMuM722zLrjdjJcdTAwMTPbdWPQfL3RePeftKVDXHUwMDFiVOjK5JnO665Tif28YMGg7CBcdTAwMTNcdTAwMDKRXHUwMDAzXGaj36HulMtuylx1MDAxZi3QwFx1MDAwNJnBzjSrvVx1MDAxZjhcdTAwMTXHM92zjIK5IfmE4iNiUlx1MDAwYjYuJqmG6eaYTc9S8peVJY1JgpBBXHUwMDE0xKRElFx0hjHLxCSh2kCEMM41XHUwMDA355dcXCwsJpGhpeSKgzaMyVx1MDAxMVx1MDAwMcm4QVx1MDAxNVx1MDAxMlxuaU0wk1SKwVx1MDAwMMVUUIlcdTAwMTVHM/CUrqazRiggXGKZJULDyFxmolxyxys7Xlx1MDAwNVx1MDAxYZN17yf5niZcIroxbDXDrlxyXHUwMDExo1xu0JMpXHUwMDAwUoKFZDzVrWI24oXIoFx1MDAxOMGUY1x1MDAwNeingIj3OvRcdTAwMTfggu2VJyvFOo9cdTAwMWN92l5v75OrWqd0XCK+XFw0ySil1kArXHJTg5HATFxizaRcdTAwMWVWXG5TQ1x1MDAxMVx1MDAwNViHOcFYa4yHtHLNMCr69bpcdTAwMTOB9Y99x4tcdTAwMDat3DXnelx1MDAxY+xV2yxcdTAwMGa2wqjSbYOo0IglZilp8m41XHSb7of++3/ej+492pnjY9iNXHUwMDEzYSvp13FoXHUwMDE22Fb0XHUwMDE0zCNcdTAwMTCNUDpcdTAwMTbSMIQvl1qpXHUwMDA0KiZhWv4kLymmQXppYIUhX0GUUKF1lmdcdTAwMTCtXGZCMSyGXHUwMDE4QStTfECzOfJcZpFgVFx1MDAxZseUXHUwMDFhwi2CtITVJoV6r5Jfbd7W6N3mbadxb53csk7rlG6ffVre/Opi97zV3melg/2Al7c7pElYScxBLrko73zaqllcdTAwMDdqneGzunu06V1V5iB3QeZ9W2Jr4mKntdU4wd+aYbXW4lvo9rD8x7hLLfaRblx1MDAxZu/fsiNyXiu5XHUwMDFi9p44q1x1MDAxZOzOo0Bycllj1YOz/a/0+HHnq/ulxYLSi+ROKlx1MDAwZYw2UFwi9ueCm0PuuEB44cVcdTAwMDFC2fhlWyhcdTAwMTmTXGKS5J2Tlu18v1jWZZuSvGWbXHUwMDAyRZSvtGzzXHUwMDExy3YqZe4v20RLyqRIxvtcblx1MDAwNVx1MDAwMaZcdTAwMTTCXHUwMDFhP8MjR1x1MDAxN1x1MDAwNKYtXHUwMDAwXHUwMDE0f2bn7669uMEpf7yOK/+uX7kuXFx7o2tcdTAwMDOcZOT0U3/Xvs36/7MqXHUwMDAzXHUwMDEz2OhgZWCi5rPTbHDG8fc3XGLTXGLy0OnD9ZN9ZNPHc+fh6/bxSXX7xrNcdTAwMWXto19cdTAwMWKufGK0XHUwMDFhXHUwMDE4XHUwMDExXHUwMDAx2CgohFx1MDAwMYCTyEQrXHUwMDEz3Fx1MDAxMMCyIVx1MDAxOedap1x1MDAwYlhzXHUwMDBmVo2Gg1VcctdcdTAwMDY0xZhS9dr3ME7XnMvS/tq3b0FJ31xir3VPXHUwMDAzbP3h2PPi2Fx1MDAwYjLv21x1MDAxMrsojv22rOBeXHUwMDE2vfXO1n2FXHUwMDE3afXIosXjqIh+PyvojaK62S6yolx1MDAxMPbp2d5DZWurvbxWWFSqMXd1J2VcdTAwMWGjL5iI7b2bJ6/LpS/jMlxySphcdTAwMWE8nTBcdTAwMTeluKY0YbqTmEu+mZeTuYgsc8lmXHUwMDE5TKLX4i1qXHUwMDA0b1x1MDAxOXFPQ1NcdTAwMWRXilOT8n+YZGw0o8j33sXnnrj6deGrXHUwMDFkXlx1MDAxN94/fWqZgWN6XHUwMDEx0PewaVkwuvGZh8xcbp9T5jGBoVx1MDAwZmZcdTAwMWWzXHInN54npCNCjlx1MDAwYmrOXHUwMDE44/g5NzLXLiufxVWntLPjXHUwMDFjuvbnzm7n7mHpq1x1MDAwN4RcbkNThFx1MDAxNCVcXGlAuYG4hnxcdTAwMDTBeYw4U9BXysVcdTAwMDX2dFx0XHSDMVx0qVx1MDAxN1A7yKWK+uiIq8+HzZ3L5teaf2Fe3W3JP+nIvNKRXHUwMDA1mfeNiV1QOvK2rLCodOSNWWFB6cjbssL8b3wsSN1JWc7oXHUwMDBiJmJ775Ygy+Fjs1x1MDAxY6KB4Ovn3E7JN/OyXHUwMDEyXCKGc1x0XHUwMDExJDqvRYimzHRcdTAwMDRcdTAwMTJUKM5cdTAwMTdQoV3qTOfQXHUwMDFmkVx1MDAxOdiAXHUwMDAzwWunOVx1MDAxM5j/XHUwMDE0ac6kseRG89h9mlx1MDAwNOHxt0fBwbHkWj5j93QuXHUwMDFjL2s8XHUwMDEzajBcdLFEhGSMZm+3UKVcZsA0QZiIN/nSXHUwMDA1XHUwMDA2M8aGXHUwMDEwglx1MDAxMcYpJYAtbDi2IdeK94tCZFx1MDAwMdBcIonpUKgrgCNGOZmhqDH7Rs1cdTAwMTeE+pRcdTAwMWI1p95cdTAwMTOJXGaGYfhgXHUwMDAyKjhHhKhUp6dcdTAwMWSRxMBgZULiXHUwMDFlQilGSa/HM/dp5sd0RicuXHUwMDA0LFx1MDAxNTzeXHRMJUpcdTAwMTC6r1x1MDAxM8w90lJoXHT6IFx1MDAwMXP8tndpjvfl+Fx1MDAxOPLiRNxK+vXZaIY104Onkz2aSkpYtNn0ezTzS+jLWYMlYHkpkFx1MDAwNk+iUjCW2lx1MDAxM/lcdTAwMDRn2oh3XG5cdTAwMGLEqKZcdTAwMDIrNqDYPPFcZmBVXHUwMDExrVx1MDAxMEw2k0IlO1x1MDAxN1x1MDAxMjwjhlx1MDAwMColMFdKXCLGdKoq3Fx1MDAwMzREXHUwMDAwk1x1MDAxNVEzlHNmXHUwMDA3NPjHuUx86Vx1MDAxN1x1MDAwMtpcdTAwMWGgXHUwMDA3zKRgmDGAdSZIqtNcdTAwMTN4gJ0pWFlRXG44I7RmM248z6/FXHUwMDBl6Fx1MDAxNKuEgFx1MDAxZmDALZxQ/tXUvnNcIrrPXHUwMDFjUayUxupNXHUwMDAz2tp4b46PYT9+JqTlXHUwMDE2oWFix6NcdTAwMWFnXHUwMDA0ol1PX4Qu31x1MDAxY1/c3947l6XPx7e6yuT5oW3+WlSjk1BccixvSLA6XHUwMDAx6ytcdTAwMDbeNLwnRlx1MDAwYlx1MDAwZcu6XHUwMDEyWCP2omdp/mKWsG85Y8OQRkSCpklcdTAwMDE6wbVcdTAwMWVmcZhcdTAwMTNccvrMsOl8XHUwMDEyZPXdakTNorTl7IR8XHUwMDE37Z5cdTAwMWNcdTAwMWY+3lhHlebGWvHlJZaiPGqZdvPTvfmlXHUwMDFkXVx1MDAxY9HD8DDam0OJZe7qTiqxjL7glNo6n3evLm4qdrBn1qnliK90s/FtOiv8XHUwMDA0gDz6XGZ/iWstqHQj5djKXHJGgoLLajZ9qpc/fctcbiMyXHUwMDE3RjQ3WFx1MDAxYUZcdTAwMTaX7KWraMmDscPpXHUwMDFjQLtcdTAwMDI6J39B5Wam5+5SlVx1MDAxYoIyZ/uVm2QoPys3divWydizO+9qdufjdeHsujDmyVid+fLcnoydsCZcdTAwMGWWZ0YrPPtcdTAwMDKvUyvWYE1cdTAwMTWmgrDnZC3ty9ZR5+BTZJ5vr31ztrfq7EBtLntcclx1MDAwNlJcdTAwMTFDQqIo4u0jXFxcdTAwMTA+UIVBkDJcbowhhWNAhNGLXHUwMDAy8+VcdTAwMGI8dIOo1LM8r/6SXHUwMDA13lx1MDAxNlx1MDAxYid3ev2uuVtad+W3dus4OKsv71x1MDAwMr8gdecudlx1MDAxMm9cdTAwMTh9wd+HNyg9dkc+iZ+Fxlx1MDAxMLfT3/LJn76lhSeRXHUwMDBiT5RcdTAwMWJoXvA0XHUwMDE33iBjXHUwMDEywyVdwHOvf3hDwlx1MDAxYiastXPgXHJja52pZ6NcdTAwMDb3pXFcdTAwMDSrXHUwMDEzXHUwMDE208dkPkg9KybJq8UkV8JAkGjHv66VpfFcdTAwMWNcdTAwMTkqrktROX6TqS3iavDsgSjhXHUwMDFhXHUwMDA0UTzqXHUwMDA3brgwQC/IXHUwMDFmXHUwMDA0RfG2XzlcXNVkQmtMkX7d2zRwSZz6XHUwMDE1g/lXNfN59Gr6llxiXHUwMDAxnGRIi/hX0SBcdTAwMTVcdTAwMWLxc1x1MDAxYVx1MDAxOEBVXHUwMDAzmGI4YmuyXodnVjXzY3RAJ+B1Or6nT1x1MDAwNJNsSCNhXHUwMDAwwnVnlOk4W6ZDXHUwMDFhvama5rBcdTAwMGZ3T1x1MDAwZrtvXCJpJf36bFwiMb6MyblShKR/X2ZcdTAwMTJmSeteXHUwMDFkXFzV/eKRf3/X1vqiendcdTAwMTUuP2ZJXHUwMDAz+Fx1MDAwM9aKgWdjNlx1MDAwMFxcSFx1MDAxYZpJQYVWWDGxuC3yPFlcdTAwMWJcdTAwMTJcdTAwMTYxjFI0vlx1MDAxZFx0gfeqv8slXHUwMDA00UrNhFJTsIjhfSM3zZub9Fx1MDAxMp+mXHIq03vavSCR31x1MDAxOMdcdTAwMTgyo1x1MDAxOKRcdTAwMDc9TZ5ia6VcdTAwMTe3XHUwMDA1s9E4jcBCfYiDSXDKvWEm8lxuLcdub4zIdm+7Ryy1XHUwMDFir3Fk2PFcdTAwMTR8/7Hy419iJnwqIn0= App()Container( id=\"dialog\")Button( \"Yes\", variant=\"success\")Button( \"No\", variant=\"error\")events.Key(key=\"T\")events.Key(key=\"T\")bubble

As before, the event bubbles to its parent (the App class).

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXGtT4lhcdTAwMTP+7q+w2C/zVlxy2XO/bNXWW4p3XHUwMDFkXHUwMDFkXHUwMDA1dZzXLStAhFxmgWBcdTAwMTK8zNb+97dcdTAwMGYqXHS3XHUwMDEwlTgwO1QpkFx1MDAxYzqdPt1Pnu7Tyd8rq6uF6KHrXHUwMDE0/lgtOPc123PrgX1X+Gi23zpB6PpcdTAwMWTYRfrfQ79cdTAwMTfU+iObUdRccv/4/fe2XHUwMDFktJyo69k1x7p1w57thVGv7vpWzW//7kZOO/yv+X9ot50/u367XHUwMDFlXHUwMDA1VnyQolN3Iz94PJbjOW2nXHUwMDEzhSD9f/B9dfXv/v+EdnXXbvuden94f0esXHUwMDFl13p066Hf6auKmWBMak3jXHUwMDExbrhcdTAwMDFHi5w67L5cdTAwMDaNnXiP2VRAZVxc/LZ3vr7+5UpUrtTa3XkobuPDXrueV45cdTAwMWW8R0PYtWYvcOK9YVx1MDAxNPgt59ytR01z9JHtg9+FPtgg/lXg91x1MDAxYc2OXHUwMDEzhkO/8bt2zY1cdTAwMWXMNoRcdTAwMDZb7U6jLyPeclx1MDAwZt+KmEhLIaQ5ZZxJitRgd19cdTAwMDDllmJKI0a1YFxu61x1MDAxMcVKvlx1MDAwNzNcdTAwMDGK/Yb6r1i1ql1rNUC/Tj1cdTAwMWWjSE3jxEnfPZ+uolx1MDAxNmOUXHUwMDEwSVx0WFx1MDAxY1x1MDAxMT5cdTAwMTjSdNxGMzJjXGJcdTAwMDE9mVCSP1x1MDAxZSphI6c/J1hJolx05yreY1x1MDAxNOju1vve8deoTZt20H2yXSE0X1x1MDAxMspcdTAwMWK9N0ddK+leiXnvKSEu9q/0fveb+vL15vtR8M0hXHUwMDAzWUO+XHUwMDE4OfdRYbDjn4+/xP4kYodGf8x6wKzaXHUwMDFlRrf35Vx1MDAwYreoP92cVnfOTlx1MDAxZZwvXHUwMDA3k7W1g8C/S8h9+lx1MDAxNPt+r1u3XHUwMDFmIVxmXHUwMDBiQVx1MDAxOeJEUarkYL/ndlqws9PzvHibX2vFqLeSUHhcZmyHzj+JtEhMRVrEXHUwMDE4w0igOOhnIW369C0u0pI0pFXCklx1MDAxY1x1MDAxM1x1MDAwMFn2ZqSNXHUwMDAyu1x1MDAxM3btXHUwMDAw4GtcdTAwMDLaytloS8bRXHUwMDE1SSFcYvxcIlZsbug6T/eMvcDvRGX3e9/FhMWxYohcYkTgmo64XHUwMDFjXHUwMDFhtWW3Xe9haGL7flxmmq91u1x1MDAxZv6TNHXogFxufZl8aPCa5zaMo1x1MDAxN2pwUk4wXHUwMDE0XHUwMDAzkVx1MDAwYtRlMKDt1utewlx1MDAxZmuggVxyMoPdLCzCXHUwMDBm3Ibbsb3KkIKpMfmIXHRcdTAwMTOCXHUwMDEyI6KmRaVcIkozJjDOXHUwMDFllKkotahBSYVFXGJFhHCFNNA9OVx1MDAxNJREUPBcdTAwMWNcdTAwMDAnJoFYYIJZflFpSawxVlxcXHRkXFyejFx1MDAwNyUjXHUwMDE2XHUwMDE1lFJcYlxcRVx1MDAxNUpQ0+dcdTAwMTiVmFx1MDAxMcpwwjczx2hf1dfG6Fx1MDAxMJq9IEbDyFx1MDAwZaJ1t1N3O1xy2Fx1MDAxOV/7nnl9lpjoR3GtZ7QsXCJcdTAwMGJcIlxcMiVcdTAwMTUgLONcdTAwMWMmViXGNeyusaOFsWCYXHUwMDFhsFVgcfI0YHBcdTAwMTUuOJ36bK22909Pd13S+7z9cNw624nOTsrlL5O0XHUwMDAypVx1MDAwMDypQDA/iiNcdTAwMDPwMeZcdTAwMGW00lx1MDAxNlKUXHUwMDBiialWiFA5ppRnh1HJb7fdXGKs/9l3O9GolfvmXFwz4d507ProXjip5L5RXFzoXHUwMDFhicNkN/60XHUwMDFhx03/y+DzX1x1MDAxZieOLk51Z/NcdTAwMWFz5FjcSvJ9XHUwMDFholx1MDAwNU4teoznXHSoRijlo5ufUY0gpYngmtLMsJY+y4tcbmuYUEsyXG54QJnJ31hsXHUwMDEyI4FiZmkuIaWD2Vx1MDAxMZrlSDZETPxcdTAwMDZApmKweEIuXHKhylx1MDAxMCM5kIs0Zu2fXHUwMDE3T2+6p9WOOjhu3n1cdTAwMGbsi1wiZ29PL7pF50SSXHLt7jKt9nuHLba9s5eNsKfKPd87u707YFx1MDAxYp9cdTAwMGVcdTAwMDJe334gPcI2xFx1MDAxY+SS8/ruzlar9kmtMVxcaXtHm52vjTnIzcm8yyW2Jc53b7e6J/iqXHUwMDE3Nlu3fFx1MDAwYl1cdTAwMWbW/3XGfUNcdTAwMGX7XHUwMDEzWSHqXu00Nq+0s1x1MDAxOVRxsX5cdTAwMWKG3l4wXHUwMDA3KyCnsvnJ82XjqHm/s394sulT725cdTAwMTGtO6tOMvmAsdhnejCVjDLNgcfH5Dmnelx1MDAwNrDsqSRcdTAwMDNcdTAwMGItXHT8yewkI93OXHUwMDBiSzKwSiVcdTAwMTlcdTAwMTRZ7H1IXHUwMDA2n0AyXHUwMDEySf5cdTAwMTPJgFx1MDAxY85cdTAwMTQxWDwv71DBeHRI/Fx1MDAwMoecXFzByFqxKD2XXHUwMDEzPlxcdsxcdTAwMGW3/uelWVx1MDAwM/H8xmXhsjO5mMHJkJxBrcJzrofd/0WljFx1MDAxOdR5tJQxU/PUSE3NXHQopmRauCpIXyVjQmaO1kP3XHUwMDA2bzU66/dcdTAwMDeoev39SFKvVKr/2GjlM4OVMG0pXHUwMDAyXHUwMDE5L1wiWFxuotVwsDJcdTAwMDU5XHUwMDE505JJzjWwcZFfsGo0XHUwMDFlrEqMXHUwMDA2K1NcdTAwMDIyYczeOSNwTnmVVuAlO8Xt1s3DWsA3rn5lXHUwMDA088pcYnIy73KJzSsjWC4r5JVcdTAwMTEsl1x1MDAxNTy9XlLV7Vx1MDAxMitcdOGUK/v3ja2teTD3nNTNK4FZlkmblb9MPmAs9unTXHUwMDBmz18oYdNbXySmlFx1MDAwYqqzM6J0Oy8sI2LpjEi+XHUwMDE3I1JcdTAwMTNcdTAwMTiRXHUwMDFjY0RKaES1UD91+rLei1wiv/PBbHvMXHUwMDAyLlx1MDAwYlx1MDAxN054Wfj4+O3WXHUwMDBlXFy7XHUwMDEzQWJcdTAwMTD2ajU4u+k5jVx1MDAxY1x1MDAxNj6nnGZcdTAwMDb3XHUwMDFmzWledzqpIT0j0Vx1MDAxMVPjmmjNMKTj2fss1Oej3fXjXHUwMDBi965q22F5u/LlU/PsfvHLXHUwMDEy1IJ4RVxcYoI1XHUwMDExmqmRuMaWwtysWULSQ4XKL64zZjpCaa6QeOe2tWtSo7h3Ujyon8pcdTAwMTLueC1ug9Bfmc6cMp2czLtcXGLzynSWy1xueWU6y2WFvDKd5bJCXis1y2KFWVx01ORcdTAwMDPGYp8+LUBcdTAwMDLFU1x1MDAxMijJJWGSZb93IN3Oi8q01FxmoiXei2hlS6BcdTAwMTjTjCgt/m1cdNShPyHhcFx1MDAwMF2C986eZiRcdTAwMTRcdTAwMTmyp1nnklx1MDAxYcxTO2FcdMJpy7mYMcp59rwpXHUwMDFk5Fx1MDAxNzWaibRcdTAwMTinlEqJXHUwMDA0I5QmXG5ccv1wRtzChGAlsemkppLnXHUwMDE3z1x1MDAxOFtCgFx1MDAxMkZcdTAwMWZcdTAwMDJQy8bDW3CLa66V6ZXUSGI6XHUwMDFh7Vx1MDAwMExCICXly6P99b2wL7/6JPSY1lx1MDAwYlx1MDAxYveRMkw5XHUwMDAxX6RiSmsrsTCYjVx1MDAxMDNCKMVoou8yS/fq0+DZnbCxTmBlXHUwMDBlXHQsolxmfFx1MDAwMsWoO9BcdCZcdTAwMTNpKbRcdTAwMDR9kIBJw9N0mlxmXHUwMDBmYzotUyPsdFc2rzEnjsWtJN9f3tqvp9d3KTiQubsxO56lV/1cdTAwMTdcdTAwMTXPqDbBgFx1MDAxOdicXHUwMDEygHBcdTAwMWRfQ1x1MDAxZvFMWFRcdTAwMGJcYiNNNVx1MDAxNXnecIOpXHUwMDA1XGZIK1x1MDAwNJPNpFBcdTAwMTN6+1x1MDAwNbGEpkhcdTAwMDBfUlx1MDAxMoGyXHR0fWIvXFxcIlx1MDAwZVx1MDAwMPOKXHUwMDA18Vx1MDAwNcWzXCKAXHUwMDA3NVxyOTA5nEgmkmD1iFx1MDAxZGA4XG5mU2ZBQlxinWgxylx0z4xORiWzJoBcdTAwMDG2cNySXHUwMDFjXHUwMDAzXHUwMDFhtYhAYFx1MDAxZkSxUlx1MDAxYatpOk0uXHUwMDE2LzWeXHUwMDE1pzuzeY278Vx1MDAwYlx1MDAxMS21uq3o9LtcYvshTjDJjmqVXHUwMDA3t1k+uHPuSifOt/Jaw1x0i9/2fyyq0VmgRihcdTAwMWVes1x1MDAxYcm4jPmxJNLgXHUwMDE5SvY0veZ2bVZcdTAwMTPONWdsXHUwMDFj0UhcIpmLK9sxTDzfj4SVuWmKklx1MDAxY+5HXHUwMDFh+NWEqkXt+vz4QNb3mq1cdTAwMWLvXHUwMDBivjj8fmJvbb29XHUwMDE4UpJHt7bT27mxT++i8yN6XHUwMDE4XHUwMDFlRvuTxb6odpOTunNcdTAwMTc7q3Yz+YCx2GdcdTAwMDBIudpcdTAwMDDyJlx1MDAxNk1yqt1IOb10g1x1MDAwMdZcdTAwMTBcdTAwMDeyllx1MDAxOUbSzbyoMFwiUmBcdTAwMDRi1sJcdFx1MDAxOHlcdTAwMTOKpDIjwifgXGJcdTAwMTlL5bBkXFzBgX5A4eZV1CdRuCFoaOugcFx1MDAxM5/Kc+HGuTU6WfvOw4eW8/DnZaFyWZhy67FcdTAwMWX68dxuPZ5xQVx1MDAxY63OTFb49Vd3nUjzx8KSYE2xTjzaY1ZYejdl9nnz/qHZ3tpu672d43DXX1/wsFx1MDAwNPSxuGKacHPNXHUwMDE0hI801Fx1MDAxM2KZZmVI/Vx1MDAxOXBmJN90N/LbL+8ms9LoVVx1MDAwYtdvubpfXHR0UdtZP946p3vVs/2Tln/8eWNxr+45qbssYmeRhslcdTAwMDfMqG2vWj/fYdXKzV6RrYn2UbVabspsc5aBjEhNaO5PRlE65W5lSLA4ZSp7TpM+fYuKejxcdTAwMTX1OLbk3FBvXHUwMDFldISYslx1MDAwMEf8XZ+EXHUwMDAy7kg5euuTUJaJjsy4gudNR8wjiaZGJqZKcYNcdTAwMGaZI3N9jbZcdTAwMTTfaWzfn5XOfMlC7yxoLXpcclx1MDAxNZtcdTAwMWGpXHUwMDEwXHUwMDFjklx1MDAwMY20XHUwMDEymlxyhSbm3JJcXHJwTZO64eRcIsyPICRcXDNO2KtcdTAwMTZ430JIPt/qYD3c6V03v29ffN3arGzc3PdcdTAwMTaXkOSk7nKJ3f3aq5xViN495Vx1MDAxN2frd07P35XeXCJcdTAwMWF3XHUwMDE2f5p8wFx1MDAxZs+fKFVEXHUwMDEynHsxXHUwMDA3J59cdTAwMDQ5XG7TXHUwMDE0XHUwMDAwQSRJ3CyUTp++RUVpjFNRWnGLzFxypedSz4G0kSMk87i5c55cdTAwMGW57Fxmalx1MDAwNueYXHUwMDAzg0p5ttz09WdJXHRcdTAwMTOcveDRcqkw9aKoJO9cdTAwMTaV5vZcItPywKnCiGAx0lx1MDAxZEeRtFx1MDAwNMdSIVxykavV9NuLXHUwMDFjIeVbgtI8fkxRhDnlXGaSK8YxmfC4XHUwMDA0XHUwMDAxWVx1MDAxNoP9XHUwMDE0eC2nVNCx9jmTnUlcdTAwMDRcdTAwMTDznlx1MDAxZDVPUfuq/rmMT5dLzzNWh5/jXHUwMDA2zF9rJMCI2DzCLNHN8bw4zCxBuVx1MDAxNppIhVx1MDAxOWj+3NPxwqfLpcfu6tCKNaJEKTgq4qbpR3MxrpayXHUwMDAwdDVkrlxcKUSVYGNaLdUqdJpP91x1MDAwN4y7cyxzJfn+Yr6RuJyNXHUwMDAwXHUwMDFiU5pcIk1Zdrpx4GyfXpVON4vnVcq/ke1cdTAwMTLZrp0sPLBJYVx1MDAxMbP2jzhcdTAwMDZGQYZxjUjz5DlOzJ2KXG6ihOXXJpgowMRkY6xvxviAIFi9a7VcdTAwMDbYXHUwMDE45TSvxaPxrt9qr1pNXHUwMDEygSS5UEOjs3byRn53XHUwMDFhr1x1MDAxODqLUVx1MDAxMvGkyWNsrTxFcMHudstcdTAwMTFYaFx1MDAwMHgwXHRu/ek0Y3mFW9e5W59QXHUwMDFhuO6/jNR+vJrIcMxcdTAwMTT8/c/KP/9cdTAwMDeoXHUwMDAzMlx1MDAwNiJ9 App()Container( id=\"dialog\")Button( \"Yes\", variant=\"success\")Button( \"No\", variant=\"error\")events.Key(key=\"T\")events.Key(key=\"T\")events.Key(key=\"T\")bubble

The App class is always the root of the DOM, so there is nowhere for the event to bubble to.

"},{"location":"guide/events/#stopping-bubbling","title":"Stopping bubbling","text":"

Event handlers may stop this bubble behavior by calling the stop() method on the event or message. You might want to do this if a widget has responded to the event in an authoritative way. For instance when a text input widget responds to a key event it stops the bubbling so that the key doesn't also invoke a key binding.

"},{"location":"guide/events/#custom-messages","title":"Custom messages","text":"

You can create custom messages for your application that may be used in the same way as events (recall that events are simply messages reserved for use by Textual).

The most common reason to do this is if you are building a custom widget and you need to inform a parent widget about a state change.

Let's look at an example which defines a custom message. The following example creates color buttons which\u2014when clicked\u2014send a custom message.

custom01.pyOutput custom01.py
from textual.app import App, ComposeResult\nfrom textual.color import Color\nfrom textual.message import Message\nfrom textual.widgets import Static\nclass ColorButton(Static):\n\"\"\"A color button.\"\"\"\nclass Selected(Message):\n\"\"\"Color selected message.\"\"\"\ndef __init__(self, color: Color) -> None:\nself.color = color\nsuper().__init__()\ndef __init__(self, color: Color) -> None:\nself.color = color\nsuper().__init__()\ndef on_mount(self) -> None:\nself.styles.margin = (1, 2)\nself.styles.content_align = (\"center\", \"middle\")\nself.styles.background = Color.parse(\"#ffffff33\")\nself.styles.border = (\"tall\", self.color)\ndef on_click(self) -> None:\n# The post_message method sends an event to be handled in the DOM\nself.post_message(self.Selected(self.color))\ndef render(self) -> str:\nreturn str(self.color)\nclass ColorApp(App):\ndef compose(self) -> ComposeResult:\nyield ColorButton(Color.parse(\"#008080\"))\nyield ColorButton(Color.parse(\"#808000\"))\nyield ColorButton(Color.parse(\"#E9967A\"))\nyield ColorButton(Color.parse(\"#121212\"))\ndef on_color_button_selected(self, message: ColorButton.Selected) -> None:\nself.screen.styles.animate(\"background\", message.color, duration=0.5)\nif __name__ == \"__main__\":\napp = ColorApp()\napp.run()\n

ColorApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aColor(0,\u00a0128,\u00a0128)\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aColor(128,\u00a0128,\u00a00)\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aColor(233,\u00a0150,\u00a0122)\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aColor(18,\u00a018,\u00a018)\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

Note the custom message class which extends Message. The constructor stores a color object which handler methods will be able to inspect.

The message class is defined within the widget class itself. This is not strictly required but recommended, for these reasons:

  • It reduces the amount of imports. If you import ColorButton, you have access to the message class via ColorButton.Selected.
  • It creates a namespace for the handler. So rather than on_selected, the handler name becomes on_color_button_selected. This makes it less likely that your chosen name will clash with another message.
"},{"location":"guide/events/#sending-messages","title":"Sending messages","text":"

To send a message call the post_message() method. This will place a message on the widget's message queue and run any message handlers.

It is common for widgets to send messages to themselves, and allow them to bubble. This is so a base class has an opportunity to handle the message. We do this in the example above, which means a subclass could add a on_color_button_selected if it wanted to handle the message itself.

"},{"location":"guide/events/#preventing-messages","title":"Preventing messages","text":"

You can temporarily disable posting of messages of a particular type by calling prevent, which returns a context manager (used with Python's with keyword). This is typically used when updating data in a child widget and you don't want to receive notifications that something has changed.

The following example will play the terminal bell as you type. It does this by handling Input.Changed and calling bell(). There is a Clear button which sets the input's value to an empty string. This would normally also result in a Input.Changed event being sent (and the bell playing). Since we don't want the button to make a sound, the assignment to value is wrapped within a prevent context manager.

Tip

In reality, playing the terminal bell as you type would be very irritating -- we don't recommend it!

prevent.pyOutput prevent.py
from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.widgets import Button, Input\nclass PreventApp(App):\n\"\"\"Demonstrates `prevent` context manager.\"\"\"\ndef compose(self) -> ComposeResult:\nyield Input()\nyield Button(\"Clear\", id=\"clear\")\ndef on_button_pressed(self) -> None:\n\"\"\"Clear the text input.\"\"\"\ninput = self.query_one(Input)\nwith input.prevent(Input.Changed):  # (1)!\ninput.value = \"\"\ndef on_input_changed(self) -> None:\n\"\"\"Called as the user types.\"\"\"\nself.bell()  # (2)!\nif __name__ == \"__main__\":\napp = PreventApp()\napp.run()\n
  1. Clear the input without sending an Input.Changed event.
  2. Plays the terminal sound when typing.

PreventApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Clear\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

"},{"location":"guide/events/#message-handlers","title":"Message handlers","text":"

Most of the logic in a Textual app will be written in message handlers. Let's explore handlers in more detail.

"},{"location":"guide/events/#handler-naming","title":"Handler naming","text":"

Textual uses the following scheme to map messages classes on to a Python method.

  • Start with \"on_\".
  • Add the message's namespace (if any) converted from CamelCase to snake_case plus an underscore \"_\".
  • Add the name of the class converted from CamelCase to snake_case.
eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nNVaa0/byFx1MDAxYf7eX1x1MDAxMeV8OStcdTAwMTV37pdKq1x1MDAxNbC0hZSKXHUwMDE2SludripjT1x1MDAxMlx1MDAxN8c29oTLVvz3845cdLGdXHUwMDFiJFx1MDAwNJaNXHUwMDA0iWcm9ut3nud5L86vXHUwMDE3rVbbXmWm/brVNpeBXHUwMDFmR2HuX7RfuvFzk1x1MDAxN1GawFx1MDAxNCmPi3SYXHUwMDA35cq+tVnx+tWrgZ+fXHUwMDFhm8V+YLzzqFx1MDAxOPpxYYdhlHpBOnhcdTAwMTVZMyj+cP8/+Fx1MDAwM/N7llx1MDAwZUKbe9VFNkxcdTAwMTjZNL+5lonNwCS2gLP/XHUwMDBmjlutX+X/mnW5XHSsn/RiU36hnKpcZqSMT45+SJPSWEyEIEghQcYrouJPuJ41IUx3wWZTzbih9uHRWXbGf36Sxu/wY3HOWYfH1WW7UVx1MDAxY1x1MDAxZtqruDSrSOFuqrnC5ump+Vx1MDAxMoW27649MT7+VuhcdTAwMTd9U/tanlx1MDAwZXv9xFx1MDAxNO7+0Xg0zfwgslfuRKhcdTAwMWG9cUJ93WXpXHUwMDAx5mkqJEOSY0mVqlx1MDAxY+JOQFx1MDAxNfVcdTAwMTRiWlx1MDAxMy2xkpLwXHTTttNcdTAwMTj2XHUwMDAyTPtcdTAwMGYqX5VtJ35w2lx1MDAwM1x1MDAwM5OwWtP1OeHg2GrVxeiWufaI1FIqxqjQRHAxXtI3Ua9vYVxymMqphlx1MDAxZKlcdTAwMTlhyt2QREmMqVx1MDAxYY+7XHUwMDBiZ7thiYu/Jr3Z9/Ns5LR2aWDNaHe4U1x1MDAwM1X15WFcdTAwMTb6N3uPhaCMYiYwl3Q8XHUwMDFmR8kpTCbDOK7G0uC0gks5ev1yXHUwMDA1nHIh5uGUaoKpVETcXHUwMDFiprvKbH3K2LtT8/VL9mZjiFx1MDAwZnW8/8xhypDwsFx1MDAwMjcgXHUwMDAxb0hKOVx1MDAwMVPmwYZIqplmgmPxIJRicqKUmIVSgpCHXHUwMDA1lkpywrRAWC2DUsw1sEgyjNaP09FEXHUwMDA1rNqGZ2pPXHUwMDA1vv3z8it786M3/FZcdTAwMWO9XHUwMDFkRuNzNVDo53l60Vx1MDAxZc9cXI8+zWdcdTAwMDHilMHNPlx0XHUwMDBiXHUwMDE0V/NYoCRBVFx1MDAwYszuzYKtg86eke92XHUwMDBlXHUwMDA2W+Kk0z00XHUwMDE355vZM2eBQMqTXHUwMDFjIaEoXHUwMDA1KFwiPEVcdTAwMDLFXGIjXHUwMDFhlNyJwsNYwFx1MDAwMmG6fFx1MDAxNlx1MDAwYjDhXHUwMDFlgFx1MDAxOPM6xu+Bf65cdTAwMTQhgj6GTC+C//bnz2FHXuxcdTAwMDdcdTAwMWb3tnl+2T/svDtGa4M/k6qGulx1MDAwN8Lfmks7XHUwMDBi+Vx1MDAxOOm5XHUwMDAxXHUwMDAwXHUwMDEyXHUwMDE1XGKcXGYtgX1fXHUwMDFl51x1MDAwN/tZxFx1MDAwZtOf4uJIbnx7n+C1Yn/iW3Xo45WgT1x1MDAxMfVcdTAwMTjSWiHIWIRqyj+XIMtcdTAwMWMxqTlnXHUwMDFhXHUwMDA06bGAr+g03jmaxLnETElIY8TyOC/cwYo4XHUwMDE3Nk63szOqTj7GfPPqfNOc7H5dXHUwMDEzzlx0oYIqslx1MDAwNM4rNKWJPYz+NmX4bIy+8Vx1MDAwN1F81YBESVx1MDAwMDBw3z81Rcv2TWtgbD9ccr8nPnwqXG6/Z1p9P1x0Y5PXN7EwYJC7XHUwMDAyo41TbcZRz/GnXHUwMDFkm26TWDaCemI8bdOa01x1MDAwMzDNh9Plu+HkLaZ51ItcdTAwMTI/PlrCzJVcYi/lXFy+Q6AjIFx1MDAwN7yWRdzF9zefd97KLOb9y1x1MDAxZPb+7eCg8377p37efGeMe1x1MDAwNFx1MDAwM6khmDHBVDPUQcrhQSjhXHUwMDA0SSYg4j1apNOVqC4gPKFgiUtcdTAwMGKflvCPmddcdTAwMTFcdTAwMDI7oKpcdTAwMWJ6dMKPWJNAzV9cdTAwMDBOzPfkv+nQmrxcdTAwMTXEflH8tlx1MDAxNNtcdTAwMDPwXl0g1sf3u6xcXInsXHUwMDAyycnRMdlcdTAwMTXVUNjo+3M97n56XHUwMDFmn3X2985+fOufpztcdTAwMWaO9o7X24RYO9dcdTAwMDUlnlx1MDAwMFx1MDAxYbuyTihBmlxc54J6RFx1MDAxMq2g2Fx1MDAwNtFTXHUwMDBm60As4DqrrruA61hLXHJ/XHUwMDFjVcHwScj+mFksRHdcdTAwMDVC+2Rkd429Vtr9ntzGypI9z4Pic2xbSOxcdTAwMWKHz6pYiZ5cdTAwMWO9ZTaTpKyH7t9eXFyc3y3BbDKJ0Vx1MDAxNZkt70zahfa05sQ1ypieiOGcS09Csq6ohDBcdTAwMGbEnsvrUDOFuqtXq641XHUwMDA0nlaUKC2FnNFZxIR6XHUwMDFhMSpcdTAwMTVcdTAwMThcdTAwMDJcdTAwMDU0q1x1MDAwNPm2duVQ5nGxXHUwMDAy6VfvMN4k3ct0XHUwMDE4a3b4ud2KkjBKejBZ6cltx3z3XHUwMDFlhWBJ5GDorNxAXHUwMDFlRYxzjClcdTAwMTJQ84Jtsras52cjT3NcdTAwMDFcdTAwMWJcdTAwMGVBS1x1MDAxM4bxaMH12CyThHdcdTAwMWLV/Vx1MDAxNvTzqyP68biTXHUwMDExxTa2gy8mnmVcdTAwMTTyXHUwMDE0XHUwMDE4xFx1MDAxMFx1MDAxNH1cdTAwMTIkWVA9bVx1MDAxMvdcdTAwMTBsLFx1MDAxM6497HrYesomoLfdTlx1MDAwN4PIgu9cdTAwMGbSKLGTPi6duek43jf+lIDAPdXnJsUgc2dsqnv1qVVcdTAwMTGmPFx1MDAxOH/+6+XM1fOx7F5cdTAwMWLTMK5O+KL+vrSQQVx1MDAxMEeTw5WSXHTYdCnv339YnLg+RyXjWHuuv4ggzVx1MDAwN6+z6sJlOaKQx6FcIlx1MDAwM4oghvWChyQ80FxmhatKXHUwMDE5XHUwMDA1XHUwMDFiXHUwMDE4p5hcbsY10bXYUXXfqMcoXHUwMDE0RIpgxFx1MDAxMVx1MDAxMzVVXHUwMDFk5S+YKIFcdTAwMTnI8dNKXHUwMDE5g1x1MDAwZlUwXFy/lC2ucZtS5kpoTDjDXG70SkheY9FINzQkpJhqXHUwMDA0rlx1MDAwNDditpqSLX7S0rRcdEmCXHUwMDA1lLRKYoqRXHUwMDE0fMomXHUwMDA1abBEXHUwMDAyYVxydkGWPG3Uv0nKNuaCuZydwvHalIyrudVcdTAwMTZWIJ5Y1blxl5QtTsv/XHUwMDAxKVN3VltaeKrsW0OgRnDHXHUwMDEzWZlcdTAwMDAoUijFXHUwMDFj7sX8xlxubFs3kKsqXHUwMDE5cTVcdTAwMWRkN1x1MDAxYVx1MDAwNFWDINVcdTAwMTKuKinDwnNSi7iCilx1MDAwYik5lZRp95iB1DvfT5OVrVos3VPKXHUwMDE2l/CNXHUwMDA0XGLCsqZcXFNcbuVcdTAwMDSkZOCQXHUwMDFhjUa6IT1QXGbwn1x1MDAwMGAzwlx1MDAxOF1NzFx1MDAxNj8wa1olmGBaXHUwMDBipIWSkFxyztJXXHL7XHUwMDBmlTSDRIVCMf3v1rL5cC6np5G8pJrN61x1MDAxY1x1MDAxMTz3iSiBbFx1MDAwNMKJXFyidbQ48W5qWd9cdTAwMGb6w9zMU7N1NY/0nSUmV56mXHUwMDAwJii1QdpcdTAwMTVrylx1MDAxOVXSQ1gypLSm5IE/XGawuZ9cdTAwMTSZn1x1MDAwMyVm5GZcdTAwMTJcIlx1MDAxNilcdTAwMDFfnmlGbkaxJ1x1MDAxNSbAjNGrZs2oyoRbgJJ4lVx1MDAxZlxiPNcnR5hJcEu1tyv2loTHXHUwMDE0XHUwMDE0Nje+hZdsrFx1MDAxYfeamt1cImdwmvxcYtxcdTAwMDb+OFx1MDAxOVpcdTAwMGJcdTAwMDdcdTAwMDVQILCmkYOPu02Ez92hp3icNNfWXHUwMDE3t74u/dz2s+zQgpfHalxyMInCkauqK7TPI3OxNetnWOXLnbVcdTAwMTRcdTAwMWPHbONA8uv6xfX/XHUwMDAx2ibQXHUwMDAzIn0= Makes the methoda message handlerMessage namespace(outer class)Name ofmessage classon_color_button_selected

Messages have a namespace if they are defined as a child class of a Widget. The namespace is the name of the parent class. For instance, the builtin Input class defines it's Changed message as follow:

class Input(Widget):\n...\nclass Changed(Message):\n\"\"\"Posted when the value changes.\"\"\"\n...\n

Because Changed is a child class of Input, its namespace will be \"input\" (and the handler name will be on_input_changed). This allows you to have similarly named events, without clashing event handler names.

Tip

If you are ever in doubt about what the handler name should be for a given event, print the handler_name class variable for your event class.

Here's how you would check the handler name for the Input.Changed event:

>>> from textual.widgets import Input\n>>> Input.Changed.handler_name\n'on_input_changed'\n
"},{"location":"guide/events/#on-decorator","title":"On decorator","text":"

In addition to the naming convention, message handlers may be created with the on decorator, which turns a method into a handler for the given message or event.

For instance, the two methods declared below are equivalent:

@on(Button.Pressed)\ndef handle_button_pressed(self):\n...\ndef on_button_pressed(self):\n...\n

While this allows you to name your method handlers anything you want, the main advantage of the decorator approach over the naming convention is that you can specify which widget(s) you want to handle messages for.

Let's first explore where this can be useful. In the following example we have three buttons, each of which does something different; one plays the bell, one toggles dark mode, and the other quits the app.

on_decorator01.pyOutput on_decorator01.py
from textual import on\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Button\nclass OnDecoratorApp(App):\nCSS_PATH = \"on_decorator.tcss\"\ndef compose(self) -> ComposeResult:\n\"\"\"Three buttons.\"\"\"\nyield Button(\"Bell\", id=\"bell\")\nyield Button(\"Toggle dark\", classes=\"toggle dark\")\nyield Button(\"Quit\", id=\"quit\")\ndef on_button_pressed(self, event: Button.Pressed) -> None:  # (1)!\n\"\"\"Handle all button pressed events.\"\"\"\nif event.button.id == \"bell\":\nself.bell()\nelif event.button.has_class(\"toggle\", \"dark\"):\nself.dark = not self.dark\nelif event.button.id == \"quit\":\nself.exit()\nif __name__ == \"__main__\":\napp = OnDecoratorApp()\napp.run()\n
  1. The message handler is called when any button is pressed

OnDecoratorApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Bell\u00a0\u00a0Toggle\u00a0dark\u00a0\u00a0Quit\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

Note how the message handler has a chained if statement to match the action to the button. While this works just fine, it can be a little hard to follow when the number of buttons grows.

The on decorator takes a CSS selector in addition to the event type which will be used to select which controls the handler should work with. We can use this to write a handler per control rather than manage them all in a single handler.

The following example uses the decorator approach to write individual message handlers for each of the three buttons:

on_decorator02.pyOutput on_decorator02.py
from textual import on\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Button\nclass OnDecoratorApp(App):\nCSS_PATH = \"on_decorator.tcss\"\ndef compose(self) -> ComposeResult:\n\"\"\"Three buttons.\"\"\"\nyield Button(\"Bell\", id=\"bell\")\nyield Button(\"Toggle dark\", classes=\"toggle dark\")\nyield Button(\"Quit\", id=\"quit\")\n@on(Button.Pressed, \"#bell\")  # (1)!\ndef play_bell(self):\n\"\"\"Called when the bell button is pressed.\"\"\"\nself.bell()\n@on(Button.Pressed, \".toggle.dark\")  # (2)!\ndef toggle_dark(self):\n\"\"\"Called when the 'toggle dark' button is pressed.\"\"\"\nself.dark = not self.dark\n@on(Button.Pressed, \"#quit\")  # (3)!\ndef quit(self):\n\"\"\"Called when the quit button is pressed.\"\"\"\nself.exit()\nif __name__ == \"__main__\":\napp = OnDecoratorApp()\napp.run()\n
  1. Matches the button with an id of \"bell\" (note the # to match the id)
  2. Matches the button with class names \"toggle\" and \"dark\"
  3. Matches the button with an id of \"quit\"

OnDecoratorApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Bell\u00a0\u00a0Toggle\u00a0dark\u00a0\u00a0Quit\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

While there are a few more lines of code, it is clearer what will happen when you click any given button.

Note that the decorator requires that the message class has a control attribute which should be the widget associated with the message. Messages from builtin controls will have this attribute, but you may need to add control to any custom messages you write.

Note

If multiple decorated handlers match the message, then they will all be called in the order they are defined.

The naming convention handler will be called after any decorated handlers.

"},{"location":"guide/events/#applying-css-selectors-to-arbitrary-attributes","title":"Applying CSS selectors to arbitrary attributes","text":"

The on decorator also accepts selectors as keyword arguments that may be used to match other attributes in a Message, provided those attributes are in Message.ALLOW_SELECTOR_MATCH.

The snippet below shows how to match the message TabbedContent.TabActivated only when the tab with id home was activated:

@on(TabbedContent.TabActivated, tab=\"#home\")\ndef home_tab(self) -> None:\nself.log(\"Switched back to home tab.\")\n...\n
"},{"location":"guide/events/#handler-arguments","title":"Handler arguments","text":"

Message handler methods can be written with or without a positional argument. If you add a positional argument, Textual will call the handler with the event object. The following handler (taken from custom01.py above) contains a message parameter. The body of the code makes use of the message to set a preset color.

    def on_color_button_selected(self, message: ColorButton.Selected) -> None:\nself.screen.styles.animate(\"background\", message.color, duration=0.5)\n

A similar handler can be written using the decorator on:

    @on(ColorButton.Selected)\ndef animate_background_color(self, message: ColorButton.Selected) -> None:\nself.screen.styles.animate(\"background\", message.color, duration=0.5)\n

If the body of your handler doesn't require any information in the message you can omit it from the method signature. If we just want to play a bell noise when the button is clicked, we could write our handler like this:

    def on_color_button_selected(self) -> None:\nself.app.bell()\n

This pattern is a convenience that saves writing out a parameter that may not be used.

"},{"location":"guide/events/#async-handlers","title":"Async handlers","text":"

Message handlers may be coroutines. If you prefix your handlers with the async keyword, Textual will await them. This lets your handler use the await keyword for asynchronous APIs.

If your event handlers are coroutines it will allow multiple events to be processed concurrently, but bear in mind an individual widget (or app) will not be able to pick up a new message from its message queue until the handler has returned. This is rarely a problem in practice; as long as handlers return within a few milliseconds the UI will remain responsive. But slow handlers might make your app hard to use.

Info

To re-use the chef analogy, if an order comes in for beef wellington (which takes a while to cook), orders may start to pile up and customers may have to wait for their meal. The solution would be to have another chef work on the wellington while the first chef picks up new orders.

Network access is a common cause of slow handlers. If you try to retrieve a file from the internet, the message handler may take anything up to a few seconds to return, which would prevent the widget or app from updating during that time. The solution is to launch a new asyncio task to do the network task in the background.

Let's look at an example which looks up word definitions from an api as you type.

Note

You will need to install httpx with pip install httpx to run this example.

dictionary.pydictionary.tcssOutput dictionary.py
import asyncio\ntry:\nimport httpx\nexcept ImportError:\nraise ImportError(\"Please install httpx with 'pip install httpx' \")\nfrom rich.json import JSON\nfrom textual.app import App, ComposeResult\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Input, Static\nclass DictionaryApp(App):\n\"\"\"Searches a dictionary API as-you-type.\"\"\"\nCSS_PATH = \"dictionary.tcss\"\ndef compose(self) -> ComposeResult:\nyield Input(placeholder=\"Search for a word\")\nyield VerticalScroll(Static(id=\"results\"), id=\"results-container\")\nasync def on_input_changed(self, message: Input.Changed) -> None:\n\"\"\"A coroutine to handle a text changed message.\"\"\"\nif message.value:\n# Look up the word in the background\nasyncio.create_task(self.lookup_word(message.value))\nelse:\n# Clear the results\nself.query_one(\"#results\", Static).update()\nasync def lookup_word(self, word: str) -> None:\n\"\"\"Looks up a word.\"\"\"\nurl = f\"https://api.dictionaryapi.dev/api/v2/entries/en/{word}\"\nasync with httpx.AsyncClient() as client:\nresults = (await client.get(url)).text\nif word == self.query_one(Input).value:\nself.query_one(\"#results\", Static).update(JSON(results))\nif __name__ == \"__main__\":\napp = DictionaryApp()\napp.run()\n
dictionary.tcss
Screen {\nbackground: $panel;\n}\nInput {\ndock: top;\nwidth: 100%;\nheight: 1;\npadding: 0 1;\nmargin: 1 1 0 1;\n}\n#results {\nwidth: auto;\nmin-height: 100%;\n}\n#results-container {\nbackground: $background 50%;\noverflow: auto;\nmargin: 1 2;\nheight: 100%;\n}\n

DictionaryApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aSearch\u00a0for\u00a0a\u00a0word\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

Note the highlighted line in the above code which calls asyncio.create_task to run a coroutine in the background. Without this you would find typing in to the text box to be unresponsive.

"},{"location":"guide/input/","title":"Input","text":"

This chapter will discuss how to make your app respond to input in the form of key presses and mouse actions.

Quote

More Input!

\u2014 Johnny Five

"},{"location":"guide/input/#keyboard-input","title":"Keyboard input","text":"

The most fundamental way to receive input is via Key events which are sent to your app when the user presses a key. Let's write an app to show key events as you type.

key01.pyOutput key01.py
from textual import events\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import RichLog\nclass InputApp(App):\n\"\"\"App to display key events.\"\"\"\ndef compose(self) -> ComposeResult:\nyield RichLog()\ndef on_key(self, event: events.Key) -> None:\nself.query_one(RichLog).write(event)\nif __name__ == \"__main__\":\napp = InputApp()\napp.run()\n

InputApp Key(key='T',\u00a0character='T',\u00a0name='upper_t',\u00a0is_printable=True) Key(key='e',\u00a0character='e',\u00a0name='e',\u00a0is_printable=True) Key(key='x',\u00a0character='x',\u00a0name='x',\u00a0is_printable=True) Key(key='t',\u00a0character='t',\u00a0name='t',\u00a0is_printable=True) Key(key='u',\u00a0character='u',\u00a0name='u',\u00a0is_printable=True) Key(key='a',\u00a0character='a',\u00a0name='a',\u00a0is_printable=True) Key(key='l',\u00a0character='l',\u00a0name='l',\u00a0is_printable=True) Key( key='exclamation_mark', character='!', name='exclamation_mark', is_printable=True )

When you press a key, the app will receive the event and write it to a RichLog widget. Try pressing a few keys to see what happens.

Tip

For a more feature rich version of this example, run textual keys from the command line.

"},{"location":"guide/input/#key-event","title":"Key Event","text":"

The key event contains the following attributes which your app can use to know how to respond.

"},{"location":"guide/input/#key","title":"key","text":"

The key attribute is a string which identifies the key that was pressed. The value of key will be a single character for letters and numbers, or a longer identifier for other keys.

Some keys may be combined with the Shift key. In the case of letters, this will result in a capital letter as you might expect. For non-printable keys, the key attribute will be prefixed with shift+. For example, Shift+Home will produce an event with key=\"shift+home\".

Many keys can also be combined with Ctrl which will prefix the key with ctrl+. For instance, Ctrl+P will produce an event with key=\"ctrl+p\".

Warning

Not all keys combinations are supported in terminals and some keys may be intercepted by your OS. If in doubt, run textual keys from the command line.

"},{"location":"guide/input/#character","title":"character","text":"

If the key has an associated printable character, then character will contain a string with a single Unicode character. If there is no printable character for the key (such as for function keys) then character will be None.

For example the P key will produce character=\"p\" but F2 will produce character=None.

"},{"location":"guide/input/#name","title":"name","text":"

The name attribute is similar to key but, unlike key, is guaranteed to be valid within a Python function name. Textual derives name from the key attribute by lower casing it and replacing + with _. Upper case letters are prefixed with upper_ to distinguish them from lower case names.

For example, Ctrl+P produces name=\"ctrl_p\" and Shift+P produces name=\"upper_p\".

"},{"location":"guide/input/#is_printable","title":"is_printable","text":"

The is_printable attribute is a boolean which indicates if the key would typically result in something that could be used in an input widget. If is_printable is False then the key is a control code or function key that you wouldn't expect to produce anything in an input.

"},{"location":"guide/input/#aliases","title":"aliases","text":"

Some keys or combinations of keys can produce the same event. For instance, the Tab key is indistinguishable from Ctrl+I in the terminal. For such keys, Textual events will contain a list of the possible keys that may have produced this event. In the case of Tab, the aliases attribute will contain [\"tab\", \"ctrl+i\"]

"},{"location":"guide/input/#key-methods","title":"Key methods","text":"

Textual offers a convenient way of handling specific keys. If you create a method beginning with key_ followed by the key name (the event's name attribute), then that method will be called in response to the key press.

Let's add a key method to the example code.

key02.py
from textual import events\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import RichLog\nclass InputApp(App):\n\"\"\"App to display key events.\"\"\"\ndef compose(self) -> ComposeResult:\nyield RichLog()\ndef on_key(self, event: events.Key) -> None:\nself.query_one(RichLog).write(event)\ndef key_space(self) -> None:\nself.bell()\nif __name__ == \"__main__\":\napp = InputApp()\napp.run()\n

Note the addition of a key_space method which is called in response to the space key, and plays the terminal bell noise.

Note

Consider key methods to be a convenience for experimenting with Textual features. In nearly all cases, key bindings and actions are preferable.

"},{"location":"guide/input/#input-focus","title":"Input focus","text":"

Only a single widget may receive key events at a time. The widget which is actively receiving key events is said to have input focus.

The following example shows how focus works in practice.

key03.pykey03.tcssOutput key03.py
from textual import events\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import RichLog\nclass KeyLogger(RichLog):\ndef on_key(self, event: events.Key) -> None:\nself.write(event)\nclass InputApp(App):\n\"\"\"App to display key events.\"\"\"\nCSS_PATH = \"key03.tcss\"\ndef compose(self) -> ComposeResult:\nyield KeyLogger()\nyield KeyLogger()\nyield KeyLogger()\nyield KeyLogger()\nif __name__ == \"__main__\":\napp = InputApp()\napp.run()\n
key03.tcss
Screen {\nlayout: grid;\ngrid-size: 2 2;\ngrid-columns: 1fr;\n}\nKeyLogger {\nborder: blank;\n}\nKeyLogger:hover {\nborder: wide $secondary;\n}\nKeyLogger:focus {\nborder: wide $accent;\n}\n

InputApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 name='o',\u258echaracter='d',\u258a is_printable=True\u258ename='d',\u258a )\u258eis_printable=True\u258a Key(\u258e)\u258a key='tab',\u258eKey(\u258a character='\\t',\u258ekey='exclamation_mark',\u258a name='tab',\u258echaracter='!',\u258a is_printable=False,\u2586\u2586\u258ename='exclamation_mark',\u2587\u2587\u258a aliases=['tab',\u00a0'ctrl+i']\u258eis_printable=True\u258a )\u258e)\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

The app splits the screen in to quarters, with a RichLog widget in each quarter. If you click any of the text logs, you should see that it is highlighted to show that the widget has focus. Key events will be sent to the focused widget only.

Tip

the :focus CSS pseudo-selector can be used to apply a style to the focused widget.

You can move focus by pressing the Tab key to focus the next widget. Pressing Shift+Tab moves the focus in the opposite direction.

"},{"location":"guide/input/#controlling-focus","title":"Controlling focus","text":"

Textual will handle keyboard focus automatically, but you can tell Textual to focus a widget by calling the widget's focus() method.

"},{"location":"guide/input/#focus-events","title":"Focus events","text":"

When a widget receives focus, it is sent a Focus event. When a widget loses focus it is sent a Blur event.

"},{"location":"guide/input/#bindings","title":"Bindings","text":"

Keys may be associated with actions for a given widget. This association is known as a key binding.

To create bindings, add a BINDINGS class variable to your app or widget. This should be a list of tuples of three strings. The first value is the key, the second is the action, the third value is a short human readable description.

The following example binds the keys R, G, and B to an action which adds a bar widget to the screen.

binding01.pybinding01.tcssOutput binding01.py
from textual.app import App, ComposeResult\nfrom textual.color import Color\nfrom textual.widgets import Footer, Static\nclass Bar(Static):\npass\nclass BindingApp(App):\nCSS_PATH = \"binding01.tcss\"\nBINDINGS = [\n(\"r\", \"add_bar('red')\", \"Add Red\"),\n(\"g\", \"add_bar('green')\", \"Add Green\"),\n(\"b\", \"add_bar('blue')\", \"Add Blue\"),\n]\ndef compose(self) -> ComposeResult:\nyield Footer()\ndef action_add_bar(self, color: str) -> None:\nbar = Bar(color)\nbar.styles.background = Color.parse(color).with_alpha(0.5)\nself.mount(bar)\nself.call_after_refresh(self.screen.scroll_end, animate=False)\nif __name__ == \"__main__\":\napp = BindingApp()\napp.run()\n
binding01.tcss
Bar {\nheight: 5;\ncontent-align: center middle;\ntext-style: bold;\nmargin: 1 2;\ncolor: $text;\n}\n

BindingApp red\u2582\u2582 green blue blue \u00a0R\u00a0\u00a0Add\u00a0Red\u00a0\u00a0G\u00a0\u00a0Add\u00a0Green\u00a0\u00a0B\u00a0\u00a0Add\u00a0Blue\u00a0

Note how the footer displays bindings and makes them clickable.

Tip

Multiple keys can be bound to a single action by comma-separating them. For example, (\"r,t\", \"add_bar('red')\", \"Add Red\") means both R and T are bound to add_bar('red').

"},{"location":"guide/input/#binding-class","title":"Binding class","text":"

The tuple of three strings may be enough for simple bindings, but you can also replace the tuple with a Binding instance which exposes a few more options.

"},{"location":"guide/input/#priority-bindings","title":"Priority bindings","text":"

Individual bindings may be marked as a priority, which means they will be checked prior to the bindings of the focused widget. This feature is often used to create hot-keys on the app or screen. Such bindings can not be disabled by binding the same key on a widget.

You can create priority key bindings by setting priority=True on the Binding object. Textual uses this feature to add a default binding for Ctrl+C so there is always a way to exit the app. Here's the bindings from the App base class. Note the first binding is set as a priority:

    BINDINGS = [\nBinding(\"ctrl+c\", \"quit\", \"Quit\", show=False, priority=True),\nBinding(\"tab\", \"focus_next\", \"Focus Next\", show=False),\nBinding(\"shift+tab\", \"focus_previous\", \"Focus Previous\", show=False),\n]\n
"},{"location":"guide/input/#show-bindings","title":"Show bindings","text":"

The footer widget can inspect bindings to display available keys. If you don't want a binding to display in the footer you can set show=False. The default bindings on App do this so that the standard Ctrl+C, Tab and Shift+Tab bindings don't typically appear in the footer.

"},{"location":"guide/input/#mouse-input","title":"Mouse Input","text":"

Textual will send events in response to mouse movement and mouse clicks. These events contain the coordinates of the mouse cursor relative to the terminal or widget.

Information

The trackpad (and possibly other pointer devices) are treated the same as the mouse in terminals.

Terminal coordinates are given by a pair values named x and y. The X coordinate is an offset in characters, extending from the left to the right of the screen. The Y coordinate is an offset in lines, extending from the top of the screen to the bottom.

Coordinates may be relative to the screen, so (0, 0) would be the top left of the screen. Coordinates may also be relative to a widget, where (0, 0) would be the top left of the widget itself.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1ba0/bSFx1MDAxNP3eX1x1MDAxMaVfdqUynfej0mpcdTAwMDVcdTAwMDGaQHktXHUwMDAxXG6rVeUmTmxw7GA7XHUwMDA0qPjve+1A7DxcdKHJhlXzXHUwMDAxyIxcdTAwMWbXM+ecOfeO+fGuUCjGd227+KlQtG9rlufWQ6tb/JC039hh5Fx1MDAwNj500fR7XHUwMDE0dMJaeqRcdTAwMTPH7ejTx48tK7yy47Zn1Wx040ZcdTAwMWTLi+JO3VxyUC1ofXRju1x1MDAxNf2Z/Ny3WvZcdTAwMWbtoFWPQ5TdZM2uu3FcdTAwMTD27mV7dsv241xirv43fC9cdTAwMTR+pD9z0YV2Lbb8pmenJ6RdWYBcdTAwMDRcdTAwMTM63LxcdTAwMWb4abREcIO1xFxc9I9wo024YWzXobtcdTAwMDFB21lP0lRcXN/ddcqh3GlFe7dcdTAwMDd1p4nXP5Nqdt+G63nH8Z2XxlVcdTAwMGKDKFpzrLjmZEdEcVx1MDAxOFxc2WduPXaehi/X3j83XG5gKLKzwqDTdHw7SkaB9FuDtlVz47v0KXG/tTdcdTAwMTSfXG5Zyy18Y1xcIS1cdTAwMTVcdTAwMTFEsuShs0dOzqdKIK2l4ZphTnVuQHpxlVx1MDAwMlx1MDAwZuZcdTAwMDPieo/TT1x1MDAxNtl3q3bVhPD8ev+YOLT8qG2FMGvZcd3HJ1x1MDAxNpwjySjjXGZcdTAwMGJCSHYjx3abTpxEKjHSXHUwMDA0XHUwMDFiziVjgmgqs2DsdGKMVIJjzni/I4mgXamnIPlneExcdTAwMWQrbD+OXTFKvuSiT1x1MDAwMt/KISw7udOuWz1cdTAwMWNcdTAwMTApOVFKJSGpfr/n+lfQ6Xc8L2tcdTAwMGJqV1x1MDAxOXTS1odcdTAwMGZzgFZRPFx0s1RcdTAwMWKYKMrUzJC9pXW+tS3Pr02zVbnc2zrnNz6ZXHUwMDAw2SHY/ZdgXHUwMDE1XHUwMDE4XHUwMDBiLFx1MDAxODbKaDlcdTAwMDJWhinBXG5LSoyii0QrQ5hIQbVQRFx1MDAxOaVH4Uo1klx1MDAxMjNcdTAwMGU6QiVcdTAwMDNoj8BVSmNcYjVcdTAwMDS/Ybjanue2o7FglUJMXHUwMDAyq1x1MDAxMdhcYi3MzFg9XzPfhFPdPmp2tXdcdTAwMTFEe/5e4/M8WCXLw6owiFx1MDAxOclcdTAwMTnAQ1x1MDAxYWL0IFa1QEJcdTAwMDEoXHUwMDE4pVx1MDAxOGuB+Wuw+r5hXHQq6ChOXHRDXHUwMDFjUyqMpPCLa81HgUooXHUwMDEyXHUwMDAwXHJcdTAwMDOaiiljNFx1MDAwN45HoFxuiFx1MDAxNEiVQ/D/XG6ocKOJToBcdTAwMTgjYU0hbGaofnXx93XiXHUwMDFj8Z3908PWXHUwMDA2/evM7norXHUwMDBlVS1cdTAwMTGjilx1MDAxOSap1ppcdTAwMTA1hFWOXHUwMDAwXHUwMDFjSmiDXHUwMDA11yQnu/Nh9TtI+KKwSlx1MDAxOSxcZkyI/6moKskmYlx1MDAxNVx1MDAxNlx1MDAxYWpcdTAwMTjFs2M12rwpy3W3s3dcdTAwMWWas9qFiNbL5Hi1scpJXHUwMDAyRlx1MDAwNoNONFx1MDAwMJaTIagypDBcdTAwMDYzZFx1MDAxOKgue5VcdTAwMDN4T+h38FSLQiphXHSjXHUwMDE4e8tItcIw6I5Nr9jExZ8rpjCTuVx1MDAwNfE5mKqD+0atpiW/3qVhyexcdTAwMTFIXHUwMDAzK1x1MDAxM2DqWDWnXHUwMDEz2v89UJlUSEkh6WBGxVx1MDAxOEFcdTAwMTgyLblAd4pcdTAwMTFVXG4yKTUmi5JitPNcdJDwUMJoMUf6lFx1MDAwNjcnILlcdTAwMDBcdTAwMWL9XHUwMDAyQObisMJ4w/Xrrt9cdTAwMWM+xfbrWc9cdTAwMTNsXHUwMDBi/apBpecqO9s7+5ub3Vx1MDAxM6dy24lOon1cdTAwMTmfZrhKkFx1MDAxNdQ6UTqghFx1MDAxOUHBrFx1MDAwM+UlOFx1MDAwMpI7qGm1XHUwMDEzVCNB01F97HjIoreiuFx1MDAxNLRablxmz31cdTAwMTi4fjxcdTAwMWNs+iDrXHSVXHUwMDFj26qPeZR83zDn2slcdTAwMTWzKkjyyf4qZKBMv/T//ufD2KPXRqGTfHKgya7wLv/7xVx1MDAwMqHkcGM/k4XEXG6QSNTsXHUwMDAyXHUwMDEx3G59bVxcnljd06tSuXFz0vWv/7pYfYGAXGZcdTAwMTFWMTUkXHUwMDEw1CDJsVx1MDAwNpVkXHUwMDFhw2rOhlwi+olZrEGQepixOkFcdTAwMTDBZEC9nlRCc2a4WrZMXHUwMDE4pvNcdP0yZeLwRl+FR+t39dYhO7mp4jiu7tTHy1x1MDAwNCZcdTAwMTTUjIO6q0RLiaa5w3pCQTCSvZF900oxip3ks9aHzVx1MDAwYnVcIrZv43EykUPZkExcYkGYJHmj/5xKTJ/HXHUwMDE1VVx0zjT43Vx1MDAwMY6mKkFcdTAwMDTSSi/WR+Sy3qysNSpcYlx1MDAwMGdcIlx1MDAxOPD051x1MDAxYtk+in7kQDaT6Fx1MDAwZqCrR4R+z8NcdTAwMTMkp7lcdTAwMTLKSTZjL5CbRuDHx+59byVcdTAwMWJo3bZarnc3gIRcdTAwMTT2SdEgP0mRXHK3SzM6PXDguuc2XHUwMDEzTlx1MDAxND27MUiW2K1ZXr87XHUwMDBlckNag1x1MDAxYltwubAyXCJcdTAwMTdB6DZd3/Kq/SDmoqiavI+iXHLVIIM4O+LZQt9US7aiXHUwMDFjXHUwMDA1XHUwMDFkQorgXHUwMDExknKMXHUwMDExJKl60Gz/bJJmsUwjqUnqO1xc5EzVUkg6PXVcdTAwMWLA1zwknTd1mIukd6tA0rvpJJ26fcTYRNMtlZGJY8mW2+eYKr/qzeq5+ra5xaPTXHUwMDFhoSdl91x1MDAxMs/H1OVtIFx0IdBwRs45RYxyOuDE5yps1pVNOOejXHUwMDFjZYlcdTAwMTCMddnSoLEuW1HCtFFkuVuZWsFgyFx1MDAxN1x1MDAxMGoqXHUwMDE2J+Z+Uk0sXHUwMDBlJU5CQs7D5cxA1MI76HSFdqtB6aLEq3LvXHUwMDFiu1xc9SVcdTAwMDOEXHUwMDE4XHQ96uu4ZFxic2rkK8G4iPpQsrPKwOjllvNlZH5cdTAwMWFzpfBcdTAwMGJA+fMyv/tNZ9uznS1TvdhVpVp5o0qP4tzq9atA9PhZQIFIYjXc2lx1MDAxN1x0WMhcYoXRnl0kqvbpl8P7vc+4XHUwMDFhhpVyffdr4/42Wn2RMIhcdTAwMWI6Ulwi4onfZJpqsshXXHUwMDFj5ilcdTAwMGVRXGYzwzWZx2a+TYnoVlx1MDAwZU7Lbjlyb732l42Se3h3fXw1oTiEXHUwMDA12Fx1MDAwM5gzpmBhXHUwMDE3ODd7hV/VodzDzpx6MjptN5Qkb4O9IPecPpUrqlx1MDAxMZKDg1SDfqHnalx1MDAxNdJcdTAwMGKWiNnqQzrZc9RCLcDK9mE0JvOcrvhcdTAwMDPwennmXHSCkzexv8pDUzia26JcdTAwMWbmqOGgXHUwMDExlJDZs87pjmxFOSpcdTAwMTRF4JuH8k7BKFqF0pDSsCwl3nS5/JyetlxyQGvl+flcdTAwMTYqQ5P4SfFEfjLNXHUwMDA1y7+g8lx1MDAxYztcdTAwMWT7vGxFYSm4a1xcXHUwMDFjXHUwMDFkXHUwMDE4vGNcImfl92HpSFxyJt1gwVx1MDAwNqnXloSeMdizsFx1MDAxM1wiXHUwMDEz4PfZkl++1FxmgzYtjUC/4Vx1MDAwZlx1MDAwNfz7KrDoMZK5qKRy7zRcctdXXHUwMDE5JpqJXHUwMDE3uNHKmXW4WflyXHUwMDE4XHUwMDFknYlcbrlukMvd49aqc4lrgyC/XHUwMDE53JZM7SjTi99cbpmRUZxcdTAwMWLMtDHLffFOU54vr/9iVGFcdTAwMTbzOHFt4jR5I1x1MDAwNM9eXHUwMDAxKlx1MDAxMa5cdTAwMGVD3N611i/3jsXJie9/2171/VxuqSmSckx6J4hChL9293/KloVcdTAwMWPzTutcdTAwMTguKYqTfzpcdTAwMTJcdTAwMGLYVpzGJVx1MDAwM7dd3upcdTAwMDTz3rTjVeDSYyQ9Lr17tMBFq90+jmGEik9lKphcdTAwMDS3/viY2fWKN67d3Vx1MDAxOIeC9JNcXDXlZ8JcdTAwMDU7mYJcdTAwMWZcdTAwMGbvXHUwMDFl/lx1MDAwNeEmVVx1MDAxOCJ9 XyXy(0, 0)(0, 0)Widget"},{"location":"guide/input/#mouse-movements","title":"Mouse movements","text":"

When you move the mouse cursor over a widget it will receive MouseMove events which contain the coordinate of the mouse and information about what modifier keys (Ctrl, Shift etc) are held down.

The following example shows mouse movements being used to attach a widget to the mouse cursor.

mouse01.pymouse01.tcss mouse01.py
from textual import events\nfrom textual.app import App, ComposeResult\nfrom textual.containers import Container\nfrom textual.widgets import RichLog, Static\nclass PlayArea(Container):\ndef on_mount(self) -> None:\nself.capture_mouse()\ndef on_mouse_move(self, event: events.MouseMove) -> None:\nself.screen.query_one(RichLog).write(event)\nself.query_one(Ball).offset = event.offset - (8, 2)\nclass Ball(Static):\npass\nclass MouseApp(App):\nCSS_PATH = \"mouse01.tcss\"\ndef compose(self) -> ComposeResult:\nyield RichLog()\nyield PlayArea(Ball(\"Textual\"))\nif __name__ == \"__main__\":\napp = MouseApp()\napp.run()\n
mouse01.tcss
Screen {\nlayers: log ball;\n}\nTextLog {\nlayer: log;\n}\nPlayArea {\nopacity: 0%;\nlayer: ball;\n}\nBall {\nlayer: ball;\nwidth: auto;\nheight: 1;\nbackground: $secondary;\nborder: tall $secondary;\ncolor: $background;\nbox-sizing: content-box;\ntext-style: bold;\npadding: 0 4;\n}\n

If you run mouse01.py you should find that it logs the mouse move event, and keeps a widget pinned directly under the cursor.

The on_mouse_move handler sets the offset style of the ball (a rectangular one) to match the mouse coordinates.

"},{"location":"guide/input/#mouse-capture","title":"Mouse capture","text":"

In the mouse01.py example there was a call to capture_mouse() in the mount handler. Textual will send mouse move events to the widget directly under the cursor. You can tell Textual to send all mouse events to a widget regardless of the position of the mouse cursor by calling capture_mouse.

Call release_mouse to restore the default behavior.

Warning

If you capture the mouse, be aware you might get negative mouse coordinates if the cursor is to the left of the widget.

Textual will send a MouseCapture event when the mouse is captured, and a MouseRelease event when it is released.

"},{"location":"guide/input/#enter-and-leave-events","title":"Enter and Leave events","text":"

Textual will send a Enter event to a widget when the mouse cursor first moves over it, and a Leave event when the cursor moves off a widget.

"},{"location":"guide/input/#click-events","title":"Click events","text":"

There are three events associated with clicking a button on your mouse. When the button is initially pressed, Textual sends a MouseDown event, followed by MouseUp when the button is released. Textual then sends a final Click event.

If you want your app to respond to a mouse click you should prefer the Click event (and not MouseDown or MouseUp). This is because a future version of Textual may support other pointing devices which don't have up and down states.

"},{"location":"guide/input/#scroll-events","title":"Scroll events","text":"

Most mice have a scroll wheel which you can use to scroll the window underneath the cursor. Scrollable containers in Textual will handle these automatically, but you can handle MouseScrollDown and MouseScrollUp if you want build your own scrolling functionality.

Information

Terminal emulators will typically convert trackpad gestures in to scroll events.

"},{"location":"guide/layout/","title":"Layout","text":"

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.

"},{"location":"guide/layout/#vertical","title":"Vertical","text":"

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.

Outputvertical_layout.pyvertical_layout.tcss

VerticalLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Two\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Three\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass VerticalLayoutExample(App):\nCSS_PATH = \"vertical_layout.tcss\"\ndef compose(self) -> ComposeResult:\nyield Static(\"One\", classes=\"box\")\nyield Static(\"Two\", classes=\"box\")\nyield Static(\"Three\", classes=\"box\")\nif __name__ == \"__main__\":\napp = VerticalLayoutExample()\napp.run()\n
Screen {\nlayout: vertical;\n}\n.box {\nheight: 1fr;\nborder: solid green;\n}\n

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.tcss, 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\"\n

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 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2582\u2582 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Two\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502

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

"},{"location":"guide/layout/#horizontal","title":"Horizontal","text":"

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.

Outputhorizontal_layout.pyhorizontal_layout.tcss

HorizontalLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502\u2502Two\u2502\u2502Three\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass HorizontalLayoutExample(App):\nCSS_PATH = \"horizontal_layout.tcss\"\ndef compose(self) -> ComposeResult:\nyield Static(\"One\", classes=\"box\")\nyield Static(\"Two\", classes=\"box\")\nyield Static(\"Three\", classes=\"box\")\nif __name__ == \"__main__\":\napp = HorizontalLayoutExample()\napp.run()\n
Screen {\nlayout: horizontal;\n}\n.box {\nheight: 100%;\nwidth: 1fr;\nborder: solid green;\n}\n

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:

Outputhorizontal_layout_overflow.pyhorizontal_layout_overflow.tcss

HorizontalLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u258a

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass HorizontalLayoutExample(App):\nCSS_PATH = \"horizontal_layout_overflow.tcss\"\ndef compose(self) -> ComposeResult:\nyield Static(\"One\", classes=\"box\")\nyield Static(\"Two\", classes=\"box\")\nyield Static(\"Three\", classes=\"box\")\nif __name__ == \"__main__\":\napp = HorizontalLayoutExample()\napp.run()\n
Screen {\nlayout: horizontal;\noverflow-x: auto;\n}\n.box {\nheight: 100%;\nborder: solid green;\n}\n

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.

"},{"location":"guide/layout/#utility-containers","title":"Utility containers","text":"

Textual comes with several \"container\" widgets. Among them, we have 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.

Outpututility_containers.pyutility_containers.tcss

UtilityContainersExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502One\u2502\u2502Three\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502Two\u2502\u2502Four\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal, Vertical\nfrom textual.widgets import Static\nclass UtilityContainersExample(App):\nCSS_PATH = \"utility_containers.tcss\"\ndef compose(self) -> ComposeResult:\nyield Horizontal(\nVertical(\nStatic(\"One\"),\nStatic(\"Two\"),\nclasses=\"column\",\n),\nVertical(\nStatic(\"Three\"),\nStatic(\"Four\"),\nclasses=\"column\",\n),\n)\nif __name__ == \"__main__\":\napp = UtilityContainersExample()\napp.run()\n
Static {\ncontent-align: center middle;\nbackground: crimson;\nborder: solid darkred;\nheight: 1fr;\n}\n.column {\nwidth: 1fr;\n}\n

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

"},{"location":"guide/layout/#composing-with-context-managers","title":"Composing with context managers","text":"

In the previous section, we've shown how you add children to a container (such as Horizontal and Vertical) using positional arguments. It's fine to do it this way, but Textual offers a simplified syntax using context managers, which is generally easier to write and edit.

When composing a widget, you can introduce a container using Python's with statement. Any widgets yielded within that block are added as a child of the container.

Let's update the utility containers example to use the context manager approach.

utility_containers_using_with.pyutility_containers.pyutility_containers.tcssOutput

Note

This code uses context managers to compose widgets.

from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal, Vertical\nfrom textual.widgets import Static\nclass UtilityContainersExample(App):\nCSS_PATH = \"utility_containers.tcss\"\ndef compose(self) -> ComposeResult:\nwith Horizontal():\nwith Vertical(classes=\"column\"):\nyield Static(\"One\")\nyield Static(\"Two\")\nwith Vertical(classes=\"column\"):\nyield Static(\"Three\")\nyield Static(\"Four\")\nif __name__ == \"__main__\":\napp = UtilityContainersExample()\napp.run()\n

Note

This is the original code using positional arguments.

from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal, Vertical\nfrom textual.widgets import Static\nclass UtilityContainersExample(App):\nCSS_PATH = \"utility_containers.tcss\"\ndef compose(self) -> ComposeResult:\nyield Horizontal(\nVertical(\nStatic(\"One\"),\nStatic(\"Two\"),\nclasses=\"column\",\n),\nVertical(\nStatic(\"Three\"),\nStatic(\"Four\"),\nclasses=\"column\",\n),\n)\nif __name__ == \"__main__\":\napp = UtilityContainersExample()\napp.run()\n
Static {\ncontent-align: center middle;\nbackground: crimson;\nborder: solid darkred;\nheight: 1fr;\n}\n.column {\nwidth: 1fr;\n}\n

UtilityContainersExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502One\u2502\u2502Three\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502Two\u2502\u2502Four\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

Note how the end result is the same, but the code with context managers is a little easier to read. It is up to you which method you want to use, and you can mix context managers with positional arguments if you like!

"},{"location":"guide/layout/#grid","title":"Grid","text":"

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

Outputgrid_layout1.pygrid_layout1.tcss

GridLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502\u2502Two\u2502\u2502Three\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Four\u2502\u2502Five\u2502\u2502Six\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass GridLayoutExample(App):\nCSS_PATH = \"grid_layout1.tcss\"\ndef compose(self) -> ComposeResult:\nyield Static(\"One\", classes=\"box\")\nyield Static(\"Two\", classes=\"box\")\nyield Static(\"Three\", classes=\"box\")\nyield Static(\"Four\", classes=\"box\")\nyield Static(\"Five\", classes=\"box\")\nyield Static(\"Six\", classes=\"box\")\nif __name__ == \"__main__\":\napp = GridLayoutExample()\napp.run()\n
Screen {\nlayout: grid;\ngrid-size: 3 2;\n}\n.box {\nheight: 100%;\nborder: solid green;\n}\n

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:

Outputgrid_layout2.pygrid_layout2.tcss

GridLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502\u2502Two\u2502\u2502Three\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Four\u2502\u2502Five\u2502\u2502Six\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Seven\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass GridLayoutExample(App):\nCSS_PATH = \"grid_layout2.tcss\"\ndef compose(self) -> ComposeResult:\nyield Static(\"One\", classes=\"box\")\nyield Static(\"Two\", classes=\"box\")\nyield Static(\"Three\", classes=\"box\")\nyield Static(\"Four\", classes=\"box\")\nyield Static(\"Five\", classes=\"box\")\nyield Static(\"Six\", classes=\"box\")\nyield Static(\"Seven\", classes=\"box\")\nif __name__ == \"__main__\":\napp = GridLayoutExample()\napp.run()\n
Screen {\nlayout: grid;\ngrid-size: 3;\n}\n.box {\nheight: 100%;\nborder: solid green;\n}\n

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.

"},{"location":"guide/layout/#row-and-column-sizes","title":"Row and column sizes","text":"

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.

Outputgrid_layout3_row_col_adjust.pygrid_layout3_row_col_adjust.tcss

GridLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502\u2502Two\u2502\u2502Three\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Four\u2502\u2502Five\u2502\u2502Six\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass GridLayoutExample(App):\nCSS_PATH = \"grid_layout3_row_col_adjust.tcss\"\ndef compose(self) -> ComposeResult:\nyield Static(\"One\", classes=\"box\")\nyield Static(\"Two\", classes=\"box\")\nyield Static(\"Three\", classes=\"box\")\nyield Static(\"Four\", classes=\"box\")\nyield Static(\"Five\", classes=\"box\")\nyield Static(\"Six\", classes=\"box\")\nif __name__ == \"__main__\":\napp = GridLayoutExample()\napp.run()\n
Screen {\nlayout: grid;\ngrid-size: 3;\ngrid-columns: 2fr 1fr 1fr;\n}\n.box {\nheight: 100%;\nborder: solid green;\n}\n

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

Outputgrid_layout4_row_col_adjust.pygrid_layout4_row_col_adjust.tcss

GridLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502\u2502Two\u2502\u2502Three\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Four\u2502\u2502Five\u2502\u2502Six\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass GridLayoutExample(App):\nCSS_PATH = \"grid_layout4_row_col_adjust.tcss\"\ndef compose(self) -> ComposeResult:\nyield Static(\"One\", classes=\"box\")\nyield Static(\"Two\", classes=\"box\")\nyield Static(\"Three\", classes=\"box\")\nyield Static(\"Four\", classes=\"box\")\nyield Static(\"Five\", classes=\"box\")\nyield Static(\"Six\", classes=\"box\")\nif __name__ == \"__main__\":\napp = GridLayoutExample()\napp.run()\n
Screen {\nlayout: grid;\ngrid-size: 3;\ngrid-columns: 2fr 1fr 1fr;\ngrid-rows: 25% 75%;\n}\n.box {\nheight: 100%;\nborder: solid green;\n}\n

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

"},{"location":"guide/layout/#auto-rows-columns","title":"Auto rows / columns","text":"

The grid-columns and grid-rows rules can both accept a value of \"auto\" in place of any of the dimensions, which tells Textual to calculate an optimal size based on the content.

Let's modify the previous example to make the first column an auto column.

Outputgrid_layout_auto.pygrid_layout_auto.tcss

GridLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502First\u00a0column\u2502\u2502Two\u2502\u2502Three\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Four\u2502\u2502Five\u2502\u2502Six\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass GridLayoutExample(App):\nCSS_PATH = \"grid_layout_auto.tcss\"\ndef compose(self) -> ComposeResult:\nyield Static(\"First column\", classes=\"box\")\nyield Static(\"Two\", classes=\"box\")\nyield Static(\"Three\", classes=\"box\")\nyield Static(\"Four\", classes=\"box\")\nyield Static(\"Five\", classes=\"box\")\nyield Static(\"Six\", classes=\"box\")\nif __name__ == \"__main__\":\napp = GridLayoutExample()\napp.run()\n
Screen {\nlayout: grid;\ngrid-size: 3;\ngrid-columns: auto 1fr 1fr;\ngrid-rows: 25% 75%;\n}\n.box {\nheight: 100%;\nborder: solid green;\n}\n

Notice how the first column is just wide enough to fit the content of each cell. The layout will adjust accordingly if you update the content for any widget in that column.

"},{"location":"guide/layout/#cell-spans","title":"Cell spans","text":"

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.

Outputgrid_layout5_col_span.pygrid_layout5_col_span.tcss

GridLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502\u2502Two\u00a0(column-span:\u00a02)\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Three\u2502\u2502Four\u2502\u2502Five\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Six\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass GridLayoutExample(App):\nCSS_PATH = \"grid_layout5_col_span.tcss\"\ndef compose(self) -> ComposeResult:\nyield Static(\"One\", classes=\"box\")\nyield Static(\"Two [b](column-span: 2)\", classes=\"box\", id=\"two\")\nyield Static(\"Three\", classes=\"box\")\nyield Static(\"Four\", classes=\"box\")\nyield Static(\"Five\", classes=\"box\")\nyield Static(\"Six\", classes=\"box\")\nif __name__ == \"__main__\":\napp = GridLayoutExample()\napp.run()\n
Screen {\nlayout: grid;\ngrid-size: 3;\n}\n#two {\ncolumn-span: 2;\ntint: magenta 40%;\n}\n.box {\nheight: 100%;\nborder: solid green;\n}\n

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.

Outputgrid_layout6_row_span.pygrid_layout6_row_span.tcss

GridLayoutExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502One\u2502\u2502Two\u00a0(column-span:\u00a02\u00a0and\u00a0row-span:\u00a02)\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2502\u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u2502\u2502 \u2502Three\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502Four\u2502\u2502Five\u2502\u2502Six\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass GridLayoutExample(App):\nCSS_PATH = \"grid_layout6_row_span.tcss\"\ndef compose(self) -> ComposeResult:\nyield Static(\"One\", classes=\"box\")\nyield Static(\"Two [b](column-span: 2 and row-span: 2)\", classes=\"box\", id=\"two\")\nyield Static(\"Three\", classes=\"box\")\nyield Static(\"Four\", classes=\"box\")\nyield Static(\"Five\", classes=\"box\")\nyield Static(\"Six\", classes=\"box\")\napp = GridLayoutExample()\nif __name__ == \"__main__\":\napp.run()\n
Screen {\nlayout: grid;\ngrid-size: 3;\n}\n#two {\ncolumn-span: 2;\nrow-span: 2;\ntint: magenta 40%;\n}\n.box {\nheight: 100%;\nborder: solid green;\n}\n

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.

"},{"location":"guide/layout/#gutter","title":"Gutter","text":"

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.

Outputgrid_layout7_gutter.pygrid_layout7_gutter.tcss

GridLayoutExample OneTwoThree FourFiveSix

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass GridLayoutExample(App):\nCSS_PATH = \"grid_layout7_gutter.tcss\"\ndef compose(self) -> ComposeResult:\nyield Static(\"One\", classes=\"box\")\nyield Static(\"Two\", classes=\"box\")\nyield Static(\"Three\", classes=\"box\")\nyield Static(\"Four\", classes=\"box\")\nyield Static(\"Five\", classes=\"box\")\nyield Static(\"Six\", classes=\"box\")\nif __name__ == \"__main__\":\napp = GridLayoutExample()\napp.run()\n
Screen {\nlayout: grid;\ngrid-size: 3;\ngrid-gutter: 1;\nbackground: lightgreen;\n}\n.box {\nbackground: darkmagenta;\nheight: 100%;\n}\n

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.

"},{"location":"guide/layout/#docking","title":"Docking","text":"

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.

Outputdock_layout1_sidebar.pydock_layout1_sidebar.tcss

DockLayoutExample SidebarDocking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0 edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0making\u00a0them\u00a0ideal\u00a0 for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0\u2587\u2587 edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0making\u00a0them\u00a0ideal\u00a0 for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0 edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0making\u00a0them\u00a0ideal\u00a0 for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0 edges\u00a0of\u00a0a\u00a0container.

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nTEXT = \"\"\"\\\nDocking a widget removes it from the layout and fixes its position, aligned to either the top, right, bottom, or left edges of a container.\nDocked widgets will not scroll out of view, making them ideal for sticky headers, footers, and sidebars.\n\"\"\"\nclass DockLayoutExample(App):\nCSS_PATH = \"dock_layout1_sidebar.tcss\"\ndef compose(self) -> ComposeResult:\nyield Static(\"Sidebar\", id=\"sidebar\")\nyield Static(TEXT * 10, id=\"body\")\nif __name__ == \"__main__\":\napp = DockLayoutExample()\napp.run()\n
#sidebar {\ndock: left;\nwidth: 15;\nheight: 100%;\ncolor: #0f2b41;\nbackground: dodgerblue;\n}\n

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.

Outputdock_layout2_sidebar.pydock_layout2_sidebar.tcss

DockLayoutExample Sidebar1Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0 fixes\u00a0its\u00a0position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0 right,\u00a0bottom,\u00a0or\u00a0left\u00a0edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0 making\u00a0them\u00a0ideal\u00a0for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0 and\u00a0sidebars. \u2587\u2587 Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0 fixes\u00a0its\u00a0position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0 right,\u00a0bottom,\u00a0or\u00a0left\u00a0edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0 making\u00a0them\u00a0ideal\u00a0for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0 and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0 fixes\u00a0its\u00a0position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0 right,\u00a0bottom,\u00a0or\u00a0left\u00a0edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0 making\u00a0them\u00a0ideal\u00a0for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0 and\u00a0sidebars.

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nTEXT = \"\"\"\\\nDocking a widget removes it from the layout and fixes its position, aligned to either the top, right, bottom, or left edges of a container.\nDocked widgets will not scroll out of view, making them ideal for sticky headers, footers, and sidebars.\n\"\"\"\nclass DockLayoutExample(App):\nCSS_PATH = \"dock_layout2_sidebar.tcss\"\ndef compose(self) -> ComposeResult:\nyield Static(\"Sidebar2\", id=\"another-sidebar\")\nyield Static(\"Sidebar1\", id=\"sidebar\")\nyield Static(TEXT * 10, id=\"body\")\napp = DockLayoutExample()\nif __name__ == \"__main__\":\napp.run()\n
#another-sidebar {\ndock: left;\nwidth: 30;\nheight: 100%;\nbackground: deeppink;\n}\n#sidebar {\ndock: left;\nwidth: 15;\nheight: 100%;\ncolor: #0f2b41;\nbackground: dodgerblue;\n}\n

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.

Outputdock_layout3_sidebar_header.pydock_layout3_sidebar_header.tcss

DockLayoutExample Sidebar1DockLayoutExample Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0 edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0making\u00a0them\u00a0ideal\u00a0 for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0 edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0making\u00a0them\u00a0ideal\u00a0 for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0 edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0making\u00a0them\u00a0ideal\u00a0 for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0

from textual.app import App, ComposeResult\nfrom textual.widgets import Header, Static\nTEXT = \"\"\"\\\nDocking a widget removes it from the layout and fixes its position, aligned to either the top, right, bottom, or left edges of a container.\nDocked widgets will not scroll out of view, making them ideal for sticky headers, footers, and sidebars.\n\"\"\"\nclass DockLayoutExample(App):\nCSS_PATH = \"dock_layout3_sidebar_header.tcss\"\ndef compose(self) -> ComposeResult:\nyield Header(id=\"header\")\nyield Static(\"Sidebar1\", id=\"sidebar\")\nyield Static(TEXT * 10, id=\"body\")\nif __name__ == \"__main__\":\napp = DockLayoutExample()\napp.run()\n
#sidebar {\ndock: left;\nwidth: 15;\nheight: 100%;\ncolor: #0f2b41;\nbackground: dodgerblue;\n}\n

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.

"},{"location":"guide/layout/#layers","title":"Layers","text":"

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

Outputlayers.pylayers.tcss

LayersExample box1\u00a0(layer\u00a0=\u00a0above) box2\u00a0(layer\u00a0=\u00a0below)

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass LayersExample(App):\nCSS_PATH = \"layers.tcss\"\ndef compose(self) -> ComposeResult:\nyield Static(\"box1 (layer = above)\", id=\"box1\")\nyield Static(\"box2 (layer = below)\", id=\"box2\")\nif __name__ == \"__main__\":\napp = LayersExample()\napp.run()\n
Screen {\nalign: center middle;\nlayers: below above;\n}\nStatic {\nwidth: 28;\nheight: 8;\ncolor: auto;\ncontent-align: center middle;\n}\n#box1 {\nlayer: above;\nbackground: darkcyan;\n}\n#box2 {\nlayer: below;\nbackground: orange;\noffset: 12 6;\n}\n
"},{"location":"guide/layout/#offsets","title":"Offsets","text":"

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.
"},{"location":"guide/layout/#putting-it-all-together","title":"Putting it all together","text":"

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.

Outputcombining_layouts.pycombining_layouts.tcss

CombiningLayoutsExample \u2b58CombiningLayoutsExample \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502HorizontallyPositionedChildrenHere\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a00\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a01\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2585\u2585\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a02\u2502\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502\u2502\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502Thispanelis\u2502 \u2502\u2502\u2502\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a03\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502usinggrid\u00a0layout!\u2502 \u2502Vertical\u00a0layout,\u00a0child\u00a04\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

from textual.app import App, ComposeResult\nfrom textual.containers import Container, Horizontal, VerticalScroll\nfrom textual.widgets import Header, Static\nclass CombiningLayoutsExample(App):\nCSS_PATH = \"combining_layouts.tcss\"\ndef compose(self) -> ComposeResult:\nyield Header()\nwith Container(id=\"app-grid\"):\nwith VerticalScroll(id=\"left-pane\"):\nfor number in range(15):\nyield Static(f\"Vertical layout, child {number}\")\nwith Horizontal(id=\"top-right\"):\nyield Static(\"Horizontally\")\nyield Static(\"Positioned\")\nyield Static(\"Children\")\nyield Static(\"Here\")\nwith Container(id=\"bottom-right\"):\nyield Static(\"This\")\nyield Static(\"panel\")\nyield Static(\"is\")\nyield Static(\"using\")\nyield Static(\"grid layout!\", id=\"bottom-right-final\")\nif __name__ == \"__main__\":\napp = CombiningLayoutsExample()\napp.run()\n
#app-grid {\nlayout: grid;\ngrid-size: 2;  /* two columns */\ngrid-columns: 1fr;\ngrid-rows: 1fr;\n}\n#left-pane > Static {\nbackground: $boost;\ncolor: auto;\nmargin-bottom: 1;\npadding: 1;\n}\n#left-pane {\nwidth: 100%;\nheight: 100%;\nrow-span: 2;\nbackground: $panel;\nborder: dodgerblue;\n}\n#top-right {\nheight: 100%;\nbackground: $panel;\nborder: mediumvioletred;\n}\n#top-right > Static {\nwidth: auto;\nheight: 100%;\nmargin-right: 1;\nbackground: $boost;\n}\n#bottom-right {\nheight: 100%;\nlayout: grid;\ngrid-size: 3;\ngrid-columns: 1fr;\ngrid-rows: 1fr;\ngrid-gutter: 1;\nbackground: $panel;\nborder: greenyellow;\n}\n#bottom-right-final {\ncolumn-span: 2;\n}\n#bottom-right > Static {\nheight: 100%;\nbackground: $boost;\n}\n

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

"},{"location":"guide/queries/","title":"DOM Queries","text":"

In the previous chapter we introduced the DOM which is how Textual apps keep track of widgets. We saw how you can apply styles to the DOM with CSS selectors.

Selectors are a very useful idea and can do more that apply styles. We can also find widgets in Python code with selectors, and make updates to widgets in a simple expressive way. Let's look at how!

"},{"location":"guide/queries/#query-one","title":"Query one","text":"

The query_one method gets a single widget in an app or other widget. If you call it with a selector it will return the first matching widget.

Let's say we have a widget with an ID of send and we want to get a reference to it in our app. We could do this with the following:

send_button = self.query_one(\"#send\")\n

If there is no widget with an ID of send, Textual will raise a NoMatches exception. Otherwise it will return the matched widget.

You can also add a second parameter for the expected type.

send_button = self.query_one(\"#send\", Button)\n

If the matched widget is not a button (i.e. if isinstance(widget, Button) equals False), Textual will raise a WrongType exception.

Tip

The second parameter allows type-checkers like MyPy to know the exact return type. Without it, MyPy would only know the result of query_one is a Widget (the base class).

"},{"location":"guide/queries/#making-queries","title":"Making queries","text":"

Apps and widgets have a query method which finds (or queries) widgets. This method returns a DOMQuery object which is a list-like container of widgets.

If you call query with no arguments, you will get back a DOMQuery containing all widgets. This method is recursive, meaning it will also return child widgets (as many levels as required).

Here's how you might iterate over all the widgets in your app:

for widget in self.query():\nprint(widget)\n

Called on the app, this will retrieve all widgets in the app. If you call the same method on a widget, it will return the children of that widget.

Note

All the query and related methods work on both App and Widget sub-classes.

"},{"location":"guide/queries/#query-selectors","title":"Query selectors","text":"

You can call query with a CSS selector. Let's look a few examples:

If we want to find all the button widgets, we could do something like the following:

for button in self.query(\"Button\"):\nprint(button)\n

Any selector that works in CSS will work with the query method. For instance, if we want to find all the disabled buttons in a Dialog widget, we could do this:

for button in self.query(\"Dialog Button.disabled\"):\nprint(button)\n

Info

The selector Dialog Button.disabled says find all the Button with a CSS class of disabled that are a child of a Dialog widget.

"},{"location":"guide/queries/#results","title":"Results","text":"

Query objects have a results method which is an alternative way of iterating over widgets. If you supply a type (i.e. a Widget class) then this method will generate only objects of that type.

The following example queries for widgets with the disabled CSS class and iterates over just the Button objects.

for button in self.query(\".disabled\").results(Button):\nprint(button)\n

Tip

This method allows type-checkers like MyPy to know the exact type of the object in the loop. Without it, MyPy would only know that button is a Widget (the base class).

"},{"location":"guide/queries/#query-objects","title":"Query objects","text":"

We've seen that the query method returns a DOMQuery object you can iterate over in a for loop. Query objects behave like Python lists and support all of the same operations (such as query[0], len(query) ,reverse(query) etc). They also have a number of other methods to simplify retrieving and modifying widgets.

"},{"location":"guide/queries/#first-and-last","title":"First and last","text":"

The first and last methods return the first or last matching widget from the selector, respectively.

Here's how we might find the last button in an app:

last_button = self.query(\"Button\").last()\n

If there are no buttons, Textual will raise a NoMatches exception. Otherwise it will return a button widget.

Both first() and last() accept an expect_type argument that should be the class of the widget you are expecting. Let's say we want to get the last widget with class .disabled, and we want to check it really is a button. We could do this:

disabled_button = self.query(\".disabled\").last(Button)\n

The query selects all widgets with a disabled CSS class. The last method gets the last disabled widget and checks it is a Button and not any other kind of widget.

If the last widget is not a button, Textual will raise a WrongType exception.

Tip

Specifying the expected type allows type-checkers like MyPy to know the exact return type.

"},{"location":"guide/queries/#filter","title":"Filter","text":"

Query objects have a filter method which further refines a query. This method will return a new query object with widgets that match both the original query and the new selector

Let's say we have a query which gets all the buttons in an app, and we want a new query object with just the disabled buttons. We could write something like this:

# Get all the Buttons\nbuttons_query = self.query(\"Button\")\n# Buttons with 'disabled' CSS class\ndisabled_buttons = buttons_query.filter(\".disabled\")\n

Iterating over disabled_buttons will give us all the disabled buttons.

"},{"location":"guide/queries/#exclude","title":"Exclude","text":"

Query objects have an exclude method which is the logical opposite of filter. The exclude method removes any widgets from the query object which match a selector.

Here's how we could get all the buttons which don't have the disabled class set.

# Get all the Buttons\nbuttons_query = self.query(\"Button\")\n# Remove all the Buttons with the 'disabled' CSS class\nenabled_buttons = buttons_query.exclude(\".disabled\")\n
"},{"location":"guide/queries/#loop-free-operations","title":"Loop-free operations","text":"

Once you have a query object, you can loop over it to call methods on the matched widgets. Query objects also support a number of methods which make an update to every matched widget without an explicit loop.

For instance, let's say we want to disable all buttons in an app. We could do this by calling add_class() on a query object.

self.query(\"Button\").add_class(\"disabled\")\n

This single line is equivalent to the following:

for widget in self.query(\"Button\"):\nwidget.add_class(\"disabled\")\n

Here are the other loop-free methods on query objects:

  • set_class Sets a CSS class (or classes) on matched widgets.
  • add_class Adds a CSS class (or classes) to matched widgets.
  • remove_class Removes a CSS class (or classes) from matched widgets.
  • toggle_class Sets a CSS class (or classes) if it is not set, or removes the class (or classes) if they are set on the matched widgets.
  • remove Removes matched widgets from the DOM.
  • refresh Refreshes matched widgets.
"},{"location":"guide/reactivity/","title":"Reactivity","text":"

Textual's reactive attributes are attributes with superpowers. In this chapter we will look at how reactive attributes can simplify your apps.

Quote

With great power comes great responsibility.

\u2014 Uncle Ben

"},{"location":"guide/reactivity/#reactive-attributes","title":"Reactive attributes","text":"

Textual provides an alternative way of adding attributes to your widget or App, which doesn't require adding them to your class constructor (__init__). To create these attributes import reactive from textual.reactive, and assign them in the class scope.

The following code illustrates how to create reactive attributes:

from textual.reactive import reactive\nfrom textual.widget import Widget\nclass Reactive(Widget):\nname = reactive(\"Paul\")  # (1)!\ncount = reactive(0) # (2)!\nis_cool = reactive(True)  # (3)!\n
  1. Create a string attribute with a default of \"Paul\"
  2. Creates an integer attribute with a default of 0.
  3. Creates a boolean attribute with a default of True.

The reactive constructor accepts a default value as the first positional argument.

Information

Textual uses Python's descriptor protocol to create reactive attributes, which is the same protocol used by the builtin property decorator.

You can get and set these attributes in the same way as if you had assigned them in an __init__ method. For instance self.name = \"Jessica\", self.count += 1, or print(self.is_cool).

"},{"location":"guide/reactivity/#dynamic-defaults","title":"Dynamic defaults","text":"

You can also set the default to a function (or other callable). Textual will call this function to get the default value. The following code illustrates a reactive value which will be automatically assigned the current time when the widget is created:

from time import time\nfrom textual.reactive import reactive\nfrom textual.widget import Widget\nclass Timer(Widget):\nstart_time = reactive(time)  # (1)!\n
  1. The time function returns the current time in seconds.
"},{"location":"guide/reactivity/#typing-reactive-attributes","title":"Typing reactive attributes","text":"

There is no need to specify a type hint if a reactive attribute has a default value, as type checkers will assume the attribute is the same type as the default.

You may want to add explicit type hints if the attribute type is a superset of the default type. For instance if you want to make an attribute optional. Here's how you would create a reactive string attribute which may be None:

    name: reactive[str | None] = reactive(\"Paul\")\n
"},{"location":"guide/reactivity/#smart-refresh","title":"Smart refresh","text":"

The first superpower we will look at is \"smart refresh\". When you modify a reactive attribute, Textual will make note of the fact that it has changed and refresh automatically.

Information

If you modify multiple reactive attributes, Textual will only do a single refresh to minimize updates.

Let's look at an example which illustrates this. In the following app, the value of an input is used to update a \"Hello, World!\" type greeting.

refresh01.pyrefresh01.tcssOutput
from textual.app import App, ComposeResult\nfrom textual.reactive import reactive\nfrom textual.widget import Widget\nfrom textual.widgets import Input\nclass Name(Widget):\n\"\"\"Generates a greeting.\"\"\"\nwho = reactive(\"name\")\ndef render(self) -> str:\nreturn f\"Hello, {self.who}!\"\nclass WatchApp(App):\nCSS_PATH = \"refresh01.tcss\"\ndef compose(self) -> ComposeResult:\nyield Input(placeholder=\"Enter your name\")\nyield Name()\ndef on_input_changed(self, event: Input.Changed) -> None:\nself.query_one(Name).who = event.value\nif __name__ == \"__main__\":\napp = WatchApp()\napp.run()\n
Input {\ndock: top;\nmargin-top: 1;\n}\nName {\nheight: 100%;\ncontent-align: center middle;\n}\n

WatchApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aTextual\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e Hello,\u00a0Textual!

The Name widget has a reactive who attribute. When the app modifies that attribute, a refresh happens automatically.

Information

Textual will check if a value has really changed, so assigning the same value wont prompt an unnecessary refresh.

"},{"location":"guide/reactivity/#disabling-refresh","title":"Disabling refresh","text":"

If you don't want an attribute to prompt a refresh or layout but you still want other reactive superpowers, you can use var to create an attribute. You can import var from textual.reactive.

The following code illustrates how you create non-refreshing reactive attributes.

class MyWidget(Widget):\ncount = var(0)  # (1)!\n
  1. Changing self.count wont cause a refresh or layout.
"},{"location":"guide/reactivity/#layout","title":"Layout","text":"

The smart refresh feature will update the content area of a widget, but will not change its size. If modifying an attribute should change the size of the widget, you can set layout=True on the reactive attribute. This ensures that your CSS layout will update accordingly.

The following example modifies \"refresh01.py\" so that the greeting has an automatic width.

refresh02.pyrefresh02.tcssOutput
from textual.app import App, ComposeResult\nfrom textual.reactive import reactive\nfrom textual.widget import Widget\nfrom textual.widgets import Input\nclass Name(Widget):\n\"\"\"Generates a greeting.\"\"\"\nwho = reactive(\"name\", layout=True)  # (1)!\ndef render(self) -> str:\nreturn f\"Hello, {self.who}!\"\nclass WatchApp(App):\nCSS_PATH = \"refresh02.tcss\"\ndef compose(self) -> ComposeResult:\nyield Input(placeholder=\"Enter your name\")\nyield Name()\ndef on_input_changed(self, event: Input.Changed) -> None:\nself.query_one(Name).who = event.value\nif __name__ == \"__main__\":\napp = WatchApp()\napp.run()\n
  1. This attribute will update the layout when changed.
Input {\ndock: top;\nmargin-top: 1;\n}\nName {\nwidth: auto;\nheight: auto;\nborder: heavy $secondary;\n}\n

WatchApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aname\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Hello,\u00a0name!\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

If you type in to the input now, the greeting will expand to fit the content. If you were to set layout=False on the reactive attribute, you should see that the box remains the same size when you type.

"},{"location":"guide/reactivity/#validation","title":"Validation","text":"

The next superpower we will look at is validation, which can check and potentially modify a value you assign to a reactive attribute.

If you add a method that begins with validate_ followed by the name of your attribute, it will be called when you assign a value to that attribute. This method should accept the incoming value as a positional argument, and return the value to set (which may be the same or a different value).

A common use for this is to restrict numbers to a given range. The following example keeps a count. There is a button to increase the count, and a button to decrease it. The validation ensures that the count will never go above 10 or below zero.

validate01.pyvalidate01.tcssOutput
from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.reactive import reactive\nfrom textual.widgets import Button, RichLog\nclass ValidateApp(App):\nCSS_PATH = \"validate01.tcss\"\ncount = reactive(0)\ndef validate_count(self, count: int) -> int:\n\"\"\"Validate value.\"\"\"\nif count < 0:\ncount = 0\nelif count > 10:\ncount = 10\nreturn count\ndef compose(self) -> ComposeResult:\nyield Horizontal(\nButton(\"+1\", id=\"plus\", variant=\"success\"),\nButton(\"-1\", id=\"minus\", variant=\"error\"),\nid=\"buttons\",\n)\nyield RichLog(highlight=True)\ndef on_button_pressed(self, event: Button.Pressed) -> None:\nif event.button.id == \"plus\":\nself.count += 1\nelse:\nself.count -= 1\nself.query_one(RichLog).write(f\"{self.count=}\")\nif __name__ == \"__main__\":\napp = ValidateApp()\napp.run()\n
#buttons {\ndock: top;\nheight: auto;\n}\n

ValidateApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0+1\u00a0\u00a0-1\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

If you click the buttons in the above example it will show the current count. When self.count is modified in the button handler, Textual runs validate_count which performs the validation to limit the value of count.

"},{"location":"guide/reactivity/#watch-methods","title":"Watch methods","text":"

Watch methods are another superpower. Textual will call watch methods when reactive attributes are modified. Watch method names begin with watch_ followed by the name of the attribute, and should accept one or two arguments. If the method accepts a single argument, it will be called with the new assigned value. If the method accepts two positional arguments, it will be called with both the old value and the new value.

The following app will display any color you type in to the input. Try it with a valid color in Textual CSS. For example \"darkorchid\" or \"#52de44\".

watch01.pywatch01.tcssOutput
from textual.app import App, ComposeResult\nfrom textual.color import Color, ColorParseError\nfrom textual.containers import Grid\nfrom textual.reactive import reactive\nfrom textual.widgets import Input, Static\nclass WatchApp(App):\nCSS_PATH = \"watch01.tcss\"\ncolor = reactive(Color.parse(\"transparent\"))  # (1)!\ndef compose(self) -> ComposeResult:\nyield Input(placeholder=\"Enter a color\")\nyield Grid(Static(id=\"old\"), Static(id=\"new\"), id=\"colors\")\ndef watch_color(self, old_color: Color, new_color: Color) -> None:  # (2)!\nself.query_one(\"#old\").styles.background = old_color\nself.query_one(\"#new\").styles.background = new_color\ndef on_input_submitted(self, event: Input.Submitted) -> None:\ntry:\ninput_color = Color.parse(event.value)\nexcept ColorParseError:\npass\nelse:\nself.query_one(Input).value = \"\"\nself.color = input_color  # (3)!\nif __name__ == \"__main__\":\napp = WatchApp()\napp.run()\n
  1. Creates a reactive color attribute.
  2. Called when self.color is changed.
  3. New color is assigned here.
Input {\ndock: top;\nmargin-top: 1;\n}\n#colors {\ngrid-size: 2 1;\ngrid-gutter: 2 4;\ngrid-columns: 1fr;\nmargin: 0 1;\n}\n#old {\nheight: 100%;\nborder: wide $secondary;\n}\n#new {\nheight: 100%;\nborder: wide $secondary;\n}\n

WatchApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258adarkorchid\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u258e\u258a\u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

The color is parsed in on_input_submitted and assigned to self.color. Because color is reactive, Textual also calls watch_color with the old and new values.

"},{"location":"guide/reactivity/#when-are-watch-methods-called","title":"When are watch methods called?","text":"

Textual only calls watch methods if the value of a reactive attribute changes. If the newly assigned value is the same as the previous value, the watch method is not called. You can override this behaviour by passing always_update=True to reactive.

"},{"location":"guide/reactivity/#compute-methods","title":"Compute methods","text":"

Compute methods are the final superpower offered by the reactive descriptor. Textual runs compute methods to calculate the value of a reactive attribute. Compute methods begin with compute_ followed by the name of the reactive value.

You could be forgiven in thinking this sounds a lot like Python's property decorator. The difference is that Textual will cache the value of compute methods, and update them when any other reactive attribute changes.

The following example uses a computed attribute. It displays three inputs for each color component (red, green, and blue). If you enter numbers in to these inputs, the background color of another widget changes.

computed01.pycomputed01.tcssOutput
from textual.app import App, ComposeResult\nfrom textual.color import Color\nfrom textual.containers import Horizontal\nfrom textual.reactive import reactive\nfrom textual.widgets import Input, Static\nclass ComputedApp(App):\nCSS_PATH = \"computed01.tcss\"\nred = reactive(0)\ngreen = reactive(0)\nblue = reactive(0)\ncolor = reactive(Color.parse(\"transparent\"))\ndef compose(self) -> ComposeResult:\nyield Horizontal(\nInput(\"0\", placeholder=\"Enter red 0-255\", id=\"red\"),\nInput(\"0\", placeholder=\"Enter green 0-255\", id=\"green\"),\nInput(\"0\", placeholder=\"Enter blue 0-255\", id=\"blue\"),\nid=\"color-inputs\",\n)\nyield Static(id=\"color\")\ndef compute_color(self) -> Color:  # (1)!\nreturn Color(self.red, self.green, self.blue).clamped\ndef watch_color(self, color: Color) -> None:  # (2)\nself.query_one(\"#color\").styles.background = color\ndef on_input_changed(self, event: Input.Changed) -> None:\ntry:\ncomponent = int(event.value)\nexcept ValueError:\nself.bell()\nelse:\nif event.input.id == \"red\":\nself.red = component\nelif event.input.id == \"green\":\nself.green = component\nelse:\nself.blue = component\nif __name__ == \"__main__\":\napp = ComputedApp()\napp.run()\n
  1. Combines color components in to a Color object.
  2. The watch method is called when the result of compute_color changes.
#color-inputs {\ndock: top;\nheight: auto;\n}\nInput {\nwidth: 1fr;\n}\n#color {\nheight: 100%;\nborder: tall $secondary;\n}\n

ComputedApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a0\u258e\u258a0\u258e\u258a0\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

Note the compute_color method which combines the color components into a Color object. It will be recalculated when any of the red , green, or blue attributes are modified.

When the result of compute_color changes, Textual will also call watch_color since color still has the watch method superpower.

Note

Textual will first attempt to call the compute method for a reactive attribute, followed by the validate method, and finally the watch method.

Note

It is best to avoid doing anything slow or CPU-intensive in a compute method. Textual calls compute methods on an object when any reactive attribute changes.

"},{"location":"guide/screens/","title":"Screens","text":"

This chapter covers Textual's screen API. We will discuss how to create screens and switch between them.

"},{"location":"guide/screens/#what-is-a-screen","title":"What is a screen?","text":"

Screens are containers for widgets that occupy the dimensions of your terminal. There can be many screens in a given app, but only one screen is active at a time.

Textual requires that there be at least one screen object and will create one implicitly in the App class. If you don't change the screen, any widgets you mount or compose will be added to this default screen.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nM1Ya0/jOFx1MDAxNP3Or6i6X3YlXGKOY8fxSKtcdTAwMTXPpSywo1x1MDAwMVxyj9VcYrmJaT3Na1x1MDAxMpfHIP77XqdMXHUwMDFlbVxiZVx1MDAxN0ZEUZv4OtfX1+fc4+R+pdfr67tU9j/0+vLWXHUwMDE3oVxuMnHTXzXt1zLLVVx1MDAxMoNcdFx1MDAxN/d5Ms38oudY6zT/sL5cdTAwMWWJbFwidVx1MDAxYVxuX1rXKp+KMNfTQCWWn0TrSsso/8P8XHUwMDFliUj+niZRoDOrXHUwMDFhZE1cdTAwMDZKJ9lsLFx1MDAxOcpIxjpcdTAwMDfv/8B9r3df/NaiXHUwMDBilIiSOCi6XHUwMDE3hlp4njvfepTERaiUIVx1MDAwN3PqkbKDyrdhMC1cdTAwMDOwXkHAsrKYpn56sbN1pD45eiNcdTAwMWKyfNP5Nvy6d1WNeqXC8FjfhbM8XGJ/PM1kZc11lkzkqVxu9Fx1MDAxOOz2XFx7+VxcnkBcbqqnsmQ6XHUwMDFhxzLPXHUwMDFizySp8JW+M21cYpWtXCJcdTAwMWVcdTAwMTU+qpZbk1x1MDAwMeJamHmO5zpcdTAwMGV1XHUwMDEwqc23cECY5VLsXHUwMDEwh9G5mLaSXHUwMDEw1lx1MDAwMGL6XHUwMDA1XHUwMDE1R1x1MDAxNdVQ+JNcdTAwMTGEXHUwMDE2XHUwMDA3VVx1MDAxZlx1MDAwZvvcrs335sdMa1x1MDAwM46lXHUwMDFhjbVpxNjyXHUwMDEwcT1GZ75r+ZBF/m3P5pRcdTAwMTKMcWkxI6aDoFx1MDAwMMKX+fyNRZY+5qmfm5tatCbQnXlcdTAwMTTVkVRbY+dcIkv5vlx1MDAxYVxmvk7GfyX88CxcdTAwMWRcdTAwMGZcdTAwMGVLX1xy2Gl5q/ul4WG1y+2Ze1x1MDAxMm1cdTAwMGUv7evp9v6BPls7+8jRfrtbkWXJzfN+XHUwMDFiUawuO5HK7eNVlchpXHUwMDFhiFx1MDAxOfZt10XE5sjjXHUwMDBl4aU9VPFcdTAwMDSM8TRcZqu2xJ9UdFmpxbtA0kacdYba5CmG2thQXHUwMDE0XHUwMDEw4i1N0e7le69cdTAwMTSldidFObeAXG6GLP+HoTpcdTAwMTNxnopcZljQwlLWxlK+wErmeraDXFxcdTAwMWK9Piu7kMihOr1cdTAwMDSJ1YInsT5W31x1MDAwYjS5XHUwMDE2hWKEsIsw41x1MDAxY1HW6LUrXCJcdTAwMTXeNdawgCxEvnMrojSUXHUwMDFiafrrb/VcdTAwMTTnXHUwMDEyXCIpXFyTxjNcdTAwMWKhXHUwMDFhXHUwMDE5aPd9mJvMXHUwMDFhqNdcbkSu7Fx1MDAxMKkgXGJrXGL0IVx1MDAxMFx1MDAwMT6zwTKCk2RqpGJcdTAwMTGetMXZScZM+nqGxVx1MDAxNkZS+qRmYlx1MDAwNCDkUJXdpVx1MDAxOXn+PdGXXyfDk+PRwblzQsefkvPLd89IXHUwMDE3W8hlhHheXHUwMDFiI1x1MDAxZNuxXHUwMDEwI9h+U0pSukhJj0GlmFx1MDAxM+tHalx1MDAwMqRcdTAwMTHFXHUwMDFlcV+fml3KXHUwMDE27MfnQ0rOXHUwMDBmtlx1MDAwMrw33tldu9zDn9+jYM78nu5/vr45INuHXHUwMDA3XHUwMDE5XHL+vMNTTLbdV/CLT4PB3u7EP/Q2iH1cdTAwMTKFf+/EXHUwMDE3ozdcdTAwMTX49sS/QOCZkVZe7a/eSOBcdPXmW3+UXHUwMDEzwinUYUKX34J3o+3dVlx1MDAxM9ZZTVxisZhdaNzbXHUwMDE1XHUwMDEz0lJMsDNfREBcdTAwMWFhXHUwMDE3wp2fKu8vx2GbvGPUaO2Q82M/kzJ+SspZo/+rSfkzMjgv5WWMnZSbVZJcdTAwMTbOMfxcdTAwMTTlQCZAv+FcXF7Bu0vxO+Wc43BcdTAwMGJe7lx1MDAxMXNaOYdcdTAwMTm1XFzOjYJcdTAwMTNujjdjXHUwMDFlslxid5vkLlx06Fx1MDAxMIsz7FJcdTAwMTcvyLlcdTAwMDebXuDGf9loXHUwMDE3wf1sJuZaZHpTxYGKR2CslFxm2OhPzbhryEKO7VLCoVx1MDAxNlKOXHTyylmb6YnU7D0tXHUwMDAyckBcdTAwMWPYg1x1MDAxYYxWr5+98kNQ19b4sXMpqX1cdTAwMTlcdTAwMDfPXHUwMDA2hThUX8Tg1Vx1MDAwME7KmLdcdTAwMTBcdTAwMTW24LWh2HVcdTAwMTXfKmyHPVx1MDAxNVY7zVx1MDAxN8JcbkWut5IoUlx1MDAxYdL/MVGxnk9zkc9ccsPvsVx1MDAxNMG8XHUwMDE1plW3zVx1MDAxN4LUeGzu3KqrXsWU4qa8/rLa2nttXHUwMDExweaoYbfysFL/NzuQwmdfpOmxXHUwMDA2pJVrXHUwMDAwYFbBY+GuJta/VvJms+Xb0lVxmDRcdTAwMTYpNCVHmundP6w8/Fx1MDAwYlxiYlx1MDAxObwifQ== ExampleApp()Screen()"},{"location":"guide/screens/#creating-a-screen","title":"Creating a screen","text":"

You can create a screen by extending the Screen class which you can import from textual.screen. The screen may be styled in the same way as other widgets, with the exception that you can't modify the screen's dimensions (as these will always be the size of your terminal).

Let's look at a simple example of writing a screen class to simulate Window's blue screen of death.

screen01.pyscreen01.tcssOutput screen01.py
from textual.app import App, ComposeResult\nfrom textual.screen import Screen\nfrom textual.widgets import Static\nERROR_TEXT = \"\"\"\nAn error has occurred. To continue:\nPress Enter to return to Windows, or\nPress CTRL+ALT+DEL to restart your computer. If you do this,\nyou will lose any unsaved information in all open applications.\nError: 0E : 016F : BFF9B3D4\n\"\"\"\nclass BSOD(Screen):\nBINDINGS = [(\"escape\", \"app.pop_screen\", \"Pop screen\")]\ndef compose(self) -> ComposeResult:\nyield Static(\" Windows \", id=\"title\")\nyield Static(ERROR_TEXT)\nyield Static(\"Press any key to continue [blink]_[/]\", id=\"any-key\")\nclass BSODApp(App):\nCSS_PATH = \"screen01.tcss\"\nSCREENS = {\"bsod\": BSOD()}\nBINDINGS = [(\"b\", \"push_screen('bsod')\", \"BSOD\")]\nif __name__ == \"__main__\":\napp = BSODApp()\napp.run()\n
screen01.tcss
BSOD {\nalign: center middle;\nbackground: blue;\ncolor: white;\n}\nBSOD>Static {\nwidth: 70;\n}\n#title {\ncontent-align-horizontal: center;\ntext-style: reverse;\n}\n#any-key {\ncontent-align-horizontal: center;\n}\n

BSODApp \u00a0Windows\u00a0 An\u00a0error\u00a0has\u00a0occurred.\u00a0To\u00a0continue: Press\u00a0Enter\u00a0to\u00a0return\u00a0to\u00a0Windows,\u00a0or Press\u00a0CTRL+ALT+DEL\u00a0to\u00a0restart\u00a0your\u00a0computer.\u00a0If\u00a0you\u00a0do\u00a0this, you\u00a0will\u00a0lose\u00a0any\u00a0unsaved\u00a0information\u00a0in\u00a0all\u00a0open\u00a0applications. Error:\u00a00E\u00a0:\u00a0016F\u00a0:\u00a0BFF9B3D4 Press\u00a0any\u00a0key\u00a0to\u00a0continue\u00a0_

If you run this you will see an empty screen. Hit the B key to show a blue screen of death. Hit Esc to return to the default screen.

The BSOD class above defines a screen with a key binding and compose method. These should be familiar as they work in the same way as apps.

The app class has a new SCREENS class variable. Textual uses this class variable to associate a name with screen object (the name is used to reference screens in the screen API). Also in the app is a key binding associated with the action \"push_screen('bsod')\". The screen class has a similar action \"pop_screen\" bound to the Esc key. We will cover these actions below.

"},{"location":"guide/screens/#named-screens","title":"Named screens","text":"

You can associate a screen with a name by defining a SCREENS class variable in your app, which should be a dict that maps names on to Screen objects. The name of the screen may be used interchangeably with screen objects in much of the screen API.

You can also install new named screens dynamically with the install_screen method. The following example installs the BSOD screen in a mount handler rather than from the SCREENS variable.

screen02.pyscreen02.tcssOutput screen02.py
from textual.app import App, ComposeResult\nfrom textual.screen import Screen\nfrom textual.widgets import Static\nERROR_TEXT = \"\"\"\nAn error has occurred. To continue:\nPress Enter to return to Windows, or\nPress CTRL+ALT+DEL to restart your computer. If you do this,\nyou will lose any unsaved information in all open applications.\nError: 0E : 016F : BFF9B3D4\n\"\"\"\nclass BSOD(Screen):\nBINDINGS = [(\"escape\", \"app.pop_screen\", \"Pop screen\")]\ndef compose(self) -> ComposeResult:\nyield Static(\" Windows \", id=\"title\")\nyield Static(ERROR_TEXT)\nyield Static(\"Press any key to continue [blink]_[/]\", id=\"any-key\")\nclass BSODApp(App):\nCSS_PATH = \"screen02.tcss\"\nBINDINGS = [(\"b\", \"push_screen('bsod')\", \"BSOD\")]\ndef on_mount(self) -> None:\nself.install_screen(BSOD(), name=\"bsod\")\nif __name__ == \"__main__\":\napp = BSODApp()\napp.run()\n
screen02.tcss
BSOD {\nalign: center middle;\nbackground: blue;\ncolor: white;\n}\nBSOD>Static {\nwidth: 70;\n}\n#title {\ncontent-align-horizontal: center;\ntext-style: reverse;\n}\n#any-key {\ncontent-align-horizontal: center;\n}\n

BSODApp \u00a0Windows\u00a0 An\u00a0error\u00a0has\u00a0occurred.\u00a0To\u00a0continue: Press\u00a0Enter\u00a0to\u00a0return\u00a0to\u00a0Windows,\u00a0or Press\u00a0CTRL+ALT+DEL\u00a0to\u00a0restart\u00a0your\u00a0computer.\u00a0If\u00a0you\u00a0do\u00a0this, you\u00a0will\u00a0lose\u00a0any\u00a0unsaved\u00a0information\u00a0in\u00a0all\u00a0open\u00a0applications. Error:\u00a00E\u00a0:\u00a0016F\u00a0:\u00a0BFF9B3D4 Press\u00a0any\u00a0key\u00a0to\u00a0continue\u00a0_

Although both do the same thing, we recommend SCREENS for screens that exist for the lifetime of your app.

"},{"location":"guide/screens/#uninstalling-screens","title":"Uninstalling screens","text":"

Screens defined in SCREENS or added with install_screen are installed screens. Textual will keep these screens in memory for the lifetime of your app.

If you have installed a screen, but you later want it to be removed and cleaned up, you can call uninstall_screen.

"},{"location":"guide/screens/#screen-stack","title":"Screen stack","text":"

Textual apps keep a stack of screens. You can think of this screen stack as a stack of paper, where only the very top sheet is visible. If you remove the top sheet, the paper underneath becomes visible. Screens work in a similar way.

Note

You can also make parts of the top screen translucent, so that deeper screens show through. See Screen opacity.

The active screen (top of the stack) will render the screen and receive input events. The following API methods on the App class can manipulate this stack, and let you decide which screen the user can interact with.

"},{"location":"guide/screens/#push-screen","title":"Push screen","text":"

The push_screen method puts a screen on top of the stack and makes that screen active. You can call this method with the name of an installed screen, or a screen object.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nOVcXG1z2khcdTAwMTL+nl/h8n3Zq1xu2nnr6Zmturpcbk5cdTAwMWN7XHUwMDEzx/Fb/HK35ZJBgNaAMFx1MDAxMrbxVv779chcdTAwMGVcdTAwMTIvXCJgMEtyVGyMRkitmaeffrpnJn+92tjYTPqdYPO3jc3gvuI3w2rXv9t87Y7fXHUwMDA23TiM2tQk0s9x1OtW0jNcdTAwMWJJ0ol/+/XXlt+9XHUwMDBlkk7Tr1x1MDAwNN5tXHUwMDE49/xmnPSqYeRVotavYVx1MDAxMrTif7vfn/xW8K9O1KomXS+7SSmohknUfbxX0FxmWkE7ienq/6HPXHUwMDFiXHUwMDFif6W/c9ZVQ79cdTAwMTW1q+npaUPOPLSjRz9F7dRUY5hgWqFcdTAwMWGcXHUwMDEwxm/pZklQpdZcdTAwMWFcdTAwMTlcdTAwMWNkLe7QJjQv9MPZWalafuj/WTqqmebJXHUwMDE3nt21XHUwMDE2NptHSb+Z2lx1MDAxNEf0KFlbnHSj6+A0rCZccmrlI8eLvtWNevVGO4jjoe9EXHUwMDFkv1x1MDAxMiZ9d4yxwVG/XU+vkVx1MDAxZLmnT4prjzFccmgtN1x1MDAwMCx7WPd9oYzHuFVWgFx1MDAxMlpxgSOGbUVNXHUwMDFhXHUwMDA2MuxcdTAwMWYsfWWmXfmV6zrZ165m53Dw/aua1tlZd09cdTAwMGasLHjKKIkwaGpcdTAwMDRhvZE4I4z2XGZyZvOtcZCOXHUwMDAyV1ZbhsCzXHUwMDE2d9PObjWFw1x1MDAxZqP92PC7naf+2ozdh5zBztZ3o1jK4yk30sFu9fz86Fg97Fx1MDAxZVx1MDAxZX5IToKqvdy6XHUwMDFmXFxrXGJ8frdcdTAwMWLdbVx1MDAwZVq+Pv2VmdbrVP1HTHGtlWVcdTAwMTJcZkeW9XQzbF9TY7vXbGbHosp1XHUwMDA2w/To19dzg19cdFVcdTAwMDR+Qo7Qmis5O/rvoHFwvV+PXHUwMDBmPnROXHK7XHUwMDEwd1dvP/TWXHUwMDFj/YJ5wjKu6FGNNiiH4c9Re1xujVx1MDAwNcGNsoh8MfjX/CvGYIng18BcdTAwMDRYo/lqwVx1MDAxZuvz8Kh8X98qb5v7crdzqe3t4VLAb1xmXHUwMDFhLkGwZYE/XHTuk0nIXHUwMDA3XHKFyDdGXHUwMDEx+Fx0XHUwMDBmMyNfto9iXHUwMDFktMzno3q3vHuO8mRfrDnvXHUwMDFiXHSe0VJoXHUwMDAzipxcdTAwMWPsXHUwMDEw8ClcdTAwMTB4TCgujaBcdTAwMWZccrBcdTAwMTDu0Vx1MDAwMquJcdxzZsZcdTAwMDGPfFxm5lx1MDAxNH6YlIrJn4fjhbZKSJhcdTAwMDPmXHUwMDE5mqJ2clx1MDAxND5cdTAwMDQpOVxmXHUwMDFk3fZbYbM/XHUwMDA0iVx1MDAxNP9k4FGlXHUwMDFiXHUwMDA07Vxy/t/2L42wWlxy2v/MXHUwMDBmWVx1MDAxY9D93Vx1MDAwNfXwN980w7rzls1mUFx1MDAxYnajJCQpNmhOolxcXHUwMDFmV8hcdTAwMTKfLtfdrY4+UdRccuth229cdTAwMWVcdTAwMTdb9Sxv1kZcdTAwMTR7M1xigUTgXHUwMDE5/L/nzVx1MDAxNyfbd35n//D+Zlx1MDAwN45bV+clXHUwMDE5Xa+5Nyu0XHUwMDFlSoZWUawg+squ4r5P4cVcdTAwMTDgkFx0ozSSWHpcdTAwMTFvXHUwMDE2MiPMgTfnjj15s2aS5FxymiyU/uAxy1x0Nupblj3pipxZbPxCiVN41VxmJjuzgKFvrsiZ81ZNdebHbp7gzVxc5eLCqDtTTFKkj0Xm8N9z5+kjP4c7i1Fsvpg7g9RcdTAwMWVcdTAwMThtKTZLhZSaXHKrUqU8y8iVXHUwMDA1Q2lAczli2HL8XHUwMDE50JOcXiR/XHUwMDE13Vx1MDAwYsxcdTAwMDT3RuY5cVxmkqhF0082bt9it6A2inXyXHUwMDE5+Vlq51x1MDAxNHf/nkPOk0Hl7PC7STlsV8N2nVx1MDAxYTMm+VZm2J0hRqQuXFzpOSuZp4FzSYNI6Vx1MDAwNVx09ixRdV3hd5zN0mOGRFx1MDAwZadMg34o6Xo64+vAqqBd/b5N0/OvIZtcdTAwMTgpXFxOXHRccpL2s1xcXHQ7Zlx1MDAxNNmkZZrzcCYkmT5mU9OPk62o1VxuXHUwMDEz6vrPUdhORrs47cs3zs1cdTAwMWKBP8ZcdTAwMWb0TPm2UT7ouCtcdTAwMGXTevbXRuYw6YfB33+8nnh2MZbda1xmxdnlXuXf52Yyul0hkZGsZsRcdTAwMDSQucz3iGy6XHUwMDFlXUtcIjPUtYpcdTAwMGLrKkdEMyp72DTLkOhRzoeugEN5iFUjdi2HxzR6hmv6p4GowfKMTFx1MDAwNzRmXGJcdTAwMDCaXHUwMDEyaVdH4qBzicZcdTAwMTONSUtcdTAwMDJcdTAwMTJgxSSGXCLPqMsnselp61x1MDAxMGFI5ViCXHUwMDEzYl1HWZM76YnEhCdcdTAwMTEoWilwWog/l8Sml1BzNpXIKGlpXFy4pZtcdTAwMDGiVuNGecagdIqBMkgk++GHZrFSIZTT1jFcdTAwMTTPSWNTXG6F0rDRo1x1MDAwM1wi05K7bFx1MDAxNmZPsPzfXHUwMDBm3vfffLx4+yG86PcvL9tcdTAwMGbNXHUwMDAztt5cdFx1MDAxNiFcdTAwMWY8XHUwMDA1oDSjIE3cnYmhtE5cdTAwMGXCQ8s1UlJPRJZcdTAwMTNsa1ImJ7BokCBeoExeXFzLI6fjdq5cIsdz8Vx0qjBjUOT3zpjZq3kx6NPLclx1MDAxZuO9m/2y6uzU7vq4t/bwtJ6SXHUwMDAyXFzh0lx1MDAwMojhQMuN8Fx1MDAxOKN2XHUwMDEyQYyyXG47qlx1MDAwMP7eOjblXGJcdTAwMDBMw1x1MDAwYpSxp1TgrFwiPWJXXHUwMDAwTi1kXHUwMDExOEFKrTnD2UXgVrVcdTAwMWQlW5c7+zbeerhcdTAwMGXOeVJcdTAwMGab61x1MDAwZU4hPFLXRlJ4XHUwMDAyru1wdaqktYdcdTAwMTTTSGExJPDgYjLQiIrlwVx1MDAxMqkzzcCZYSueYTRBsNP7+PDQvLrQlzWZhLJ/wXJCaCz/XHUwMDE4tHx9Pe26+/H+obi77iad29Ln2t71l/v34tNcdTAwMTKuW8Wbm+B3/Vx0opCfRNt7N3uldydLuO5N+fN+VZ2etu7Ptt9WoFx1MDAwYu/fXHUwMDA0/WVV4Y1GsHJZXHUwMDFjUFSelpJcdTAwMTdcdTAwMTFcdTAwMDBSVkHaWsqZXHTg0n93/mf/rnZcdTAwMTi/3T/dub1Oro72dtY7XHUwMDBipDOMR1x1MDAxOZ5kLolcdTAwMTJsZJa1RKmFJ4hcYkmju0lYu2BcIkhZ01UwQTxBbrZ74PpqzOGVm/cmmbdSqYRcYozPXHUwMDE1jbJcdTAwMTHPSsiUT3NDbKtJoXKj7dA5g4JyhrVvXHUwMDA1Zb/T8Tq9uHFcdTAwMTmnNdxfXHUwMDFl3+TksnKupL+KsnKhbVNdsbAkI6coRZe1XHUwMDAym10oTqe8Zbhi1Y9cdTAwMWLBsoOx9ijUSlxutIYh48PB2IBcdTAwMDecXHUwMDE5amTkhsYsNvFb5IncXHUwMDEzzsW04kZxd5dcdI7JuSRSsKBpRCwl72JsJom7iVx1MDAwMiZI187vqc8vy5BuJLvnWaCQs2Omssx0ibeRL8tYo5hQSlCqRfwhclXNp1xuXGJ4Wlxu5UaUXHUwMDEz/VEnPp1QUJVcdTAwMTl+ilx1MDAxZqg0UoyotHVcZkvZ9V7l3+emXHUwMDEzZVxuQztHkETtYlx1MDAwZXE/XeusKaFcdTAwMDBcdTAwMDePUkpcdEaCXCKVn027pISiSFkjXG5cdTAwMDDNgVx1MDAxOGexuapcIkJcdTAwMTFcdTAwMWVQnm+UIHfgUptcdFx1MDAxYZ9z7qFby6WsK7vz3ErHgdJHXHUwMDFhM3jWcqpF+IS6jWfG/I18UlwiQrGCuVVBjjGktpg765FQKHhcYkB3krbaMGV/UkIpRJR7jWNpWXxC2UAhnyhcdTAwMDZMXG4+R6F1eq63pnxcIjR6qJTRmlx1MDAxMiPQw3RcIoT1pFx1MDAxNVppXHUwMDFhXHUwMDE4o5larJJVLFAsY5xuT9lcbpOcTZj6pifxSEXRXHUwMDE1LKUsbv52lE9QXHUwMDExMii1y1x1MDAxYVZCJ0ipy0vOXHUwMDFhzSFPSJxJKy2iXHUwMDA2olxuXHUwMDFh1DE6sVx1MDAxZadcdTAwMGUkkyl40Kjit6nXn41OXG5cdTAwMDGVNo5BaU46mbbEu3ihK7FcdTAwMWKSaJqjMv7pTInTqFxcOjg9tlx1MDAwZqdYPWbnX2prXnwkXHTmKTBGK0U9j7lcdTAwMTn5lE8096hcdTAwMDeMJFVohFx1MDAxNHKx0sNLbHBAXHUwMDEy+EOZ2ErKj0dcdTAwMTf7b5p7R8FJr1x1MDAwM+XD6qfT3avP0bLKbujkRcbcL1h6l4XhVEmwXHUwMDFhQc++kKxxU2p3XHUwMDBmto7fn15Hl5dxq9xTbz+sO/qBMlx1MDAxZlx1MDAwNsg5PaxQo/tcdTAwMWIs86RbdUSpXHUwMDEx0zQq6zUvxLlgXHUwMDE0RC0+I4IuMDGElph4XHUwMDFlQf5cXHSiLlxctmyF0qTF5+Dmz7+fd9iX3bPem/P6Wb9S7d10+s8qRq1cdTAwMTSdgjRcdTAwMDJ1ttRcdTAwMWElz3XH41x1MDAwNZRcdTAwMDdcXKBcdTAwMTKkXHUwMDExXHUwMDAw0C6WOy59Yki4WXVjV7wrYYF5oe9cdTAwMTKzJI3CzNKIuWg+RPDiJMcyRVx1MDAwMlxi7ezbzirJznZ8dVx1MDAxYpr7sniItpvxzVFcdTAwMTfWPckxaDw3XHUwMDExQsLEas1gWJWUXHUwMDA0+Vx1MDAwNaBUaaqthF5s31lcdTAwMTHy81x1MDAxYlCmrNjnXGYkokSxYqSrOHp7v7d3wpPjg6D8caeCeLK/XHUwMDE0pFPmzjVH8ayqy1wiS/blWi7Zl1x1MDAwYi/ZXHUwMDA3LC6DOpaXgs1cdTAwMTHKplx1MDAwZvx6TnFazTyBXHUwMDAyJMFcbpRcdTAwMWTZRq09bi0pXHUwMDE5Zlx1MDAwNXk7vkwgk5LuopVA6TJcdTAwMWGYtFx1MDAxZMcqz1xiSomsUW5cdTAwMTNcdTAwMGVcdTAwMWL3dVx1MDAwMaBcZua3XHUwMDFmr2Kp69xxJ2fHTEWL6UFiI1+0oFx1MDAwNF1cdTAwMGLLSSy7moTiuZNcdTAwMDZLXVx1MDAwNWjDnO3I3L7EpzN+tqJFqVx1MDAxMFHuNYal7HKv8u9zq1x1MDAwM1moio3klKHjXHUwMDFje/mOXHUwMDBm3vdKeHi6/eXjx/pd/fL0T41FS01cdTAwMWJ+pdHrXHUwMDA266CL0Vx1MDAxNYRAWMNcdTAwMTQoPZK1XHUwMDAxqWYrgCSElJbU68uIg9zE1jRtXHUwMDAwlMhz58Gr1Vx1MDAwNrUusWjnXGaO3zW2XHUwMDBmv1x1MDAxY4ja4bugM5s2eD3tui9a9rDWTZCtTHM8bqldXHUwMDA3nfFkyfO0XHUwMDA1isL/ocWme+D4XHUwMDFjM6zTcTNcdTAwMTdcdTAwMWasUFxcaItcdTAwMWVBR9Cjulx1MDAxZPt2ZFx1MDAxYo1cdTAwMTJcdTAwMWVoXHUwMDA13M2aUFx1MDAxZbtYXHUwMDExpzBZXHUwMDAwj9PFXHLnkrJxJifJXHUwMDBiro1cdTAwMDeIRksrXHUwMDA1XHUwMDEy6Mc20qCbu2GYXHUwMDFisZXMiTzb8WaUXHUwMDE308PMxvCuXHUwMDE1t0NDS+pAt0JcdTAwMDAmLNrgdJLmMt1qwMBcdTAwMDD8rFx1MDAwMqNcdTAwMThT7lVcdTAwMWGH05xcdTAwMTKjkFM0K+RcdTAwMTTQbjGylLNrjOkxY105XHUwMDA1085329qY0E5VjXCKJVx1MDAwNWKYNcYoXHUwMDE0L7Qm2yjPpYaoLaOsg7p9nFLIXHUwMDBlt1KNJCjRXHUwMDFiSW4xXoejhEpxQlHWef9/nFwiaFx1MDAxOK2rXHUwMDE4XHUwMDE56lx1MDAwYs3HOUW6rXCIlIQq1EaQfpzOKUVWTZ9cdTAwMDJcdTAwMWOxijFlJLOKI1GZlVx1MDAxM5jOc5UwTckxxVx1MDAwNUvZxI+9Qa9cdTAwMTDP7lVcdTAwMWGHclx1MDAxMZ29erqDW/x6lFx1MDAxMO5cdTAwMDZcdTAwMDNC0Fx1MDAwZatPSjB7zM3bMLgrT5qRSV9OeKVcdTAwMWTquChwXHUwMDBm+9fXV1//XHUwMDA3mNhRliJ9 Screen 1(hidden)Screen 2 (visible)app.push_screen(screen3)Screen 3 (visible)hidden"},{"location":"guide/screens/#action","title":"Action","text":"

You can also push screens with the \"app.push_screen\" action, which requires the name of an installed screen.

"},{"location":"guide/screens/#pop-screen","title":"Pop screen","text":"

The pop_screen method removes the top-most screen from the stack, and makes the new top screen active.

Note

The screen stack must always have at least one screen. If you attempt to remove the last screen, Textual will raise a ScreenStackError exception.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN1cXOtT20hcdTAwMTL/nr+C4r7sVcWz0z3vrbq6XHUwMDAyXHUwMDEyXHUwMDEyQnhsyObB3VZK2MLW4ddaMsZs5X+/XHUwMDFlhSDJQorBxnHiXHUwMDBmXHUwMDE4a+RRa+bX3b9+yH8/2djYTKbDcPO3jc3wqlx1MDAxOXSj1iiYbD71xy/DUVx1MDAxY1xy+jSE6ed4MFx1MDAxZTXTMztJMox/+/XXXjC6XGKTYTdohuwyisdBN07GrWjAmoPer1FcdTAwMTL24n/7v4dBL/zXcNBrJSOWXaRcdTAwMTG2omQw+nKtsFx1MDAxYvbCflx1MDAxMtPs/6HPXHUwMDFiXHUwMDFif6d/c9K1oqA36LfS09OBnHhazFx1MDAxZT1cdTAwMWP0U1FBXHUwMDBiLZW28vaEKH5GXHUwMDE3S8JcdTAwMTaNnpPAYTbiXHUwMDBmbcrRNNFcdTAwMDae897Fx119cmhcdTAwMGZOxyfZVc+jbvckmXZTmeJcdTAwMDHdSjZcdTAwMTYno8FF+D5qJVx1MDAxZH/pmeNV31x1MDAxYVxyxu1OP4zjwndcdTAwMDbDoFx1MDAxOSVTOqb47cGg306nyI5cXNGnXHUwMDA2cs6M0Vx1MDAxNqTiXHUwMDEy6G7V7fiXXHRcdTAwMDQz1lx1MDAxOFx1MDAwNUJcdTAwMWEhXHUwMDA1qFx1MDAxOcl2XHUwMDA2XdpcdTAwMDeS7Fx1MDAxZjx9ZbKdXHUwMDA1zYs2XHTYb2XngFxugrPz7JzJzf1Kp5i0Uphs+k5cdTAwMTi1O4nfIauZNcBdfjRcdTAwMGXTTXCgpJNaZlvkrzjca6Vg+HN2XHUwMDE1O8FoeLNam7H/kJPWXHUwMDBi+nxcdTAwMTZJeTTl9lm8grPdXHUwMDEwYKe1v/3X85NcdTAwMDP5+2CrfztXXHUwMDAxesFoNJhs3o58vvkvXHUwMDEzbTxsXHUwMDA1X1x1MDAxMFx1MDAwNVpLa43TXHUwMDEyTVx1MDAwNspu1L+gwf64282OXHKaXHUwMDE3XHUwMDE5XGLTo5+f3lx1MDAxYvp0mSroo+OO0KD03NBcdTAwMGbHU3ux39vnfHz+ctLeiyb6hfue0Fx1MDAwN/5N7IPTTFx1MDAxOSNRc1x1MDAwZVx1MDAwMoyyXHUwMDA17EuBXGalQYKeddo4vlx1MDAxOPbPgzPO1Vx1MDAxMrGPQipwlq9cdTAwMTb7vd45n2zx5NlhNFxmwz9eXHUwMDFlbb86iJeEfVx1MDAwYlxccG6Whf0kvEruXHUwMDAyvkVdXHUwMDA1fFx1MDAxMNZx5NLh3Mh/d941l1fDy5fT3taHwfjj8PiF2F1v5CMqprRBXHUwMDA0dEZ6XHUwMDBiWlx1MDAwML7lwMhcdTAwMDRJclxi1iHkrMBDcG+c4udYxj1wW1x1MDAwNryBWZhrgdL7pp/IxDtcdTAwMGJK2PvAPEPToJ+cRNepjbaFo7tBL+pOXHUwMDBikEjxT1x1MDAwMp40R2HY34D/9n/pRK1W2P9nfsfikK7vJ9TFb251o7bXls1ueF5UoyRcIlx1MDAxZXY7nFxmcmvcJElcdTAwMDKabrTXmr2jwShqR/2g+7Zaqlpt/rLMd6gzUVx1MDAxM5w9nNNnIHojxPz6XFy/8/fQZ5zF5uPps3HMSFx1MDAwMGm4pXdbVGfjJFx1MDAwM81cdTAwMWRcdTAwMWGU6EjjXHUwMDFmRZ1cdTAwMWQyrogwS2MsR9TuXHUwMDBl5XZMIzk6KVFcdTAwMDHXOlx1MDAwM/BXlyaFv4VcdTAwMDeoeipkjao/ijLGSTBKtqN+K+q3aTCzXCJfQ5K9OVx1MDAxY0Sqvs2xl5IzhVxcS8FpI5UgL1x1MDAwNLmT2sHQLyFcdTAwMDMgTqLJZKNVTtibXHUwMDEzPt9cblx1MDAxNfZb31x1MDAxNqk+UMmJ1OBcZml5nPV7pjhqilx1MDAxM0pCSaZcdTAwMDVIXHUwMDA3nITiTlmnSlJ1gzjZXHUwMDE59HpRQmt/PIj6yexcdTAwMWGni7nldbxcdTAwMTNcdTAwMDYl40F3lVx1MDAxZps1XHUwMDA2Qz9j0aZn/21k2pJ+uP3/z6d3nt2ohHI6WkJxNt+T/PtcdTAwMDNcdTAwMTi5sJWGXGYtqVx1MDAwNlx1MDAxMZdM8b/JyM9G2NpcdTAwMGKuw/1nW1x1MDAwN89f9LWNIVhvXlwinGOOTFx1MDAxNVxiNN5y68xcdTAwMTJ8XHRGLeNcdTAwMDLAOeK+ZCaEmJHsIcGo1suj5ESVkKJcYs5cdTAwMWaBrNRYMECFuIqIkYLtSkeLZLhcdTAwMDVwNb+jPdzpXHUwMDFjXGZfXHUwMDFjvdtcdTAwMWRcdTAwMWNfj9xhcjQ+XHUwMDFmivVcdTAwMDao5IJcdJ9cdFGWdFWqYrJEXG7NuOPGXHUwMDEyQqVcdTAwMDZcXFxmnsuOXHUwMDE3SVx1MDAxZaE4mTS7fHDWMenXZ8rufYyvOsPx+MNWdPomuVx1MDAxOMllXHUwMDA1jIQ3yHG7x4O+XHUwMDE1qlxu+k6AUkS65jfN76eHb9o9t3+0r/76NH32abtzKI+WivxWXHUwMDEwd8IlQ98xolx1MDAxZYJcdTAwMTONXHUwMDA0YpGuXHUwMDAwfaGQXHUwMDExKZFcdTAwMDY5cDBcXC9GMi02XHUwMDFkhGqZ6CdcdTAwMTNpSTS+4nSJXHLDl+PX19fds1P96VxcJJGYnvL50P+0bt5Y7U+v4+3jyejgdO/Vofj0x9SeLWHe8+H7ydvG5Ni+v+7Fp1x1MDAxN83wI3ZcdTAwMGaWMC9cdTAwMWab//VO3mLyprl9XHUwMDE1NT/uvlx1MDAxMc1oWfE0J+S5pTnAqrSRrktcdTAwMWKRWjiKvOzcNuAsnuKOONpcdTAwMDXV3u6Mmq33h+r6dL3DTGlcdTAwMWSzSkvNOZleNZMuXHUwMDA1QaNOcrKERJ3JXHUwMDE3zlxudj9cdTAwMTNA6npcdTAwMTbewc1ErqZxq/myrO9CWqIrhj+Ct6tBolx1MDAwMan1fZCYbXiW2Vx1MDAxMVx1MDAxNOWlgYfnwDZcdTAwMTdJXHUwMDE38jzZVb7meYLhkFxyXHUwMDA3w09xmln55e4sj9CF7z12lqckU63qVeZ4dHVkROxXXHUwMDEz71Zyft2rN57L0L1H8L8gmERUXHUwMDE0ZiOZOzFTq7CGIVx1MDAxN9pcYqPJXHUwMDE3w2Ip2yrd40RvXHUwMDAxkVgwXHUwMDAx06E1QpZ1XHUwMDExuPGCmpRvXHUwMDAyXHUwMDE5XHUwMDA0V9ZNQjUpTC7JvopEXHUwMDBmcFqcx0z01NO6jUJWxVEsXHUwMDBmjmyoRE2rmUsx3CRVXHUwMDE0U1x1MDAxNFx1MDAwMyuOdFx1MDAwMtpvZnqKt/EjZVtqQJWOl/GUTfkk/35vo2JyRYVZo0JwXHUwMDAx8iD3yFx1MDAxYtczp/U0Ko5LZi1ZXGZDsaySZtaoXGJmXHUwMDA0V4pWnlxmXHUwMDBizEZcdTAwMWLLMipoOWghfSmfLpIrRuVsimWSglx1MDAwZetcdTAwMWNcdTAwMTiCXG6WykSgjZXOc5PV2lx1MDAxNKCYXCJbte9oU4DRXHUwMDBl0PIopykqJlx1MDAxZVJOXHUwMDFlXHUwMDAzZ8QmrNTkQoThXFyan9SmVEPKv1x1MDAxYWU0LcuiXHUwMDE08l+zXHUwMDE5XFyurZS+nDm3SalPnaynSdHKMCO55lx1MDAxMiVIk+tk8d/XXHUwMDAyXHUwMDE4+X3iMd6x4YItXHUwMDE1VSaFdMFcdTAwMTkjyWhJWnXSiez+b02KU1xmpXbKcsFccqrcrtxYXHUwMDE0dPRdhfpcdTAwMDFcdTAwMDHEQiSFK5fJ8nCDMqt8P4FaN6r3NVx1MDAxZC5t6T3Vuq5XSldTXHUwMDA1Tv5cdTAwMTM06vmpgn6m8FX0cnLZfvfhQrevT49+j79rn+C31ZrIMzBcbj6QTCatr5lpXHUwMDE5UUAkTSluXHUwMDFkUSaXL8ivR2VGeTOvVptcZlhdJ59cdTAwMTbVXlx1MDAwN4Rwvk1s/ui4cbU/tn+1wsvOycdLXHUwMDExTl87PNhZe3Rq5uNcdTAwMDdB2DMobLFwSG6XkS9cIuRcbq2EQbdcdTAwMTA6l16YcUIgUTfzgHB4kdR0I57s7CB2I9lcdTAwMTZcdTAwMWZa4z+O48nFslKyVlpuga9cdTAwMDD7NuegS1xy3IAkjJyfcMX93dFe7+L5azHF8L1otTvDg9Z6Q79cdTAwMDHWMrCKmKXTzmlcdTAwMGVcdTAwMDXoXHUwMDBiKYlcZiuL3KLv6l1cYvlfyjLLLElcdTAwMTJHXHUwMDA0XHUwMDEy7CcpypxtPT9/XHUwMDFmbPHD9rv9t1x1MDAwN83rQZDE46X1xqK0uDSNqlxmYWpcbp2gfHVbajN/W3h92Wd9I1x1MDAxOFxupYVcdTAwMDRSXHUwMDFiXHUwMDAxekafiOiQWfHxi6b9WKzOWVx1MDAxZMBcdTAwMThFXHUwMDExlKPolYiLJZ9VVizincxnZZRcIq/mfLmjpF5cdTAwMTTqy2Jss5pcdTAwMTCG1P1BNZBl50Q448AlxXBKXHUwMDE5jeA4N3d2r/neNk3RKsWlZJBuTvjZklwijWpQfVx1MDAxOS7hKZvxSf79vnVTqWD26C071cjR3ec5k6vXoVx1MDAxY530XHUwMDFiz/u6od1+6/CV6thcboPSXHSanfEoXFxcdTAwMDNcdTAwMWZN+GJcdTAwMDZ89yTB0T/jUEy0XHUwMDFhJ1x1MDAxOHF0X1x1MDAwZVx1MDAxMMpavlD1Jlx1MDAxOVx1MDAwNf14XHUwMDE4jEhd7jAsucxpTdO9XHUwMDE0qJw1YsWM9DGfLfHZXHUwMDAy41bedI9r2XSPizfdc1fdXHJBtsOSf7zHk5P1O38vtV5dP0SDXHUwMDAySlx1MDAwNspYIynYXHUwMDE3zuqZxnuLjLyhptPQmVxccWXpWlxyilx0XHUwMDAwKYiekW1x7q5cdTAwMTJcblx1MDAwMpM+XHLCLYVMoPJcdTAwMWRaN3TBu1x1MDAwN4fwkCzJXCJ0gTyzclx1MDAwZtHLOelCvcvYKDa7k+8z5CONTOvo5bIscEaLJITl3JdR9Ndi5D1cdTAwMWLw61x1MDAxZpcsUFx1MDAxOFx1MDAxMCCNpZ3zXHUwMDE5XHUwMDAyJY0uyWRcdTAwMTjSgCR648jGXHUwMDE5xJJMP1x1MDAxMk+pXHUwMDA2s381yjheXHUwMDE2TVx1MDAxMZWJXHUwMDA0XHUwMDA0n2+mUHV+nlwiPlxcyNa1fHn54tnhm9Z04sK+qirdrFx1MDAwZk9cdTAwMDHBmSFoI7FD32ePRYOGqH1cdTAwMDXRKCAuY6VSXHUwMDBipVx1MDAxM75BVO5o8ypcdTAwMTNcdTAwMTWyXHUwMDFjUlx0IdWPw1Se1s37mFx1MDAxOVx1MDAwNLK0Wq+eXHUwMDAxXHTiXHUwMDFhl1FcdTAwMWOddcN1okBcdTAwMDWxXHUwMDFlxoFcdTAwMTRWXHUwMDA2NuAsoZK4/vxN4fVbv65cdTAwMTTIP/IglFTeLKBRYqYtXHUwMDFjkFx0Ylx1MDAxNVx1MDAxNoUherTYXHUwMDAzO7X2XHUwMDAyXHUwMDFkOUokj01Ow3IpsyvdWlx1MDAwZu3du/TJXHUwMDAwXHS0MVjOl/jSXHUwMDAxRaSr7iF5sFrOSYDqfVGRXHUwMDAwgdCeXHUwMDA3Ulx1MDAxNFxuXHUwMDE2yeHlclx1MDAwNF9cdTAwMTmQZNJvJaBcdTAwMTJcdTAwMWOMeegziPW59qJUXFxcbmPJISljtNEu94NcdTAwMWa3YllmNHFcdTAwMDPaWue4MkKWpPqRSFAlnP2rXHUwMDA05CUxIFGdqCFhUEtcdTAwMGbXue3ZyfXlKzWNj6ZHL+LB5OPWZft479O6MyCK1ohcdTAwMDGh4MSCjK9ZXHUwMDE0XHUwMDEzNUJcdTAwMThGXHUwMDExgnRElFxibvlfUPlOmVx1MDAxYSBcdTAwMWHmIFx1MDAxN1x1MDAxNfzotUPHXHUwMDA1t8LlbmiFmZo15Cm4OE8xrlqvwVx1MDAxN1xyrTXzN6/Ub/2a8lx1MDAxNFxuKsH/mFx1MDAwZjk3w41cdTAwMTC5eCFtXHUwMDEwXHUwMDAwzdAnckxcdTAwMWHai8VcdTAwMWXdrNVr7dtotPDP9/tcdTAwMDdcdTAwMDfhXHUwMDBlLdeGOeu7XHUwMDE2NddWlVx1MDAxYdP8U2zKoV3l7yQspJVz0pR6h7FRKOs4RS9OXHUwMDFiXHUwMDA1WphcXGfyRpZcdTAwMTORXGLCao1cdTAwMWH8XHUwMDEzYuWfJJiLpNT3wszIRFxcSCvyyEJq8lx1MDAxZqIkXHUwMDEzkmfx7Wv+4XY0d7X0/0hcdTAwMTSlXHUwMDEyyOlgXHUwMDExwlVcdTAwMDTlyc3s/jGhk4TwdrtcdTAwMTVcdTAwMDTpqHVjyrNb3LyMwsn2XT056cvbx3QxvVx1MDAxNVxu/Y3+/fnJ5/9cdTAwMDPV4pXXIn0= Screen 1(hidden)app.pop_screen()Screen 2(hidden)Screen 3(visible)Screen 2(visible)

When you pop a screen it will be removed and deleted unless it has been installed or there is another copy of the screen on the stack.

"},{"location":"guide/screens/#action_1","title":"Action","text":"

You can also pop screens with the \"app.pop_screen\" action.

"},{"location":"guide/screens/#switch-screen","title":"Switch screen","text":"

The switch_screen method replaces the top of the stack with a new screen.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN1cXG1T20hcdTAwMTL+nl9BcV/2qsLsTPe8btXVXHUwMDE1XHUwMDAxNlx1MDAwMVx1MDAxMpNccuH17opcdTAwMTK2bGsxsrFlXGZs5b9fjyCWLL9gg3DIKlXY1sjj1szTTz/dM8pfb1ZWVpPbTrj628pqeFNcclpRrVx1MDAxYlxmVt/689dht1x1MDAxN7VjaoL0c6/d71bTK5tJ0un99uuvl0H3XCJMOq2gXHUwMDFhsuuo11x1MDAwZlq9pF+L2qzavvw1SsLL3r/930pwXHUwMDE5/qvTvqwlXZb9yFpYi5J29/63wlZ4XHUwMDE5xkmPev9cdTAwMGZ9Xln5K/2bs65cdTAwMTZcdTAwMDWX7biWXp425MwzUDxbacepqWiElFxiKrsg6m3SjyVhjVrrZHCYtfhTq7fHrZPex6uT643+2ra4qrq7KKlmv1qPWq395LaV2tRr061kbb2k275cYo+iWtKkVlE4P+1b3Xa/0YzDXm/kO+1OUI2SWzqn+PBkXHUwMDEwN9IusjM39GlccpVgxmgrpOJSKG3VsN13IDUyY41RXHUwMDAypUGJQlx1MDAxNSzbaLdoXHUwMDFlyLJ/8PTIbDtcdTAwMGaqXHUwMDE3XHIyMK5l11xiXHUwMDE1XHUwMDA05/XsmsHD/UqnmLRcdTAwMTJN1n0zjFx1MDAxYc3Ez5DVzFx1MDAxYcFdvrVcdTAwMTemk1x1MDAwMCA5OpR22OB/sbNdS8Hwv+IoNoNu52G0Vnv+Q85ab+hWXHUwMDExSXk05eY5TCpR2N89QLt7uub0detcZuKPw75GoFx1MDAxN3S77cHqsOXbw7vMtH6nXHUwMDE23CNKaC2tdU5cIupsMltRfEGNcb/Vys61q1x1MDAxN1x1MDAxOVxi07Pf3i5cZn2Jalx1MDAxYfSFXHUwMDExllx1MDAxYm5cdTAwMTTOjf0/wi+wKW/OKp9cdTAwMGU+XHUwMDFjXHLuTreCne0vP1x1MDAxMvuCP1x1MDAwZX5pmDJGguZcXKAwelx1MDAwNPvogKHlXHUwMDFjXGJfTlx1MDAxYsefh/16cM65Klx1MDAxMftkmHBOw5LB/2Vf7Z00vsRna/31w62d/atccn5cdTAwMTmUXHUwMDAyfsdRcO2kKVx1MDAwYvxJeJNMQr6yelx1MDAxYfKNdMid4PNcdTAwMDM/6H95t3n2Z3vzc3x4dHP8YaNcIlx1MDAwZVx1MDAwZl478J1hloNBXHUwMDAx3FhrR4FvhGJIzi+ERUW+XHUwMDAxz8K9cYrXYVx1MDAxY/eC23HAXHUwMDFiUYS5UVxuLU1cdCxcdTAwMTflL0fxXHUwMDFl5dKAxlx1MDAwNVCeoalcdTAwMWQn+9FdmHLDyNnfg8uodTtcdTAwMDKJXHUwMDE0/mTgfrVcdTAwMWKG8Yr4b/xLM6rVwvif+Vx1MDAxOeuF9Pu+Qz36zfVW1PDOstpcbuujXpREpMOGzUk7N8ZVsiSg7rrbteJcdTAwMWS1u1EjioPW1+lWPc2ZXHUwMDAxi2eHYVxmOEVUoXF+bz4wx1f609ZBv9M53ZKbXHUwMDFmq+8/71x1MDAxZL52b7aOkXJDq1BobnKKNlxyYyiYoJNaodNKKlMwrFx1MDAxY29cdTAwMDbMOGTozblzXHUwMDBm3uxcZqCRSmZ38DeIWUpQzFi2N8PKL5Q2ReetcLI3g1x1MDAxYfnmkrw5b9VMb75cdTAwMWbmXHTuLCjgTPVnXHJcdTAwMTSXtMuJocf8efbML+DPxSD4gv5cZsYxdEpLZVx1MDAwNXJNideoQ2vrm41cdTAwMTDg6FJcdTAwMTC6YFo5XHUwMDFlrYk2hFBcdTAwMTKApsQ6nvHG0L9cdTAwMWRnXHUwMDE20GpDjsCdmaRRSVxccKKdJ0Tv1MxcdTAwMTn+PsMjlbJOLKJcInN2XHUwMDA03eRdXHUwMDE016K4QY1cdTAwMTmVfK8ybM9cdTAwMTElUlx1MDAxZq72vZWcXHTllPSJsybcWpVcdEs/XHUwMDE2QcdcdTAwMWLNQFhJk01aXmr/7+GKb0Ozwrj2uFGzM7CcUWucgeHcijTrp+R/kk2KQqexUjjpaO6tXHUwMDE5s6lcdTAwMTX0ko325WWU0Nh/bkdxUlx1MDAxY+N0MNe9ozfDYIxB6J7ybUVG6PhcdTAwMWVHiT17t5K5TPph+P5/bydePVx1MDAxNcv+XHUwMDE4Q3HW25v868JUZkFOT7BcdTAwMTUnW3CBPGO2XCJ9pUxmgUnUymjDSX7wQnXJSMtIXHUwMDBmWFx1MDAxNFx1MDAwMlx1MDAxMVxi/C9DZI5ZckJjuOFWTiQy5Vx1MDAxOGiN5H9cdTAwMDLISclZx5iMfIE6cFnDMojs6YnCnEQ2O3lcdTAwMWQhMs0taUhKXHRBccvR5i6651xmw1BrcjJCtsy50YIsNruGOmJcdTAwMTGXxPIkJo1cdTAwMTREU1x1MDAxM0js5+asabD1x9o4Ylx1MDAxN2StXHUwMDE5hUG0tnj2O29cdTAwMDHXXHUwMDE0yEDLjNlcdTAwMWXjrePT885e5Th+31x1MDAxZlxcRWu1TrR+dNF83Vx1MDAxOVx1MDAxNfi6oPWFXHUwMDExMFx1MDAwMimBzO72vihuXHUwMDE5+aZwzlx1MDAxMYNbmUsvn15cdTAwMTTXurzSoCNgSJJemdmlZVmPXHUwMDE1rrPg8nKFa6LGafjUToLBXFx6+Vx1MDAxODrXb+tcdTAwMDfBzqf1+LyyTTnCIN6CratS0VlcdTAwMGJ6zbBcXHgqYL5cXG2c4Vx1MDAwZYBcdTAwMTdcdTAwMTN+R0GXlI5cdTAwMDalwaJ2ZdStVYmVayFcdTAwMDWCUy+CzyFcdTAwMGJOqFx1MDAwMtTtYSVuXHUwMDFjbFx1MDAxY21tXZ9WKlx1MDAxZiuN3km/nCqAplx1MDAxMETiQC1cdTAwMDH9gtT4dFnprFEqP+WP0vOHs697h5+PzuqdXHUwMDAzPKq15Hn7Knnl9KzSlFx0lNdcdTAwMWFcblRu+S/twCGzaDSBTNNYQK75Kfi3UHVcIixz3caBXCI5vOxFS1x1MDAxYoZcdTAwMWb6XHUwMDFm7+5a56f6rI5JhLenfD70v53V77vbzul54/Bi1+3dbVx1MDAwZrqbUbR7Wi+h3+NrXHUwMDFi697B7mDQaZze1bu1qLr+R1x0/fbE+sGOXHUwMDFl3Fx1MDAwZWrrXCLYUdHazWkvLIdcdTAwMDW8KJDKubJYYFrJXHUwMDFi3dRcdTAwMDDo9Tl3Ml9Ee4xcdTAwMDFcdTAwMGX1p+jaXHUwMDFkh1eV3ubmn1j5uvm+rZ7CXHUwMDAwy0ssXHUwMDAxNVOCXCJcdTAwMWNcdTAwMTjKLa1Uo1x1MDAwYliCI6OMUyNpOGVcdTAwMTQ+s+ZccmDPw1x08kzpXHSppJxcdTAwMTDuaEpcdTAwMTRlTi9Q9J4lx5RxsFxiXHUwMDE0s1x1MDAxOc/K0mhcdTAwMTjZXHKgvVxmttqNXFwzLFJn+vd7kTrodFhvQJlb86yXVoZ/uX/BycXq3ELBMorVM6yb6Y/Ti9ZcdTAwMWPMNI90vvRHOkzM7ZCzma9cZod8XHRNqli6sMNcdH7kl06MeCRKzVxmSLBcXFhDI1JcdTAwMTRcdTAwMGLlOCQw37lUoFxyWIE4qdRjXHUwMDA1Q4da+1VlXHUwMDE0+dTtwVvRSikt8ieE5+dUeoBTQvlcdTAwMTRvnbPSM1vnreTrKs761X+Lylx1MDAxOcmlyVx1MDAxNVx1MDAxZVx1MDAxZVxuK4o5wf1FSFOluJFcdTAwMGZcdTAwMTdMKfWM3sVPVIKZjid/XHUwMDE0kZT19ib/+oRcdTAwMTWwnHuMhXdcdTAwMGUkLFx1MDAwNZ9f4M/WO6+TTYw0zO+70ki3i4iFXHUwMDA1MFx0TKVcdTAwMDOhTH7TVplUwpklia5IO5BzgsZcdFwi3zpcdTAwMDagUFJYXHUwMDE0xlx1MDAxYZlcdTAwMWKfXHUwMDA3Klx1MDAxMZo0XGKiWm7R2C836SetR5dNJWuCUTwgMVwiOWViXG5cdTAwMTByXHUwMDE33VOJZKTPNEpBI619reJvSiVrU1x1MDAwMeWPMSiVxiXcTS9cdTAwMTZcdTAwMTC1SePdaG4umZ3rvU4ukY5cdTAwMDaXK2JVXHUwMDFmzLRcdTAwMWRcdTAwMTUmNOSMa1x1MDAwZVx1MDAwNECjKVuXXHUwMDA1w8piXHUwMDEzpL7RXGItXHUwMDE1IV2bSUVcdTAwMDPHmfa0RmlcdTAwMDVcdTAwMDClNFx1MDAxM6SJcSgo21nyajrkPffHSlx1MDAxM6JcdTAwMTMgceaXRSjqar9vfTKlOO5cZo20X71cdTAwMWVfuv57UMpcZlD5Y1xmTlx1MDAwYnLKrJ3jZvqWO0cj75TUXHUwMDE57TxKKr/38fxz/Wj75FCfSLu373ai3dddgbSWM+VcdTAwMDR4tuZo5Gj5gSQzI6bn1ilcdTAwMDSH8nn7Z8tfXHUwMDFlks44Q1x1MDAxZbLkesSyloesnFx1MDAxZfJoSEg1gpj/mZ6dXHJcdTAwMGW7h4Ov8CFpVFx1MDAwNvWrvT+7W+9fOzolo2nXVlx1MDAwYidcdTAwMDGwXHUwMDEw8biXXCJCXHUwMDFiR6qMW8WfXHUwMDE38souj5NmJlx1MDAwNlEvUSxcdTAwMWJcdTAwMTLgXHUwMDEyq+NcdTAwMWLHXHUwMDFmZaRO63+Yg9v1rb3Eid8/XZRSbS7dpaZusJ7+iJyg8FwiXHSQXHUwMDBiXHUwMDE0t1x1MDAwME+qX9Q+bm13XHUwMDE0XHUwMDFjvW+a5ufaa5eQVvpcctbKbzJBa/Lrb2k6KvzOXHUwMDE3J8ibKFx1MDAxZlXuZfxcdCaloONcdTAwMWKshd/jrVx1MDAxNVx1MDAxN0t+XuLlcO6LY2ilWPrzXHUwMDEy+Cp3WOPzd1hLmPr4k/D79Ei32Pn3Jc6e+Ve5fOS0ZqBImVHex7WDwv5cdK2Ypmb/JIWw+efryvRnbVx1MDAxOGl30ipSS+B20sNQ1jFOXHUwMDEzXHUwMDAyXHUwMDAwXHUwMDBl1Eih68HXwVxuxV1+rW8ppeone+Oc+eDsXHUwMDEwMZJcdTAwMGZcbktJMeU3KFx1MDAxZFx0WpywXHUwMDA3XHUwMDEwaKa11ztaXHUwMDFhTjP+PVxyWnBb4mxcdTAwMTm4MrK5WlC+ZZVcdTAwMTbaot+GkD1dN7Rcblx1MDAxOKWupHGs81dKbse3fP9MmehULPujiOKsszf510VVicht8iqSXHUwMDE4aVx1MDAxMrA0vvPnoFti33aSRudT+6ZDaDpcYmrx2bspJNZcZqrNfjf88TpfkPDgQlxiIDCholxmf1ToXHUwMDFigcwh+ke+QPuc7zk8lnSDuNdcdLrkXHUwMDEwXHUwMDEztEkuxc20ybi058I/s/xcIlx1MDAwYuGztMmL7vuS4DRmkm9pT391w8v2dV7m/nBlktn0NF2SX+YperT0iSwp7Pk9evakL+TRS9zXXCItU9zHdK7AXHUwMDA3hsLzXHUwMDEyfl+LzzMoeZbGuGetos/0aFx1MDAwMaSB/Fx1MDAxNlNOXHUwMDFhSXOZ+48gsno1MFx1MDAwNc5vMjHoXHUwMDFmRCu6u0/wpXJmmdXq5/jjnOpkdqgoKFx1MDAwMUupo6LwbqSQOlx1MDAxYqJMnnCGXHUwMDE0XHUwMDE40Vx1MDAxMI+npamnqZPZu5hHXHUwMDE0XHUwMDEzd1xuaL6Ms1r6xYRxmyhqkDiRaK2jTFx1MDAwMIT6uZ/9mlx1MDAwZWV/rFx1MDAxNVE8TZ68efhcdTAwMDG/eWg/IcxccqeDYFx1MDAxZNVcdTAwMWWIPLvL1esoXHUwMDFjvJu0nzo9PEem4+mZKPT3+te3N9/+XHUwMDBmJe3LnyJ9 Screen 1(hidden)Screen 2 (visible)app.switch_screen(screen3)Screen 3 (visible)Screen 2 removed

Like pop_screen, if the screen being replaced is not installed it will be removed and deleted.

"},{"location":"guide/screens/#action_2","title":"Action","text":"

You can also switch screens with the \"app.switch_screen\" action which accepts the name of the screen to switch to.

"},{"location":"guide/screens/#screen-opacity","title":"Screen opacity","text":"

If a screen has a background color with an alpha component, then the background color will be blended with the screen beneath it. For example, if the top-most screen has a background set to rgba(0,0,255,0.5) then anywhere in the screen not occupied with a widget will display the second screen from the top, tinted with 50% blue.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nNVZ2VLjRlx1MDAxNH3nK1x1MDAxY+dlUlx1MDAwNZrel0mlUixhwizMXHUwMDAyXHUwMDE0Q5KplLDatsay5EhtMDPFv+dKXHUwMDE4tySvgJmFXHUwMDA3Y/dtt666zzn3XFz5y0aj0bRXXHUwMDAz03zWaJpRy4/CIPUvm5v5+IVJszCJIUSKz1kyTFvFzK61g+zZ06d9P+1cdTAwMTk7iPyW8S7CbOhHmVx1MDAxZFx1MDAwNmHitZL+09CafvZ7/nro981vg6RcdTAwMWbY1HNcdTAwMTfZMkFok/TmWiYyfVx1MDAxM9tcZlb/XHUwMDFiPjdcdTAwMWFfitdSdkHo95M4KKZcdTAwMTeBUnqK1kdcdTAwMGaTuEiVIEJcdTAwMTTmXHUwMDEyqcmMMNuDq1lcdTAwMTNAuFxyXHUwMDE5XHUwMDFiXHUwMDE3yYeaXHUwMDA3e6fPj7blaKsjRm/7r+WLbo+G7rLtMIqO7FVUJJUlcC8ultk06ZnTMLBdiOLa+Lxvpcmw041NllW+k1xm/FZor2CMo8mgXHUwMDFmd4ol3MhcYj5RwT2EhMKaXG4tKGVuO/LvXHUwMDBirDzKidKEUU0x4bW8dpNcYo5cdTAwMDHy+lx1MDAxOVx1MDAxNX8us3O/1etAenHg5mByrpRwcy7Hd8s095hiVLrluybsdG1xQMJTXHUwMDEyI12OZqY4XHUwMDAyrFx1MDAwNOFcYiniTii/5OAgKMDwsbxNcTDepnhcdTAwMThFLss88EdcdTAwMWRAZVx1MDAxMJVO99S+XHUwMDFh7n/OaHv44cxcdTAwMGbRXHQjvc7u5G4qiPPTNLlsTlwi1+N3LqPhIPBvcISF1Eoohlx1MDAxNEVu86Mw7tWTjZJWz0GvXHUwMDE4vd6cjXhrRnZcdTAwMTbctVx1MDAxNvPgzpXmikuyMti3tzvmg1x1MDAxY52/39k7XGbOXrB9/+jlm29cdHa1XGbskjOPYlx1MDAwZZCRhGvEZVx1MDAwNetKY48qXHUwMDA0jGBYYYJcdTAwMWaGdak5apNprONcdTAwMTIlJyDnuFx1MDAwZW2CqdSSUcF+cGhrJCnnWsg7QNthKIntUfj5Ro0ro/t+P4yuKkAoMFx1MDAwZlx07viZaWSt1Jj4n/jJwE9t6Edccqgx4XlkfimfWmYgl3zxkurlq2xHYSdnSzMy7SqNbFxiJWhcdTAwMTK2ycBFW5CVXHUwMDBmy6VcdTAwMDdB/e6SNOyEsVx1MDAxZlx1MDAxZK+W4UJm32z/XGZqY4llffiW21hJhDSidHV2L0bEXHUwMDFk2E1q4/dlN0ZL6S2FR6FqM62k4sLxt2C3VFDoXHUwMDE051goweRcdTAwMDMr2Vxcdlx1MDAwYk8rXHUwMDAypUpxRFx1MDAxMSup6YTrjIDKQFx1MDAwZZAoQpxxR/Ax9TlVipOy7VjOfEfpW6CQ8cj1fEFYUI1cdTAwMDT8x+w+lM0swHknjIMw7lRcdTAwMTNcdTAwMWL7tINcdTAwMTWKR0Hy1jDPclx1MDAwYnmMXHUwMDExTjCWXHUwMDFja02k5LQ0reNcdTAwMGbyrKlHJYg2lVx1MDAxNFx1MDAwYqZcdTAwMTGmU3dv4mB5Vov9Wy0rSYhcdTAwMDY0gUeUXHUwMDA03rNZWYGMc5goXHUwMDE54pzK6TOJ/MzuJv1+aGH73yZhbOvbXFzs53ZO+q7xp5RcdTAwMDXuqlx1MDAxY6urwyBfsSr+7l3D0af4MHn/cXPm7K254C6iU7h2622U/89cdTAwMTO2XHUwMDA1Jl2XqmDdpGMmKNdwXGYrK9v5q+10p3+xS+K93f1Pg97OSetcXH/nJl16YFx1MDAwZlx1MDAxNVx1MDAxMYJcdTAwMDFcdTAwMTFwzaRzykDaKEVcdTAwMWPioHO0ltfdpC2f0W6vz6QrTYG1uFR6XHUwMDFl08hkx1x1MDAwN0efgyx5d3T4qtc+XHUwMDE57tn/9vl6jFxmQUJAidGP7dFcdTAwMDWei3aMXHUwMDE1dDtSs9XR3rvAXHUwMDA3Q7/bZfzy9fut5+jw5cuT/lx1MDAxY7R3/VZ3mJrHxrtehnfCpKdcdTAwMTDCQlMmudBYVfDOoJTDXHUwMDE0qMFaXHUwMDAx9Vx1MDAxMWNcdTAwMGZcdTAwMDG8Tf04XHUwMDAzXHUwMDBmXHUwMDA26JpcdTAwMDY90WpcdTAwMWHt025cdTAwMWS6XHUwMDA1XHKtk6Lix1x1MDAwNzlXuX5/Nbd+nFxmtvpJZid+2Fx1MDAxZNGzRto595+gTbRJON9EXHUwMDFl/+XX78G+3zXl+/l5XVLMulxmXHUwMDEwiolAtESkZTKwXHUwMDE4MneSga9n6Fx1MDAxOZdcdTAwMWU4p7yJXHUwMDE0WKmyaS/qXHUwMDFlo56kXGaD+6JKXHUwMDEzVU9sfTJQ6Fx1MDAxMaRcdTAwMDCSxEB+S1x1MDAwZlx1MDAwZZytp55ASjFcZonkRdopwa1GIIrh4PSdXG7hWn19QW7KXHUwMDFm1dcvLjdVXHUwMDA3jYnWmGHwiphLrkt28tZAM3BcdTAwMWOC5s9cdTAwMWQpwlJSdT9bv9jxVZNC4HBcdTAwMTTO5VxcXG4o9nQ6K+VJXHUwMDAxglx1MDAwZolcdTAwMDMuOViCXHUwMDFm2tXPhXZcdTAwMTGsg/qOnr7Q51nahuY+hsR5h1x1MDAwMVxyk1hd20YjLsy74+fxv+/SreNR/81W/Fx1MDAxN/qWhn65slx0rj2Ru3VcZrZcdTAwMWSUy1GyeOqOmCeIgK6RsvyZ7MNcZn17lpsnTHlcdTAwMWFLaNhcdTAwMDDo4M/pXGafXHUwMDAzzPTAYyEmQHkxNFx1MDAxN072bjVccu6UcSRLT15cdTAwMWbqe5Y9XCJfh3jVyTYnsmZcdTAwMWFXYuvuzFn+qIFyUYNT/lx1MDAwN4XHQ0RcdTAwMTM4KyrhMMXS5SSUXZDbvJ/UnGJV0YRpVCxbTzFcdTAwMGauyzFcdTAwMDHTLlx1MDAxONOV5Th4JFx1MDAwNEJKsZZMwuJs2XLz9mYlRZrbdPFcdTAwMDVuXHUwMDBieMpcdTAwMDCBqyuSPbeH5oCEn9jhi1x1MDAxM3N0XHUwMDE2nqG389zWN1Mk7oHOglx1MDAwMHFcIjlUV+S+lytcdTAwMTRcdTAwMTfYg6BWgFx1MDAxYlx1MDAwNWfmXHUwMDA0u1AoXHUwMDAxVVx1MDAxMLxcZlVcdTAwMWO8XHUwMDE3zF2/QuGST3GS5LrvsVx1MDAwNFx0XHUwMDAyXaBcdTAwMDJ3+OhcbpT/kiFcdFbuXu/XXHUwMDFilaxhpTeqNjH5xvxpoihpnCZpXHUwMDE0/DSz8Sn9RvU1XHUwMDFhn0o+NzTbXHUwMDE407TpXHUwMDBmXHUwMDA2R1x1MDAxNnZrYsPgXHUwMDFjwmB8y27V5kVoLndmYyCHwcaYujlHTOGArzeu/1x1MDAwN0PH3J0ifQ== Base screen(partial visible)Top-most screenbackground: rgba(0,0,255,0.5);Hello World!

Note

Although parts of other screens may be made visible with background alpha, only the top-most is active (can respond to mouse and keyboard).

One use of background alpha is to style modal dialogs (see below).

"},{"location":"guide/screens/#modal-screens","title":"Modal screens","text":"

Screens may be used to create modal dialogs, where the main interface is temporarily disabled (but still visible) while the user is entering information.

The following example pushes a screen when you hit the Q key to ask you if you really want to quit. From the quit screen you can click either Quit to exit the app immediately, or Cancel to dismiss the screen and return to the main screen.

OutputOutput (after pressing Q)modal01.pymodal01.tcss

ModalApp \u2b58ModalApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.I\u00a0must\u00a0not\u00a0f Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2585\u2585 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.I\u00a0must\u00a0not\u00a0f Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.I\u00a0must\u00a0not\u00a0f Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. \u00a0Q\u00a0\u00a0Quit\u00a0

ModalApp \u2588\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2588 \u2588\u2588 \u2588\u2588 \u2588Are\u00a0you\u00a0sure\u00a0you\u00a0want\u00a0to\u00a0quit?\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2588 \u2588\u00a0Quit\u00a0\u00a0Cancel\u00a0\u2588 \u2588\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2588 \u2588\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2588

modal01.py
from textual.app import App, ComposeResult\nfrom textual.containers import Grid\nfrom textual.screen import Screen\nfrom textual.widgets import Button, Footer, Header, Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass QuitScreen(Screen):\n\"\"\"Screen with a dialog to quit.\"\"\"\ndef compose(self) -> ComposeResult:\nyield Grid(\nLabel(\"Are you sure you want to quit?\", id=\"question\"),\nButton(\"Quit\", variant=\"error\", id=\"quit\"),\nButton(\"Cancel\", variant=\"primary\", id=\"cancel\"),\nid=\"dialog\",\n)\ndef on_button_pressed(self, event: Button.Pressed) -> None:\nif event.button.id == \"quit\":\nself.app.exit()\nelse:\nself.app.pop_screen()\nclass ModalApp(App):\n\"\"\"An app with a modal dialog.\"\"\"\nCSS_PATH = \"modal01.tcss\"\nBINDINGS = [(\"q\", \"request_quit\", \"Quit\")]\ndef compose(self) -> ComposeResult:\nyield Header()\nyield Label(TEXT * 8)\nyield Footer()\ndef action_request_quit(self) -> None:\nself.push_screen(QuitScreen())\nif __name__ == \"__main__\":\napp = ModalApp()\napp.run()\n
modal01.tcss
QuitScreen {\nalign: center middle;\n}\n#dialog {\ngrid-size: 2;\ngrid-gutter: 1 2;\ngrid-rows: 1fr 3;\npadding: 0 1;\nwidth: 60;\nheight: 11;\nborder: thick $background 80%;\nbackground: $surface;\n}\n#question {\ncolumn-span: 2;\nheight: 1fr;\nwidth: 1fr;\ncontent-align: center middle;\n}\nButton {\nwidth: 100%;\n}\n

Note the request_quit action in the app which pushes a new instance of QuitScreen. This makes the quit screen active. If you click Cancel, the quit screen calls pop_screen to return the default screen. This also removes and deletes the QuitScreen object.

There are two flaws with this modal screen, which we can fix in the same way.

The first flaw is that the app adds a new quit screen every time you press Q, even when the quit screen is still visible. Consequently if you press Q three times, you will have to click Cancel three times to get back to the main screen. This is because bindings defined on App are always checked, and we call push_screen for every press of Q.

The second flaw is that the modal dialog doesn't look modal. There is no indication that the main interface is still there, waiting to become active again.

We can solve both those issues by replacing our use of Screen with ModalScreen. This screen sub-class will prevent key bindings on the app from being processed. It also sets a background with a little alpha to allow the previous screen to show through.

Let's see what happens when we use ModalScreen.

OutputOutput (after pressing Q)modal02.pymodal01.tcss

ModalApp \u2b58ModalApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.I\u00a0must\u00a0not\u00a0f Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2585\u2585 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.I\u00a0must\u00a0not\u00a0f Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.I\u00a0must\u00a0not\u00a0f Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. \u00a0Q\u00a0\u00a0Quit\u00a0

ModalApp \u2b58ModalApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0i\u2588\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2588 Where\u00a0the\u00a0\u2588\u2588st\u00a0not\u00a0f Fear\u00a0is\u00a0th\u2588\u2588 Fear\u00a0is\u00a0th\u2588Are\u00a0you\u00a0sure\u00a0you\u00a0want\u00a0to\u00a0quit?\u2588 I\u00a0will\u00a0fac\u2588\u2588 I\u00a0will\u00a0per\u2588\u2588\u2585\u2585 And\u00a0when\u00a0i\u2588\u2588 Where\u00a0the\u00a0\u2588\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2588st\u00a0not\u00a0f Fear\u00a0is\u00a0th\u2588\u00a0Quit\u00a0\u00a0Cancel\u00a0\u2588 Fear\u00a0is\u00a0th\u2588\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2588 I\u00a0will\u00a0fac\u2588\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2588 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.I\u00a0must\u00a0not\u00a0f Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. \u00a0Q\u00a0\u00a0Quit\u00a0

modal02.py
from textual.app import App, ComposeResult\nfrom textual.containers import Grid\nfrom textual.screen import ModalScreen\nfrom textual.widgets import Button, Footer, Header, Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass QuitScreen(ModalScreen):\n\"\"\"Screen with a dialog to quit.\"\"\"\ndef compose(self) -> ComposeResult:\nyield Grid(\nLabel(\"Are you sure you want to quit?\", id=\"question\"),\nButton(\"Quit\", variant=\"error\", id=\"quit\"),\nButton(\"Cancel\", variant=\"primary\", id=\"cancel\"),\nid=\"dialog\",\n)\ndef on_button_pressed(self, event: Button.Pressed) -> None:\nif event.button.id == \"quit\":\nself.app.exit()\nelse:\nself.app.pop_screen()\nclass ModalApp(App):\n\"\"\"An app with a modal dialog.\"\"\"\nCSS_PATH = \"modal01.tcss\"\nBINDINGS = [(\"q\", \"request_quit\", \"Quit\")]\ndef compose(self) -> ComposeResult:\nyield Header()\nyield Label(TEXT * 8)\nyield Footer()\ndef action_request_quit(self) -> None:\n\"\"\"Action to display the quit dialog.\"\"\"\nself.push_screen(QuitScreen())\nif __name__ == \"__main__\":\napp = ModalApp()\napp.run()\n
modal01.tcss
QuitScreen {\nalign: center middle;\n}\n#dialog {\ngrid-size: 2;\ngrid-gutter: 1 2;\ngrid-rows: 1fr 3;\npadding: 0 1;\nwidth: 60;\nheight: 11;\nborder: thick $background 80%;\nbackground: $surface;\n}\n#question {\ncolumn-span: 2;\nheight: 1fr;\nwidth: 1fr;\ncontent-align: center middle;\n}\nButton {\nwidth: 100%;\n}\n

Now when we press Q, the dialog is displayed over the main screen. The main screen is darkened to indicate to the user that it is not active, and only the dialog will respond to input.

"},{"location":"guide/screens/#returning-data-from-screens","title":"Returning data from screens","text":"

It is a common requirement for screens to be able to return data. For instance, you may want a screen to show a dialog and have the result of that dialog processed after the screen has been popped.

To return data from a screen, call dismiss() on the screen with the data you wish to return. This will pop the screen and invoke a callback set when the screen was pushed (with push_screen).

Let's modify the previous example to use dismiss rather than an explicit pop_screen.

modal03.pymodal01.tcss modal03.py
from textual.app import App, ComposeResult\nfrom textual.containers import Grid\nfrom textual.screen import ModalScreen\nfrom textual.widgets import Button, Footer, Header, Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass QuitScreen(ModalScreen[bool]):  # (1)!\n\"\"\"Screen with a dialog to quit.\"\"\"\ndef compose(self) -> ComposeResult:\nyield Grid(\nLabel(\"Are you sure you want to quit?\", id=\"question\"),\nButton(\"Quit\", variant=\"error\", id=\"quit\"),\nButton(\"Cancel\", variant=\"primary\", id=\"cancel\"),\nid=\"dialog\",\n)\ndef on_button_pressed(self, event: Button.Pressed) -> None:\nif event.button.id == \"quit\":\nself.dismiss(True)\nelse:\nself.dismiss(False)\nclass ModalApp(App):\n\"\"\"An app with a modal dialog.\"\"\"\nCSS_PATH = \"modal01.tcss\"\nBINDINGS = [(\"q\", \"request_quit\", \"Quit\")]\ndef compose(self) -> ComposeResult:\nyield Header()\nyield Label(TEXT * 8)\nyield Footer()\ndef action_request_quit(self) -> None:\n\"\"\"Action to display the quit dialog.\"\"\"\ndef check_quit(quit: bool) -> None:\n\"\"\"Called when QuitScreen is dismissed.\"\"\"\nif quit:\nself.exit()\nself.push_screen(QuitScreen(), check_quit)\nif __name__ == \"__main__\":\napp = ModalApp()\napp.run()\n
  1. See below for an explanation of the [bool]
modal01.tcss
QuitScreen {\nalign: center middle;\n}\n#dialog {\ngrid-size: 2;\ngrid-gutter: 1 2;\ngrid-rows: 1fr 3;\npadding: 0 1;\nwidth: 60;\nheight: 11;\nborder: thick $background 80%;\nbackground: $surface;\n}\n#question {\ncolumn-span: 2;\nheight: 1fr;\nwidth: 1fr;\ncontent-align: center middle;\n}\nButton {\nwidth: 100%;\n}\n

In the on_button_pressed message handler we call dismiss with a boolean that indicates if the user has chosen to quit the app. This boolean is passed to the check_quit function we provided when QuitScreen was pushed.

Although this example behaves the same as the previous code, it is more flexible because it has removed responsibility for exiting from the modal screen to the caller. This makes it easier for the app to perform any cleanup actions prior to exiting, for example.

Returning data in this way can help keep your code manageable by making it easy to re-use your Screen classes in other contexts.

"},{"location":"guide/screens/#typing-screen-results","title":"Typing screen results","text":"

You may have noticed in the previous example that we changed the base class to ModalScreen[bool]. The addition of [bool] adds typing information that tells the type checker to expect a boolean in the call to dismiss, and that any callback set in push_screen should also expect the same type. As always, typing is optional in Textual, but this may help you catch bugs.

"},{"location":"guide/screens/#modes","title":"Modes","text":"

Some apps may benefit from having multiple screen stacks, rather than just one. Consider an app with a dashboard screen, a settings screen, and a help screen. These are independent in the sense that we don't want to prevent the user from switching between them, even if there are one or more modal screens on the screen stack. But we may still want each individual screen to have a navigation stack where we can push and pop screens.

In Textual we can manage this with modes. A mode is simply a named screen stack, which we can switch between as required. When we switch modes, the topmost screen in the new mode becomes the active visible screen.

The following diagram illustrates such an app with modes. On startup the app switches to the \"dashboard\" mode which makes the top of the stack visible.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO2aW0/bSFx1MDAxNMff+ylQ9mVXKu7cL5VWK6ClXHUwMDA1UmhJuZRtVTn2JPHGsY3tJEDFd99jw8ZcdTAwMTdcYiSkXHUwMDA0Km1cdTAwMWWCPWfsOTPz+885M+HHi5WVRnpcdTAwMWWZxuuVhjlzbN9zY3vceJmVj0yceGFcdTAwMDAmkt8n4TB28pq9NI2S169eXHLsuG/SyLdcdTAwMWRjjbxkaPtJOnS90HLCwSsvNYPkr+x711x1MDAxZZg/o3DgprFVNLJqXFwvXHLjq7aMb1x1MDAwNiZIXHUwMDEzePvfcL+y8iP/LnnnevYgXGbcvHpuKLmnab10N1xmclcx5YpcdTAwMTOJNZrU8JI30FpqXFwwd8BjU1iyosaRjYZ7zeh78+JTW9uO2t48+/ChaLbj+X4rPfdzp5JcdTAwMTD6UtiSNFx1MDAwZfvmyHPTXtZ2rXzaU3E47PZcdTAwMDKTJJVnwsh2vPRcdTAwMWPKeOG7XHUwMDFkdPNXXHUwMDE0JWdwtyotjVx00pxcdTAwMTFOlGKMTMxXz1OLXHUwMDEwwlx1MDAwNYyT0lxuXHUwMDA2pObYRujDPIBjv6H8U7jWtp1+XHUwMDE3/Fx1MDAwYtyiXHUwMDBl5rbd7lx1MDAxNHXG190lQlpMM1x1MDAwMe1ThbVWk1x1MDAxYT3jdXtp1jvOLCWxkFxiXHUwMDBiTqUo/DD5dGioXHUwMDAwb2BsYshcdTAwMWGPttyci2/lXHUwMDExXHUwMDBi3OtcdTAwMTFcdTAwMGKGvl/4m1x1MDAxOd6WWCqeXHUwMDE5Rq59NelYwDhQxbGkolx1MDAxOCnfXHUwMDBi+vXX+aHTLzjJSy9fzo0n43wqnoogISijbGY8t9+hQ9U8+dTc2Wz7zY3Drffn0f5T4onRvXxyXHUwMDBi5lRrwpiQmFAlK3zChFtcdTAwMDIhSiSlSmjJXHUwMDE2wrNjt1x1MDAxMeKPgyehjGOt0Fx1MDAxMvBkQiMk+Vx1MDAxMvBUpaGo4amFXCLgzVx1MDAxY3SK3ogo01x1MDAxMu6J8665ftzdx1x1MDAwM3fzmdOJLYyBTC44V1xmRlx1MDAxZtEqnlhCXHUwMDA1ilx1MDAxOPArXHUwMDA1XHUwMDExdLHlU1x1MDAxMUdj8yh8YkJcdTAwMTiGJYUsXHUwMDAxUIZcdTAwMTFF0NxcdTAwMTJcdTAwMDClkkxcdTAwMDM0XHUwMDBiaowjiWdcdTAwMDd0dEC3u1EoRu/eXHUwMDFlOKnauaCO/5SA0vv4lDhcdTAwMDOUQ1dcdTAwMDVDWlx1MDAxMF6hkyNkMWCTaSGZYrTu1nxwtlxya7vtXz22g4gxXHUwMDEzVC6DzVIwuFx1MDAxMduZZJrCnM1cZufnL/2NcNPZ+4L39b67ZtZcdTAwMDej95+fNZxUUEtcbqmk1Fx1MDAwMsM3rcGpLSWIJCBhzLhaiM2OK1xyZv+zOTObnN3BJkJKc0g8Z2bT/b56vLcrm+GgXHUwMDFk+73xXHUwMDA2e+tvnjw3Ni3IXCJplkUykucuvFx1MDAwNiu3lIZcdTAwMDSTMYlcdTAwMTFcdTAwMTdcdTAwMTVWmURgXHUwMDE08Fx1MDAxNIxccqRcdTAwMDJ0IViZI0znl89CfzasqTlLbyNcdTAwMTWXwlaNVMVhaYG1Q85cZqp/SFtvdk9OTtaJcS76slx1MDAxZnL1aVxuqD3b6VxyY/P0SShcdTAwMTOWXHUwMDEwkFxcMlx1MDAwNmkoIZKzXG6cXHUwMDEySVx1MDAwYlZRyHUgxSOYL5aCTovymCuLaq6xlkAmYvImnOXk91xuR4Ipw1x1MDAxY5V20o+II+TmisyTc1x1MDAxNtNcdTAwMWVcdTAwMDZpy7vIk0ZVKd20XHUwMDA3nn9embmcU1x1MDAxOKmvXHLXTnrt0I7dr41Gxbzme90gx810qkynnmP7XHUwMDEzc1x1MDAxYUaF1YHmbC8w8ZZbdzuMva5cdTAwMTfY/ue7m4ZcdTAwMWWb95OVwiolg207MZk1z4pcdTAwMWakQqBrarzAXGa2o4ry2eOFQVx0+uLt75zzXvfirFx1MDAxZq7udFx1MDAwZbaeVobsPlx1MDAxNSpMsr0ggTVcdTAwMDfyadiD10SoLYIo5lx1MDAxYWFcYjNqsXOKaVwiXHUwMDE0ylx1MDAxMopqXGJRXGJyJlXKSZ6NXGKh92iek4lFRdgzfrR8/dVbfVTpTVx1MDAwZoDZgVx1MDAxOFwipdbuXHUwMDEz3sH2If9cdTAwMThcdTAwMWSGp7J1vrHzXHUwMDBmYmunb5vPXFx4jHKLYlx1MDAwNblcdTAwMWJHnGOlqvuIXFx5XHUwMDA0Q1qnRCa/R1xuf0RZsIvWRCDCqS6r6dlIXHUwMDBmKTlXOrao9Fx1MDAxMpOmXtBNli+/21p+TFx0lnO0mzt5yLnpPLsl0Vx1MDAxMuHx+MBcdTAwMWZ8XGLGXFydXlx1MDAxY1x1MDAwZtvjvYeJkNTKXHUwMDFmL1x0JcRCXHUwMDAy4p5cdTAwMDagQVx1MDAwNJRU41x1MDAxZtHEXHUwMDAyocLOKtNcdTAwMDfEpukqVLLD21x1MDAwZlShVlkrXHUwMDEwZJDimpaOwO9cdTAwMTChUOAz4z9vS3RtKPgpze1ol1x1MDAxY21/POqf4vFWa20zSnbtuNjqVWCz4zhcdTAwMWM3JpbL66s7XHUwMDE0rjQofJ5fpVx1MDAxNlP4mpN6I7Py+8hLvLZv/liuyqe3/jOUfjX4t0mdsHrpROqwXHUwMDA3k0rL2bebd9PwJEqX91x0PYt0QnPKXHUwMDE51Vx1MDAxNFx1MDAxM1ngklx1MDAxZqxAMOZIXHSuXGLnTEl2x1HIXHUwMDAyQkdcdTAwMTZWXHUwMDFjgStMZb9OXHUwMDEzSW45XGZhzJKIXHUwMDExgYXQWGB9U/lCQFx1MDAwZlx1MDAxOCqWqvulX2j6P1bIdcnlg6LywzWbpHacrnuBXHUwMDBika7q2PU/RGzNXHUwMDEwTXKVO8PMy1VkIcmFJJpjXGJZTFx1MDAxNucm2cjYUbbJgSqUXG5CKMKSiJtdN4FbuFTthZ2kXHUwMDFi4WDgpdD/j6FcdTAwMTek9Vx1MDAxYXmH1jLh9Yx9Q//w5rKtrtAoe2N1+S2uVlxuhvObyfW3l7fWXr2Dr+xzg6zihS/Kf7M1O2+iYUdRK4WZn0xcdTAwMTSg5rnXS27Rz8bIM+P121x1MDAwZbDzT1x1MDAxNlxy8rHOllx1MDAwNpPjePni8l9cdTAwMDSHyVx1MDAxMCJ9 \"dashboard\"\"help\"\"settings\"Active (visible)

If we later change the mode to \"settings\", the top of that mode's screen stack becomes visible.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN2a2VLbSFx1MDAxNIbv81x1MDAxNJTnNii9L6mammJfMmxcdTAwMDFcdTAwMDJkkkrJUttWLEtCkrFJinef01xuYy1gNrNlfGHsPrL6tPr7z1wi8fPN3FxcKz9PTOv9XFzLjD03XGb81Fx1MDAxZLXe2vEzk2ZBXHUwMDFjgYlcdTAwMTTfs3iYesWRvTxPsvfv3lxy3LRv8iR0PeOcXHUwMDA12dBccrN86Fx1MDAwN7HjxYN3QW5cdTAwMDbZX/Z921x1MDAxZJg/k3jg56lTTjJv/CCP019zmdBcZkyUZ3D2f+D73NzP4r3inVx1MDAxZriDOPKLw1x1MDAwYkPpXHUwMDFlJbw5ulx1MDAxZEeFq5xcdIlcdTAwMTlcIlx1MDAxM3uQLcNcXLnxwdhcdTAwMDF/TWmxQ62j3Z01s7iXu9m8TpLTlW/JsEPLSTtBXHUwMDE47ufnYeFSXHUwMDE2w0pKW5ancd9cdTAwMWNcdTAwMDV+3lx1MDAwMytujE/7VVx1MDAxYVx1MDAwZru9yGRZ7Tdx4npBfm5cdTAwMTeHJoNu1C1OUY6M7Y80dbDWmGtcIpggQtOJ2f6eKe4wXCK1XHUwMDA2XHUwMDEzZVxcNdxaikPYXHUwMDAzcOtcdTAwMGZUvErH2q7X74J3kV9cdTAwMWWDueu2O+Uxo8vFXHUwMDEyIVx1MDAxZKaZUIxRXHUwMDA1zpSz9EzQ7eXWTc5cdTAwMWMlsZBcYlx1MDAwYk6lKP0wxWbAXHUwMDAy7Fx1MDAxOVx1MDAxOJtcdTAwMTjs5MmGXzDxtXq9XCL/8npFwzAs/bWGlSZHVZYq27y/sX7a3/Hzle1w72Rk0sWTxW97k3XVwHPTNFx1MDAxZbUmlovLT6VHw8R3f1x1MDAwMYXh4iuGXHUwMDA1oVKWSIZB1G86XHUwMDFixl6/ZLBcdTAwMTi9eHtv8JlcdTAwMTLTwFx1MDAwNyooJVx1MDAwMle24jb0+3LpQ1x1MDAxYeJvx6j9PTzoxt3R9+HWK0dcdTAwMWbYXHUwMDE2XHUwMDAwNiGMXHUwMDEy1Fx1MDAwMJ9cdEchLlx1MDAwNVx1MDAxMoLCzrCZyO+4bYT405BPQJewUejRyH9ccmxqoqexSTVBmHBy96jMR8nR1tjfpOM12U+Xu0ujT/HglaOpXHUwMDFkiLlSaURcdTAwMTmSVNbYpGClWDFFuCRIK8ZnglNcdTAwMTFPY/MkcGKQXHUwMDE2xoqQ/1x1MDAxN520kiWbJYOWmFx1MDAxM87Znek88Vx1MDAwZdaOt7zOp7PNY7W1tDFYWF7cfkk62W10akxcdTAwMWNCXHUwMDA0x5hSpKSqwVx0VDpcdTAwMWHUXHTVhKRcdTAwMTji60xstlxya/vt36RkuFx1MDAxMU0hlHpcdTAwMTY01TQ0MVKwYCwqaf82NlPlkf7ybrhcdTAwMWLujE829lx1MDAwZvn6SftF61mMboOTXHUwMDBiu+1cdTAwMWFTXHUwMDBiqNSkXHUwMDFlOpkmXHUwMDBlXHUwMDEyXG5ziVx1MDAxONdcdTAwMTQ1/bpnWvelwez3p1x1MDAxM1xuXHUwMDFjhckz0Mn59F6LXG6GIKKgO8OZeWp1NzvrbVxmP6ydjr6vXHUwMDA0KplfeXVwOohcdTAwMTIoppVgRFx1MDAwYlx1MDAwNVA2aJVcdTAwMGXCSENcdTAwMDcmXHUwMDEwpFx1MDAwZVanlUNzhlxixlx1MDAwMlxuXHUwMDFlTORsRSjzhOn8XHUwMDBmitDHpTU34/w6VGGSqYGUI4FcdTAwMTnn8u7dUZet7+xcdTAwMWRcdTAwMWStRHtYXHUwMDFk8uHpeEzaXHUwMDBiU1jtuV5vmJpcdTAwMTdP84QpXHUwMDA3NpxcdMKVRErX2Vx1MDAxNLZcYoXuiCPIKZg+UZ7HXFw5VHONtVx1MDAwNDBcdTAwMTGTV9mkvEkjwZTZPVwiz0GjJlCG03vQWG56XHUwMDFj5fvBXHUwMDBme+GJqo2uuoMgPK/tW4EpXFypLy3fzXrt2E39L61WzbxcdTAwMTBcdTAwMDZdS24rNJ060nngueHEnMdJafVgOjeITLrhN92O06BcdTAwMWJEbnhw89SwYrM+XHRcdTAwMTRO5W5a282MtdpcdTAwMDXyXHUwMDA3ibBcdTAwMWHymlwiXHUwMDE0UHpcIoird+9cdTAwMDPnPZxcdTAwMWZcdTAwMWZ8Xo/ib5tcdTAwMWZHW6uHy8qLXrlcYjFEXFyHKi6FgFpcdTAwMDZcdTAwMTFdr2ckkkWG4IgwTrSa7Vx1MDAwNt00XHUwMDE1XG7lXGJFNWPgXHUwMDAwZqpSlLxcdTAwMWFcdTAwMTVCI4Lv0/rNqsKeXHST51x1MDAxN2Bz1ifVnsDN0Yn2JKOSQ69992Ltx+r4cIVcdTAwMDdq6cTdPmDR2s5cdTAwMGZ/8PFltXd7LyEocygniDJbc6Ayxf2SXHUwMDFlNLpSI6lcdTAwMTDngqjZWompXHSQKEdcdTAwMGLoY1x1MDAwNII0o6tyejXaI5yx+9Rjs2ovM3lcdTAwMWVE3ez59XfdzE+pQYxlc7TMf1RqXHUwMDAxjcHd85//cXfps4/mN8+S4OPSh8V2XHUwMDEwz88/TIOkMf6EXHUwMDFhZNpB0MkjibWwbVJNhERxh2PMidJQqaLqfbcrKlSyw9tcdTAwMGZToYI4QDn0b1xiQ9yrdlx1MDAxZjeIUEDJXGZtwuP1RJeGa1x1MDAxZknhwd/n/vKmiFx1MDAwZldcdTAwMGU6cZxiubpQ3pmowXb/R1Ka2j/PpvBcdTAwMDUvXHUwMDBmzszzars552Oo+teFvk7WeuqTN4K0JETf48HbzTv/XCKqlreK2lxuXG60LyhcdTAwMTSuiuD6LWRcbjlPSlvTaY5cdTAwMTWYb3i+MYOoqVx1MDAwMz08gTDKtOQgVVROM1G11I5iXHUwMDFjXHUwMDBlXHUwMDAxzdubhldFblx1MDAxZlx1MDAwZlx1MDAxMlapXHJuV3kp3/9QIZcjXHUwMDE3XHUwMDBmLH5cdTAwMWYqzyx303wxiHxIanXHLv+lYuNcdTAwMGWJo1x1MDAxMLQ3tF5cIlx1MDAwNyvrk6BcblOoQVx1MDAxMKGVo7puYmOpXHUwMDAztVxmWFx1MDAxMId2XHUwMDFlXHUwMDEyXHUwMDE4u7J2XHUwMDEz+aVP9WW4Wb5cdTAwMTRcdTAwMGZcdTAwMDZBXHUwMDBlXHUwMDE3YDdcdTAwMGWivHlEsaJcdTAwMDUrvJ5xr6hcdTAwMWXOXFy1NVx1MDAxNZrYM9ZDbflprmS4+DL5/PXttUdPx8u+roBVnu5N9a+NzsVcdTAwMDQtN0n2c9j4yT5cdTAwMDFpgX9cdTAwMTlcXMtVts5cdTAwMDIzWrzuZnXxsnG/uNI2MJiCxos3XHUwMDE3/1x1MDAwMiHc1HkifQ== \"dashboard\"\"help\"\"settings\"Active

To add modes to your app, define a MODES class variable in your App class which should be a dict that maps the name of the mode on to either a screen object, a callable that returns a screen, or the name of an installed screen. However you specify it, the values in MODES set the base screen for each mode's screen stack.

You can switch between these screens at any time by calling App.switch_mode. When you switch to a new mode, the topmost screen in the new stack becomes visible. Any calls to App.push_screen or App.pop_screen will affect only the active mode.

Let's look at an example with modes:

modes01.pyOutputOutput (after pressing S)
from textual.app import App, ComposeResult\nfrom textual.screen import Screen\nfrom textual.widgets import Footer, Placeholder\nclass DashboardScreen(Screen):\ndef compose(self) -> ComposeResult:\nyield Placeholder(\"Dashboard Screen\")\nyield Footer()\nclass SettingsScreen(Screen):\ndef compose(self) -> ComposeResult:\nyield Placeholder(\"Settings Screen\")\nyield Footer()\nclass HelpScreen(Screen):\ndef compose(self) -> ComposeResult:\nyield Placeholder(\"Help Screen\")\nyield Footer()\nclass ModesApp(App):\nBINDINGS = [\n(\"d\", \"switch_mode('dashboard')\", \"Dashboard\"),  # (1)!\n(\"s\", \"switch_mode('settings')\", \"Settings\"),\n(\"h\", \"switch_mode('help')\", \"Help\"),\n]\nMODES = {\n\"dashboard\": DashboardScreen,  # (2)!\n\"settings\": SettingsScreen,\n\"help\": HelpScreen,\n}\ndef on_mount(self) -> None:\nself.switch_mode(\"dashboard\")  # (3)!\nif __name__ == \"__main__\":\napp = ModesApp()\napp.run()\n
  1. switch_mode is a builtin action to switch modes.
  2. Associates DashboardScreen with the name \"dashboard\".
  3. Switches to the dashboard mode.

ModesApp Dashboard\u00a0Screen \u00a0D\u00a0\u00a0Dashboard\u00a0\u00a0S\u00a0\u00a0Settings\u00a0\u00a0H\u00a0\u00a0Help\u00a0

ModesApp Settings\u00a0Screen \u00a0D\u00a0\u00a0Dashboard\u00a0\u00a0S\u00a0\u00a0Settings\u00a0\u00a0H\u00a0\u00a0Help\u00a0

Here we have defined three screens. One for a dashboard, one for settings, and one for help. We've bound keys to each of these screens, so the user can switch between the screens.

Pressing D, S, or H switches between these modes.

"},{"location":"guide/styles/","title":"Styles","text":"

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

"},{"location":"guide/styles/#styles-object","title":"Styles object","text":"

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\nclass ScreenApp(App):\ndef on_mount(self) -> None:\nself.screen.styles.background = \"darkblue\"\nself.screen.styles.border = (\"heavy\", \"white\")\nif __name__ == \"__main__\":\napp = ScreenApp()\napp.run()\n

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 \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

"},{"location":"guide/styles/#styling-widgets","title":"Styling widgets","text":"

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\nfrom textual.widgets import Static\nclass WidgetApp(App):\ndef compose(self) -> ComposeResult:\nself.widget = Static(\"Textual\")\nyield self.widget\ndef on_mount(self) -> None:\nself.widget.styles.background = \"darkblue\"\nself.widget.styles.border = (\"heavy\", \"white\")\nif __name__ == \"__main__\":\napp = WidgetApp()\napp.run()\n

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 \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Textual\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

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.

"},{"location":"guide/styles/#colors","title":"Colors","text":"

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\"\n

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\nfrom textual.color import Color\nfrom textual.widgets import Static\nclass ColorApp(App):\ndef compose(self) -> ComposeResult:\nself.widget1 = Static(\"Textual One\")\nyield self.widget1\nself.widget2 = Static(\"Textual Two\")\nyield self.widget2\nself.widget3 = Static(\"Textual Three\")\nyield self.widget3\ndef on_mount(self) -> None:\nself.widget1.styles.background = \"#9932CC\"\nself.widget2.styles.background = \"hsl(150,42.9%,49.4%)\"\nself.widget2.styles.color = \"blue\"\nself.widget3.styles.background = Color(191, 78, 96)\nif __name__ == \"__main__\":\napp = ColorApp()\napp.run()\n

Here is the output:

ColorApp Textual\u00a0One Textual\u00a0Two Textual\u00a0Three

"},{"location":"guide/styles/#alpha","title":"Alpha","text":"

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\nfrom textual.color import Color\nfrom textual.widgets import Static\nclass ColorApp(App):\ndef compose(self) -> ComposeResult:\nself.widgets = [Static(\"\") for n in range(10)]\nyield from self.widgets\ndef on_mount(self) -> None:\nfor index, widget in enumerate(self.widgets, 1):\nalpha = index * 0.1\nwidget.update(f\"alpha={alpha:.1f}\")\nwidget.styles.background = Color(191, 78, 96, a=alpha)\nif __name__ == \"__main__\":\napp = ColorApp()\napp.run()\n

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

"},{"location":"guide/styles/#dimensions","title":"Dimensions","text":"

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

"},{"location":"guide/styles/#box-model","title":"Box Model","text":"

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"},{"location":"guide/styles/#width-and-height","title":"Width and height","text":"

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\nfrom textual.widgets import Static\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass DimensionsApp(App):\ndef compose(self) -> ComposeResult:\nself.widget = Static(TEXT)\nyield self.widget\ndef on_mount(self) -> None:\nself.widget.styles.background = \"purple\"\nself.widget.styles.width = 30\nself.widget.styles.height = 10\nif __name__ == \"__main__\":\napp = DimensionsApp()\napp.run()\n

This code produces the following result.

DimensionsApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0 brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0 me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0 will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see its\u00a0path.

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

"},{"location":"guide/styles/#auto-dimensions","title":"Auto dimensions","text":"

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\nfrom textual.widgets import Static\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass DimensionsApp(App):\ndef compose(self) -> ComposeResult:\nself.widget = Static(TEXT)\nyield self.widget\ndef on_mount(self) -> None:\nself.widget.styles.background = \"purple\"\nself.widget.styles.width = 30\nself.widget.styles.height = \"auto\"\nif __name__ == \"__main__\":\napp = DimensionsApp()\napp.run()\n

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

DimensionsApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0 brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0 me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0 will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0 will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0 remain.

"},{"location":"guide/styles/#units","title":"Units","text":"

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\nfrom textual.widgets import Static\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass DimensionsApp(App):\ndef compose(self) -> ComposeResult:\nself.widget = Static(TEXT)\nyield self.widget\ndef on_mount(self) -> None:\nself.widget.styles.background = \"purple\"\nself.widget.styles.width = \"50%\"\nself.widget.styles.height = \"80%\"\nif __name__ == \"__main__\":\napp = DimensionsApp()\napp.run()\n

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

60 x 2080 x 30120 x 40

DimensionsApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0 brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0 me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0 will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0 will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0 remain.

DimensionsApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0 total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0 through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0 the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0 nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.

DimensionsApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0 its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0 remain.

"},{"location":"guide/styles/#fr-units","title":"FR units","text":"

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\nfrom textual.widgets import Static\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass DimensionsApp(App):\ndef compose(self) -> ComposeResult:\nself.widget1 = Static(TEXT)\nyield self.widget1\nself.widget2 = Static(TEXT)\nyield self.widget2\ndef on_mount(self) -> None:\nself.widget1.styles.background = \"purple\"\nself.widget2.styles.background = \"darkgreen\"\nself.widget1.styles.height = \"2fr\"\nself.widget2.styles.height = \"1fr\"\nif __name__ == \"__main__\":\napp = DimensionsApp()\napp.run()\n

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\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain. I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.

"},{"location":"guide/styles/#maximum-and-minimums","title":"Maximum and minimums","text":"

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.

  • min-width sets a minimum width.
  • max-width sets a maximum width.
  • min-height sets a minimum height.
  • max-height sets a maximum height.
"},{"location":"guide/styles/#padding","title":"Padding","text":"

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\nfrom textual.widgets import Static\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass PaddingApp(App):\ndef compose(self) -> ComposeResult:\nself.widget = Static(TEXT)\nyield self.widget\ndef on_mount(self) -> None:\nself.widget.styles.background = \"purple\"\nself.widget.styles.width = 30\nself.widget.styles.padding = 2\nif __name__ == \"__main__\":\napp = PaddingApp()\napp.run()\n

Notice the additional space around the text:

PaddingApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0 that\u00a0brings\u00a0total\u00a0 obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0 over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past, I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0 to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0 there\u00a0will\u00a0be\u00a0nothing.\u00a0 Only\u00a0I\u00a0will\u00a0remain.

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\nfrom textual.widgets import Static\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass PaddingApp(App):\ndef compose(self) -> ComposeResult:\nself.widget = Static(TEXT)\nyield self.widget\ndef on_mount(self) -> None:\nself.widget.styles.background = \"purple\"\nself.widget.styles.width = 30\nself.widget.styles.padding = (2, 4)\nif __name__ == \"__main__\":\napp = PaddingApp()\napp.run()\n

Compare the output of this example to the previous example:

PaddingApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0 mind-killer. Fear\u00a0is\u00a0the\u00a0 little-death\u00a0that\u00a0 brings\u00a0total\u00a0 obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0 pass\u00a0over\u00a0me\u00a0and\u00a0 through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0 past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0 inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0 path. Where\u00a0the\u00a0fear\u00a0has\u00a0 gone\u00a0there\u00a0will\u00a0be\u00a0 nothing.\u00a0Only\u00a0I\u00a0will\u00a0 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.

"},{"location":"guide/styles/#border","title":"Border","text":"

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\nfrom textual.widgets import Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass BorderApp(App):\ndef compose(self) -> ComposeResult:\nself.widget = Label(TEXT)\nyield self.widget\ndef on_mount(self) -> None:\nself.widget.styles.background = \"darkblue\"\nself.widget.styles.width = \"50%\"\nself.widget.styles.border = (\"heavy\", \"yellow\")\nif __name__ == \"__main__\":\napp = BorderApp()\napp.run()\n

Here is the result:

BorderApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503I\u00a0must\u00a0not\u00a0fear.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0mind-killer.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0\u2503 \u2503total\u00a0obliteration.\u2503 \u2503I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2503 \u2503I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0\u2503 \u2503through\u00a0me.\u2503 \u2503And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0\u2503 \u2503the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.\u2503 \u2503Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0\u2503 \u2503nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

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

textual borders\n
"},{"location":"guide/styles/#title-alignment","title":"Title alignment","text":"

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\".

  • border-title-align sets the alignment of the title, which defaults to \"left\".
  • border-subtitle-align sets the alignment of the subtitle, which defaults to \"right\".

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

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass BorderTitleApp(App[None]):\ndef compose(self) -> ComposeResult:\nself.widget = Static(TEXT)\nyield self.widget\ndef on_mount(self) -> None:\nself.widget.styles.background = \"darkblue\"\nself.widget.styles.width = \"50%\"\nself.widget.styles.border = (\"heavy\", \"yellow\")\nself.widget.border_title = \"Litany Against Fear\"\nself.widget.border_subtitle = \"by Frank Herbert, in \u201cDune\u201d\"\nself.widget.styles.border_title_align = \"center\"\nif __name__ == \"__main__\":\napp = BorderTitleApp()\napp.run()\n

Note the addition of the titles and their alignments:

BorderTitleApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0Litany\u00a0Against\u00a0Fear\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503I\u00a0must\u00a0not\u00a0fear.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0mind-killer.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0\u2503 \u2503total\u00a0obliteration.\u2503 \u2503I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2503 \u2503I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0\u2503 \u2503through\u00a0me.\u2503 \u2503And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0\u2503 \u2503the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.\u2503 \u2503Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0\u2503 \u2503nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0by\u00a0Frank\u00a0Herbert,\u00a0in\u00a0\u201cDune\u201d\u00a0\u2501\u251b

"},{"location":"guide/styles/#outline","title":"Outline","text":"

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\nfrom textual.widgets import Static\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass OutlineApp(App):\ndef compose(self) -> ComposeResult:\nself.widget = Static(TEXT)\nyield self.widget\ndef on_mount(self) -> None:\nself.widget.styles.background = \"darkblue\"\nself.widget.styles.width = \"50%\"\nself.widget.styles.outline = (\"heavy\", \"yellow\")\nif __name__ == \"__main__\":\napp = OutlineApp()\napp.run()\n

Notice how the outline overlaps the text in the widget.

OutlineApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503ear\u00a0is\u00a0the\u00a0mind-killer.\u2503 \u2503ear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0\u2503 \u2503otal\u00a0obliteration.\u2503 \u2503\u00a0will\u00a0face\u00a0my\u00a0fear.\u2503 \u2503\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0\u2503 \u2503hrough\u00a0me.\u2503 \u2503nd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0\u2503 \u2503he\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.\u2503 \u2503here\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

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

"},{"location":"guide/styles/#box-sizing","title":"Box sizing","text":"

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 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 to the box model for border-box.

content-boxborder-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\nfrom textual.widgets import Static\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass BoxSizing(App):\ndef compose(self) -> ComposeResult:\nself.widget1 = Static(TEXT)\nyield self.widget1\nself.widget2 = Static(TEXT)\nyield self.widget2\ndef on_mount(self) -> None:\nself.widget1.styles.background = \"purple\"\nself.widget2.styles.background = \"darkgreen\"\nself.widget1.styles.width = 30\nself.widget2.styles.width = 30\nself.widget1.styles.height = 6\nself.widget2.styles.height = 6\nself.widget1.styles.border = (\"heavy\", \"white\")\nself.widget2.styles.border = (\"heavy\", \"white\")\nself.widget1.styles.padding = 1\nself.widget2.styles.padding = 1\nself.widget2.styles.box_sizing = \"content-box\"\nif __name__ == \"__main__\":\napp = BoxSizing()\napp.run()\n

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 \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503I\u00a0must\u00a0not\u00a0fear.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0mind-killer.\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503I\u00a0must\u00a0not\u00a0fear.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0mind-killer.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0\u2503 \u2503brings\u00a0total\u00a0obliteration.\u2503 \u2503I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2503 \u2503I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

"},{"location":"guide/styles/#margin","title":"Margin","text":"

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\nfrom textual.widgets import Static\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass MarginApp(App):\ndef compose(self) -> ComposeResult:\nself.widget1 = Static(TEXT)\nyield self.widget1\nself.widget2 = Static(TEXT)\nyield self.widget2\ndef on_mount(self) -> None:\nself.widget1.styles.background = \"purple\"\nself.widget2.styles.background = \"darkgreen\"\nself.widget1.styles.border = (\"heavy\", \"white\")\nself.widget2.styles.border = (\"heavy\", \"white\")\nself.widget1.styles.margin = 2\nself.widget2.styles.margin = 2\nif __name__ == \"__main__\":\napp = MarginApp()\napp.run()\n

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

MarginApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503I\u00a0must\u00a0not\u00a0fear.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0mind-killer.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2503 \u2503I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2503 \u2503I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2503 \u2503And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.\u2503 \u2503Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503I\u00a0must\u00a0not\u00a0fear.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0mind-killer.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2503 \u2503I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2503 \u2503I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2503 \u2503And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.\u2503 \u2503Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

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.

"},{"location":"guide/styles/#more-styles","title":"More styles","text":"

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.

"},{"location":"guide/testing/","title":"Testing","text":"

Code testing is an important part of software development. This chapter will cover how to write tests for your Textual apps.

"},{"location":"guide/testing/#what-is-testing","title":"What is testing?","text":"

It is common to write tests alongside your app. A test is simply a function that confirms your app is working correctly.

Learn more about testing

We recommend Python Testing with pytest for a comprehensive guide to writing tests.

"},{"location":"guide/testing/#do-you-need-to-write-tests","title":"Do you need to write tests?","text":"

The short answer is \"no\", you don't need to write tests.

In practice however, it is almost always a good idea to write tests. Writing code that is completely bug free is virtually impossible, even for experienced developers. If you want to have confidence that your application will run as you intended it to, then you should write tests. Your test code will help you find bugs early, and alert you if you accidentally break something in the future.

"},{"location":"guide/testing/#testing-frameworks-for-textual","title":"Testing frameworks for Textual","text":"

Textual doesn't require any particular test framework. You can use any test framework you are familiar with, but we will be using pytest in this chapter.

"},{"location":"guide/testing/#testing-apps","title":"Testing apps","text":"

You can often test Textual code in the same way as any other app, and use similar techniques. But when testing user interface interactions, you may need to use Textual's dedicated test features.

Let's write a simple Textual app so we can demonstrate how to test it. The following app shows three buttons labelled \"red\", \"green\", and \"blue\". Clicking one of those buttons or pressing a corresponding R, G, and B key will change the background color.

rgb.pyOutput
from textual import on\nfrom textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.widgets import Button, Footer\nclass RGBApp(App):\nCSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    Horizontal {\n        width: auto;\n        height: auto;\n    }\n    \"\"\"\nBINDINGS = [\n(\"r\", \"switch_color('red')\", \"Go Red\"),\n(\"g\", \"switch_color('green')\", \"Go Green\"),\n(\"b\", \"switch_color('blue')\", \"Go Blue\"),\n]\ndef compose(self) -> ComposeResult:\nwith Horizontal():\nyield Button(\"Red\", id=\"red\")\nyield Button(\"Green\", id=\"green\")\nyield Button(\"Blue\", id=\"blue\")\nyield Footer()\n@on(Button.Pressed)\ndef pressed_button(self, event: Button.Pressed) -> None:\nassert event.button.id is not None\nself.action_switch_color(event.button.id)\ndef action_switch_color(self, color: str) -> None:\nself.screen.styles.background = color\nif __name__ == \"__main__\":\napp = RGBApp()\napp.run()\n

RGBApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Red\u00a0\u00a0Green\u00a0\u00a0Blue\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u00a0R\u00a0\u00a0Go\u00a0Red\u00a0\u00a0G\u00a0\u00a0Go\u00a0Green\u00a0\u00a0B\u00a0\u00a0Go\u00a0Blue\u00a0

Although it is straightforward to test an app like this manually, it is not practical to click every button and hit every key in your app after changing a single line of code. Tests allow us to automate such testing so we can quickly simulate user interactions and check the result.

To test our simple app we will use the run_test() method on the App class. This replaces the usual call to run() and will run the app in headless mode, which prevents Textual from updating the terminal but otherwise behaves as normal.

The run_test() method is an async context manager which returns a Pilot object. You can use this object to interact with the app as if you were operating it with a keyboard and mouse.

Let's look at the tests for the example above:

test_rgb.py
from rgb import RGBApp\nfrom textual.color import Color\nasync def test_keys():  # (1)!\n\"\"\"Test pressing keys has the desired result.\"\"\"\napp = RGBApp()\nasync with app.run_test() as pilot:  # (2)!\n# Test pressing the R key\nawait pilot.press(\"r\")  # (3)!\nassert app.screen.styles.background == Color.parse(\"red\")  # (4)!\n# Test pressing the G key\nawait pilot.press(\"g\")\nassert app.screen.styles.background == Color.parse(\"green\")\n# Test pressing the B key\nawait pilot.press(\"b\")\nassert app.screen.styles.background == Color.parse(\"blue\")\n# Test pressing the X key\nawait pilot.press(\"x\")\n# No binding (so no change to the color)\nassert app.screen.styles.background == Color.parse(\"blue\")\nasync def test_buttons():\n\"\"\"Test pressing keys has the desired result.\"\"\"\napp = RGBApp()\nasync with app.run_test() as pilot:\n# Test clicking the \"red\" button\nawait pilot.click(\"#red\")  # (5)!\nassert app.screen.styles.background == Color.parse(\"red\")\n# Test clicking the \"green\" button\nawait pilot.click(\"#green\")\nassert app.screen.styles.background == Color.parse(\"green\")\n# Test clicking the \"blue\" button\nawait pilot.click(\"#blue\")\nassert app.screen.styles.background == Color.parse(\"blue\")\n
  1. The run_test() method requires that it run in a coroutine, so tests must use the async keyword.
  2. This runs the app and returns a Pilot instance we can use to interact with it.
  3. Simulates pressing the R key.
  4. This checks that pressing the R key has resulted in the background color changing.
  5. Simulates clicking on the widget with an id of red (the button labelled \"Red\").

There are two tests defined in test_rgb.py. The first to test keys and the second to test button clicks. Both tests first construct an instance of the app and then call run_test() to get a Pilot object. The test_keys function simulates key presses with Pilot.press, and test_buttons simulates button clicks with Pilot.click.

After simulating a user interaction, Textual tests will typically check the state has been updated with an assert statement. The pytest module will record any failures of these assert statements as a test fail.

If you run the tests with pytest test_rgb.py you should get 2 passes, which will confirm that the user will be able to click buttons or press the keys to change the background color.

If you later update this app, and accidentally break this functionality, one or more of your tests will fail. Knowing which test has failed will help you quickly track down where your code was broken.

"},{"location":"guide/testing/#simulating-key-presses","title":"Simulating key presses","text":"

We've seen how the press method simulates keys. You can also supply multiple keys to simulate the user typing in to the app. Here's an example of simulating the user typing the word \"hello\".

await pilot.press(\"h\", \"e\", \"l\", \"l\", \"o\")\n

Each string creates a single keypress. You can also use the name for non-printable keys (such as \"enter\") and the \"ctrl+\" modifier. These are the same identifiers as used for key events, which you can experiment with by running textual keys.

"},{"location":"guide/testing/#simulating-clicks","title":"Simulating clicks","text":"

You can simulate mouse clicks in a similar way with Pilot.click. If you supply a CSS selector Textual will simulate clicking on the matching widget.

Note

If there is another widget in front of the widget you want to click, you may end up clicking the topmost widget rather than the widget indicated in the selector. This is generally what you want, because a real user would experience the same thing.

"},{"location":"guide/testing/#clicking-the-screen","title":"Clicking the screen","text":"

If you don't supply a CSS selector, then the click will be relative to the screen. For example, the following simulates a click at (0, 0):

await pilot.click()\n
"},{"location":"guide/testing/#click-offsets","title":"Click offsets","text":"

If you supply an offset value, it will be added to the coordinates of the simulated click. For example the following line would simulate a click at the coordinates (10, 5).

await pilot.click(offset=(10, 5))\n

If you combine this with a selector, then the offset will be relative to the widget. Here's how you would click the line above a button.

await pilot.click(Button, offset(0, -1))\n
"},{"location":"guide/testing/#modifier-keys","title":"Modifier keys","text":"

You can simulate clicks in combination with modifier keys, by setting the shift, meta, or control parameters. Here's how you could simulate ctrl-clicking a widget with an ID of \"slider\":

await pilot.click(\"#slider\", control=True)\n
"},{"location":"guide/testing/#changing-the-screen-size","title":"Changing the screen size","text":"

The default size of a simulated app is (80, 24). You may want to test what happens when the app has a different size. To do this, set the size parameter of run_test to a different size. For example, here is how you would simulate a terminal resized to 100 columns and 50 lines:

async with app.run_test(size=(100, 50)) as pilot:\n...\n
"},{"location":"guide/testing/#pausing-the-pilot","title":"Pausing the pilot","text":"

Some actions in a Textual app won't change the state immediately. For instance, messages may take a moment to bubble from the widget that sent them. If you were to post a message and immediately assert you may find that it fails because the message hasn't yet been processed.

You can generally solve this by calling pause() which will wait for all pending messages to be processed. You can also supply a delay parameter, which will insert a delay prior to waiting for pending messages.

"},{"location":"guide/testing/#textuals-tests","title":"Textual's tests","text":"

Textual itself has a large battery of tests. If you are interested in how we write tests, see the tests/ directory in the Textual repository.

"},{"location":"guide/testing/#snapshot-testing","title":"Snapshot testing","text":"

A snapshot is a record of what an application looked like at a given point in time.

Snapshot testing is the process of creating a snapshot of an application while a test runs, and comparing it to a historical version. If there's a mismatch, the snapshot testing framework flags it for review.

This offers a simple, automated way of checking our application displays like we expect.

"},{"location":"guide/testing/#pytest-textual-snapshot","title":"pytest-textual-snapshot","text":"

You can use pytest-textual-snapshot to snapshot test your Textual app. This is a plugin for pytest which adds support for snapshot testing Textual apps, and it's maintained by the developers of Textual.

A test using this package saves a snapshot (in this case, an SVG screenshot) of a running Textual app to disk. The next time the test runs, it takes another snapshot and compares it to the previously saved one. If the snapshots differ, the test fails, and you can view a side-by-side diff showing the visual change.

"},{"location":"guide/testing/#installation","title":"Installation","text":"

You can install pytest-textual-snapshot using your favorite package manager (pip, poetry, etc.).

pip install pytest-textual-snapshot\n
"},{"location":"guide/testing/#creating-a-snapshot-test","title":"Creating a snapshot test","text":"

With the package installed, you now have access to the snap_compare pytest fixture.

Let's look at an example of how we'd create a snapshot test for the calculator app below.

CalculatorApp \u257a\u2501\u2513\u00a0\u00a0\u2513\u00a0\u257b\u00a0\u257b\u250f\u2501\u2578\u250f\u2501\u2513\u257a\u2501\u2513 \u00a0\u2501\u252b\u00a0\u00a0\u2503\u00a0\u2517\u2501\u252b\u2517\u2501\u2513\u2517\u2501\u252b\u250f\u2501\u251b \u257a\u2501\u251b.\u257a\u253b\u2578\u00a0\u00a0\u2579\u257a\u2501\u251b\u257a\u2501\u251b\u2517\u2501\u2578 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0C\u00a0\u00a0+/-\u00a0\u00a0%\u00a0\u00a0\u00f7\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a07\u00a0\u00a08\u00a0\u00a09\u00a0\u00a0\u00d7\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a04\u00a0\u00a05\u00a0\u00a06\u00a0\u00a0-\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a01\u00a0\u00a02\u00a0\u00a03\u00a0\u00a0+\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a00\u00a0\u00a0.\u00a0\u00a0=\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

First, we need to create a new test and specify the path to the Python file containing the app. This path should be relative to the location of the test.

def test_calculator(snap_compare):\nassert snap_compare(\"path/to/calculator.py\")\n

Let's run the test as normal using pytest.

pytest\n

When this test runs for the first time, an SVG screenshot of the calculator app is generated, and the test will fail. Snapshot tests always fail on the first run, since there's no previous version to compare the snapshot to.

If you open the snapshot report in your browser, you'll see something like this:

Tip

You can usually open the link directly from the terminal, but some terminal emulators may require you to hold Ctrl or Cmd while clicking for links to work.

The report explains that there's \"No history for this test\". It's our job to validate that the initial snapshot looks correct before proceeding. Our calculator is rendering as we expect, so we'll save this snapshot:

pytest --snapshot-update\n

Warning

Only ever run pytest with --snapshot-update if you're happy with how the output looks on the left hand side of the snapshot report. When using --snapshot-update, you're saying \"I'm happy with all of the screenshots in the snapshot test report, and they will now represent the ground truth which all future runs will be compared against\". As such, you should only run pytest --snapshot-update after running pytest and confirming the output looks good.

Now that our snapshot is saved, if we run pytest (with no arguments) again, the test will pass. This is because the screenshot taken during this test run matches the one we saved earlier.

"},{"location":"guide/testing/#catching-a-bug","title":"Catching a bug","text":"

The real power of snapshot testing comes from its ability to catch visual regressions which could otherwise easily be missed.

Imagine a new developer joins your team, and tries to make a few changes to the calculator. While making this change they accidentally break some styling which removes the orange coloring from the buttons on the right of the app. When they run pytest, they're presented with a report which reveals the damage:

On the right, we can see our \"historical\" snapshot - this is the one we saved earlier. On the left is how our app is currently rendering - clearly not how we intended!

We can click the \"Show difference\" toggle at the top right of the diff to overlay the two versions:

This reveals another problem, which could easily be missed in a quick visual inspection - our new developer has also deleted the number 4!

Tip

Snapshot tests work well in CI on all supported operating systems, and the snapshot report is just an HTML file which can be exported as a build artifact.

"},{"location":"guide/testing/#pressing-keys","title":"Pressing keys","text":"

You can simulate pressing keys before the snapshot is captured using the press parameter.

def test_calculator_pressing_numbers(snap_compare):\nassert snap_compare(\"path/to/calculator.py\", press=[\"1\", \"2\", \"3\"])\n
"},{"location":"guide/testing/#changing-the-terminal-size","title":"Changing the terminal size","text":"

To capture the snapshot with a different terminal size, pass a tuple (width, height) as the terminal_size parameter.

def test_calculator(snap_compare):\nassert snap_compare(\"path/to/calculator.py\", terminal_size=(50, 100))\n
"},{"location":"guide/testing/#running-setup-code","title":"Running setup code","text":"

You can also run arbitrary code before the snapshot is captured using the run_before parameter.

In this example, we use run_before to hover the mouse cursor over the widget with ID number-5 before taking the snapshot.

def test_calculator_hover_number(snap_compare):\nasync def run_before(pilot) -> None:\nawait pilot.hover(\"#number-5\")\nassert snap_compare(\"path/to/calculator.py\", run_before=run_before)\n

For more information, visit the pytest-textual-snapshot repo on GitHub.

"},{"location":"guide/widgets/","title":"Widgets","text":"

In this chapter we will explore widgets in more detail, and how you can create custom widgets of your own.

"},{"location":"guide/widgets/#what-is-a-widget","title":"What is a widget?","text":"

A widget is a component of your UI responsible for managing a rectangular region of the screen. Widgets may respond to events in much the same way as an app. In many respects, widgets are like mini-apps.

Information

Every widget runs in its own asyncio task.

"},{"location":"guide/widgets/#custom-widgets","title":"Custom widgets","text":"

There is a growing collection of builtin widgets in Textual, but you can build entirely custom widgets that work in the same way.

The first step in building a widget is to import and extend a widget class. This can either be Widget which is the base class of all widgets, or one of its subclasses.

Let's create a simple custom widget to display a greeting.

hello01.py
from textual.app import App, ComposeResult, RenderResult\nfrom textual.widget import Widget\nclass Hello(Widget):\n\"\"\"Display a greeting.\"\"\"\ndef render(self) -> RenderResult:\nreturn \"Hello, [b]World[/b]!\"\nclass CustomApp(App):\ndef compose(self) -> ComposeResult:\nyield Hello()\nif __name__ == \"__main__\":\napp = CustomApp()\napp.run()\n

The three highlighted lines define a custom widget class with just a render() method. Textual will display whatever is returned from render in the content area of your widget. We have returned a string in the code above, but there are other possible return types which we will cover later.

Note that the text contains tags in square brackets, i.e. [b]. This is console markup which allows you to embed various styles within your content. If you run this you will find that World is in bold.

CustomApp Hello,\u00a0World!

This (very simple) custom widget may be styled in the same way as builtin widgets, and targeted with CSS. Let's add some CSS to this app.

hello02.pyhello02.tcss hello02.py
from textual.app import App, ComposeResult, RenderResult\nfrom textual.widget import Widget\nclass Hello(Widget):\n\"\"\"Display a greeting.\"\"\"\ndef render(self) -> RenderResult:\nreturn \"Hello, [b]World[/b]!\"\nclass CustomApp(App):\nCSS_PATH = \"hello02.tcss\"\ndef compose(self) -> ComposeResult:\nyield Hello()\nif __name__ == \"__main__\":\napp = CustomApp()\napp.run()\n
hello02.tcss
Screen {\nalign: center middle;\n}\nHello {\nwidth: 40;\nheight: 9;\npadding: 1 2;\nbackground: $panel;\ncolor: $text;\nborder: $secondary tall;\ncontent-align: center middle;\n}\n

The addition of the CSS has completely transformed our custom widget.

CustomApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258aHello,\u00a0World!\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

"},{"location":"guide/widgets/#static-widget","title":"Static widget","text":"

While you can extend the Widget class, a subclass will typically be a better starting point. The Static class is a widget subclass which caches the result of render, and provides an update() method to update the content area.

Let's use Static to create a widget which cycles through \"hello\" in various languages.

hello03.pyhello03.tcssOutput hello03.py
from itertools import cycle\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Static\nhellos = cycle(\n[\n\"Hola\",\n\"Bonjour\",\n\"Guten tag\",\n\"Salve\",\n\"N\u01d0n h\u01ceo\",\n\"Ol\u00e1\",\n\"Asalaam alaikum\",\n\"Konnichiwa\",\n\"Anyoung haseyo\",\n\"Zdravstvuyte\",\n\"Hello\",\n]\n)\nclass Hello(Static):\n\"\"\"Display a greeting.\"\"\"\ndef on_mount(self) -> None:\nself.next_word()\ndef on_click(self) -> None:\nself.next_word()\ndef next_word(self) -> None:\n\"\"\"Get a new hello and update the content area.\"\"\"\nhello = next(hellos)\nself.update(f\"{hello}, [b]World[/b]!\")\nclass CustomApp(App):\nCSS_PATH = \"hello03.tcss\"\ndef compose(self) -> ComposeResult:\nyield Hello()\nif __name__ == \"__main__\":\napp = CustomApp()\napp.run()\n
hello03.tcss
Screen {\nalign: center middle;\n}\nHello {\nwidth: 40;\nheight: 9;\npadding: 1 2;\nbackground: $panel;\nborder: $secondary tall;\ncontent-align: center middle;\n}\n

CustomApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258aHola,\u00a0World!\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

Note that there is no render() method on this widget. The Static class is handling the render for us. Instead we call update() when we want to update the content within the widget.

The next_word method updates the greeting. We call this method from the mount handler to get the first word, and from a click handler to cycle through the greetings when we click the widget.

"},{"location":"guide/widgets/#default-css","title":"Default CSS","text":"

When building an app it is best to keep your CSS in an external file. This allows you to see all your CSS in one place, and to enable live editing. However if you intend to distribute a widget (via PyPI for instance) it can be convenient to bundle the code and CSS together. You can do this by adding a DEFAULT_CSS class variable inside your widget class.

Textual's builtin widgets bundle CSS in this way, which is why you can see nicely styled widgets without having to copy any CSS code.

Here's the Hello example again, this time the widget has embedded default CSS:

hello04.pyhello04.tcssOutput hello04.py
from itertools import cycle\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Static\nhellos = cycle(\n[\n\"Hola\",\n\"Bonjour\",\n\"Guten tag\",\n\"Salve\",\n\"N\u01d0n h\u01ceo\",\n\"Ol\u00e1\",\n\"Asalaam alaikum\",\n\"Konnichiwa\",\n\"Anyoung haseyo\",\n\"Zdravstvuyte\",\n\"Hello\",\n]\n)\nclass Hello(Static):\n\"\"\"Display a greeting.\"\"\"\nDEFAULT_CSS = \"\"\"\n    Hello {\n        width: 40;\n        height: 9;\n        padding: 1 2;\n        background: $panel;\n        border: $secondary tall;\n        content-align: center middle;\n    }\n    \"\"\"\ndef on_mount(self) -> None:\nself.next_word()\ndef on_click(self) -> None:\nself.next_word()\ndef next_word(self) -> None:\n\"\"\"Get a new hello and update the content area.\"\"\"\nhello = next(hellos)\nself.update(f\"{hello}, [b]World[/b]!\")\nclass CustomApp(App):\nCSS_PATH = \"hello04.tcss\"\ndef compose(self) -> ComposeResult:\nyield Hello()\nif __name__ == \"__main__\":\napp = CustomApp()\napp.run()\n
hello04.tcss
Screen {\nalign: center middle;\n}\n

CustomApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258aHola,\u00a0World!\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

"},{"location":"guide/widgets/#scoped-css","title":"Scoped CSS","text":"

Default CSS is scoped by default. All this means is that CSS defined in DEFAULT_CSS will affect the widget and potentially its children only. This is to prevent you from inadvertently breaking an unrelated widget.

You can disabled scoped CSS by setting the class var SCOPED_CSS to False.

"},{"location":"guide/widgets/#default-specificity","title":"Default specificity","text":"

CSS defined within DEFAULT_CSS has an automatically lower specificity than CSS read from either the App's CSS class variable or an external stylesheet. In practice this means that your app's CSS will take precedence over any CSS bundled with widgets.

"},{"location":"guide/widgets/#text-links","title":"Text links","text":"

Text in a widget may be marked up with links which perform an action when clicked. Links in console markup use the following format:

\"Click [@click='app.bell']Me[/]\"\n

The @click tag introduces a click handler, which runs the app.bell action.

Let's use markup links in the hello example so that the greeting becomes a link which updates the widget.

hello05.pyhello05.tcssOutput hello05.py
from itertools import cycle\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Static\nhellos = cycle(\n[\n\"Hola\",\n\"Bonjour\",\n\"Guten tag\",\n\"Salve\",\n\"N\u01d0n h\u01ceo\",\n\"Ol\u00e1\",\n\"Asalaam alaikum\",\n\"Konnichiwa\",\n\"Anyoung haseyo\",\n\"Zdravstvuyte\",\n\"Hello\",\n]\n)\nclass Hello(Static):\n\"\"\"Display a greeting.\"\"\"\ndef on_mount(self) -> None:\nself.action_next_word()\ndef action_next_word(self) -> None:\n\"\"\"Get a new hello and update the content area.\"\"\"\nhello = next(hellos)\nself.update(f\"[@click='next_word']{hello}[/], [b]World[/b]!\")\nclass CustomApp(App):\nCSS_PATH = \"hello05.tcss\"\ndef compose(self) -> ComposeResult:\nyield Hello()\nif __name__ == \"__main__\":\napp = CustomApp()\napp.run()\n
hello05.tcss
Screen {\nalign: center middle;\n}\nHello {\nwidth: 40;\nheight: 9;\npadding: 1 2;\nbackground: $panel;\nborder: $secondary tall;\ncontent-align: center middle;\n}\n

CustomApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258aHola,\u00a0World!\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

If you run this example you will see that the greeting has been underlined, which indicates it is clickable. If you click on the greeting it will run the next_word action which updates the next word.

"},{"location":"guide/widgets/#border-titles","title":"Border titles","text":"

Every widget has a border_title and border_subtitle attribute. Setting border_title will display text within the top border, and setting border_subtitle will display text within the bottom border.

Note

Border titles will only display if the widget has a border enabled.

The default value for these attributes is empty string, which disables the title. You can change the default value for the title attributes with the BORDER_TITLE and BORDER_SUBTITLE class variables.

Let's demonstrate setting a title, both as a class variable and a instance variable:

hello06.pyhello06.tcssOutput hello06.py
from itertools import cycle\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Static\nhellos = cycle(\n[\n\"Hola\",\n\"Bonjour\",\n\"Guten tag\",\n\"Salve\",\n\"N\u01d0n h\u01ceo\",\n\"Ol\u00e1\",\n\"Asalaam alaikum\",\n\"Konnichiwa\",\n\"Anyoung haseyo\",\n\"Zdravstvuyte\",\n\"Hello\",\n]\n)\nclass Hello(Static):\n\"\"\"Display a greeting.\"\"\"\nBORDER_TITLE = \"Hello Widget\"  # (1)!\ndef on_mount(self) -> None:\nself.action_next_word()\nself.border_subtitle = \"Click for next hello\"  # (2)!\ndef action_next_word(self) -> None:\n\"\"\"Get a new hello and update the content area.\"\"\"\nhello = next(hellos)\nself.update(f\"[@click='next_word']{hello}[/], [b]World[/b]!\")\nclass CustomApp(App):\nCSS_PATH = \"hello05.tcss\"\ndef compose(self) -> ComposeResult:\nyield Hello()\nif __name__ == \"__main__\":\napp = CustomApp()\napp.run()\n
  1. Setting the default for the title attribute via class variable.
  2. Setting subtitle via an instance attribute.
hello06.tcss
Screen {\nalign: center middle;\n}\nHello {\nwidth: 40;\nheight: 9;\npadding: 1 2;\nbackground: $panel;\nborder: $secondary tall;\ncontent-align: center middle;\n}\n

CustomApp \u258a\u2594\u00a0Hello\u00a0Widget\u00a0\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258aHola,\u00a0World!\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u00a0Click\u00a0for\u00a0next\u00a0hello\u00a0\u2581\u258e

Note that titles are limited to a single line of text. If the supplied text is too long to fit within the widget, it will be cropped (and an ellipsis added).

There are a number of styles that influence how titles are displayed (color and alignment). See the style reference for details.

"},{"location":"guide/widgets/#rich-renderables","title":"Rich renderables","text":"

In previous examples we've set strings as content for Widgets. You can also use special objects called renderables for advanced visuals. You can use any renderable defined in Rich or third party libraries.

Lets make a widget that uses a Rich table for its content. The following app is a solution to the classic fizzbuzz problem often used to screen software engineers in job interviews. The problem is this: Count up from 1 to 100, when the number is divisible by 3, output \"fizz\"; when the number is divisible by 5, output \"buzz\"; and when the number is divisible by both 3 and 5 output \"fizzbuzz\".

This app will \"play\" fizz buzz by displaying a table of the first 15 numbers and columns for fizz and buzz.

fizzbuzz01.pyfizzbuzz01.tcssOutput fizzbuzz01.py
from rich.table import Table\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass FizzBuzz(Static):\ndef on_mount(self) -> None:\ntable = Table(\"Number\", \"Fizz?\", \"Buzz?\")\nfor n in range(1, 16):\nfizz = not n % 3\nbuzz = not n % 5\ntable.add_row(\nstr(n),\n\"fizz\" if fizz else \"\",\n\"buzz\" if buzz else \"\",\n)\nself.update(table)\nclass FizzBuzzApp(App):\nCSS_PATH = \"fizzbuzz01.tcss\"\ndef compose(self) -> ComposeResult:\nyield FizzBuzz()\nif __name__ == \"__main__\":\napp = FizzBuzzApp()\napp.run()\n
fizzbuzz01.tcss
Screen {\nalign: center middle;\n}\nFizzBuzz {\nwidth: auto;\nheight: auto;\nbackground: $primary;\ncolor: $text;\n}\n

FizzBuzzApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Number\u2503Fizz?\u2503Buzz?\u2503 \u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529 \u25021\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25022\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25023\u00a0\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u2502\u2502 \u25024\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25025\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502buzz\u00a0\u2502 \u25026\u00a0\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u2502\u2502 \u25027\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25028\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25029\u00a0\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u2502\u2502 \u250210\u00a0\u00a0\u00a0\u00a0\u2502\u2502buzz\u00a0\u2502 \u250211\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u250212\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u2502\u2502 \u250213\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u250214\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u250215\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u2502buzz\u00a0\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

"},{"location":"guide/widgets/#content-size","title":"Content size","text":"

Textual will auto-detect the dimensions of the content area from rich renderables if width or height is set to auto. You can override auto dimensions by implementing get_content_width() or get_content_height().

Let's modify the default width for the fizzbuzz example. By default, the table will be just wide enough to fix the columns. Let's force it to be 50 characters wide.

fizzbuzz02.pyfizzbuzz02.tcssOutput fizzbuzz02.py
from rich.table import Table\nfrom textual.app import App, ComposeResult\nfrom textual.geometry import Size\nfrom textual.widgets import Static\nclass FizzBuzz(Static):\ndef on_mount(self) -> None:\ntable = Table(\"Number\", \"Fizz?\", \"Buzz?\", expand=True)\nfor n in range(1, 16):\nfizz = not n % 3\nbuzz = not n % 5\ntable.add_row(\nstr(n),\n\"fizz\" if fizz else \"\",\n\"buzz\" if buzz else \"\",\n)\nself.update(table)\ndef get_content_width(self, container: Size, viewport: Size) -> int:\n\"\"\"Force content width size.\"\"\"\nreturn 50\nclass FizzBuzzApp(App):\nCSS_PATH = \"fizzbuzz02.tcss\"\ndef compose(self) -> ComposeResult:\nyield FizzBuzz()\nif __name__ == \"__main__\":\napp = FizzBuzzApp()\napp.run()\n
fizzbuzz02.tcss
Screen {\nalign: center middle;\n}\nFizzBuzz {\nwidth: auto;\nheight: auto;\nbackground: $primary;\ncolor: $text;\n}\n

FizzBuzzApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Number\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Fizz?\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503Buzz?\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503 \u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529 \u25021\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25022\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25023\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502 \u25024\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25025\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502buzz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502 \u25026\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502 \u25027\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25028\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u25029\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502 \u250210\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502buzz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502 \u250211\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u250212\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502 \u250213\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u250214\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u2502\u2502 \u250215\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502fizz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502buzz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

Note that we've added expand=True to tell the Table to expand beyond the optimal width, so that it fills the 50 characters returned by get_content_width.

"},{"location":"guide/widgets/#tooltips","title":"Tooltips","text":"

Widgets can have tooltips which is content displayed when the user hovers the mouse over the widget. You can use tooltips to add supplementary information or help messages.

Tip

It is best not to rely on tooltips for essential information. Some users prefer to use the keyboard exclusively and may never see tooltips.

To add a tooltip, assign to the widget's tooltip property. You can set text or any other Rich renderable.

The following example adds a tooltip to a button:

tooltip01.pyOutput (before hover)Output (after hover) tooltip01.py
from textual.app import App, ComposeResult\nfrom textual.widgets import Button\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\"\"\"\nclass TooltipApp(App):\nCSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Button(\"Click me\", variant=\"success\")\ndef on_mount(self) -> None:\nself.query_one(Button).tooltip = TEXT\nif __name__ == \"__main__\":\napp = TooltipApp()\napp.run()\n

TooltipApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Click\u00a0me\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

TooltipApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2581\u2581I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear.

"},{"location":"guide/widgets/#customizing-the-tooltip","title":"Customizing the tooltip","text":"

If you don't like the default look of the tooltips, you can customize them to your liking with CSS. Add a rule to your CSS that targets Tooltip. Here's an example:

tooltip02.pyOutput (before hover)Output (after hover) tooltip02.py
from textual.app import App, ComposeResult\nfrom textual.widgets import Button\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\"\"\"\nclass TooltipApp(App):\nCSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    Tooltip {\n        padding: 2 4;\n        background: $primary;\n        color: auto 90%;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Button(\"Click me\", variant=\"success\")\ndef on_mount(self) -> None:\nself.query_one(Button).tooltip = TEXT\nif __name__ == \"__main__\":\napp = TooltipApp()\napp.run()\n

TooltipApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Click\u00a0me\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

TooltipApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2581\u2581 I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0 brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear.

"},{"location":"guide/widgets/#line-api","title":"Line API","text":"

A downside of widgets that return Rich renderables is that Textual will redraw the entire widget when its state is updated or it changes size. If a widget is large enough to require scrolling, or updates frequently, then this redrawing can make your app feel less responsive. Textual offers an alternative API which reduces the amount of work required to refresh a widget, and makes it possible to update portions of a widget (as small as a single character) without a full redraw. This is known as the line API.

Note

The Line API requires a little more work that typical Rich renderables, but can produce powerful widgets such as the builtin DataTable which can handle thousands or even millions of rows.

"},{"location":"guide/widgets/#render-line-method","title":"Render Line method","text":"

To build a widget with the line API, implement a render_line method rather than a render method. The render_line method takes a single integer argument y which is an offset from the top of the widget, and should return a Strip object containing that line's content. Textual will call this method as required to get content for every row of characters in the widget.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXGtT28hcdTAwMTL9nl9BsV/2Vlx1MDAwNe109zxTtXVcdTAwMGJIIISER4BcdTAwMTBya4tcdTAwMTK2bFx1MDAxNORcdTAwMDe2eG7lv99cdTAwMWWHWPJDxjY2ce5d71x1MDAwNowk2+OZc7pPP0Z/v1haWk7vmtHyq6Xl6LZcdTAwMTQmcblcdTAwMTXeLL/0x6+jVjtu1PlcdTAwMTR2/m43rlqlzpXnadpsv/rjj1rYuojSZlx1MDAxMpai4DpuX4VJO70qx42g1Kj9XHUwMDExp1Gt/W//cyesRX82XHUwMDFitXLaXG6yXHUwMDBmWYnKcdpoff+sKIlqUT1t87v/h/9eWvq78zM3ulZUSsN6NYk6L+icylx1MDAwNqjR9Vx1MDAxZt1p1DuDtWhJkJa6e0Hcfs1cdTAwMWaXRmU+W+EhR9lcdTAwMTl/aFndXHUwMDFlnVx1MDAxY16fNnfM0d398cmhkqdlzD61XHUwMDEyJ8lBepd8n4mwdH7Vyo2pnbZcdTAwMWFcdTAwMTfRcVxcTs/5vOw73n1du8GTkL2q1biqntejtv/+0D3aaIalOL3zX0J0XHUwMDBmfp+DV0vZkVv+a0VcdTAwMDVOklx1MDAwMuuUMMZcdJd9sn89KFxurFJGgUGhrXR941pvJLxcdTAwMTI8rt9UhcolmY3sLCxdVHl49XL3mrRcdTAwMTXW282wxeuVXXfz8I3JUICCXHUwMDAwVffUeVx1MDAxNFfPU1x1MDAwZiPUgVLOKtLKOevQZMOIOstcdTAwMDFaXHUwMDFhRONM9u38hze3ylx1MDAxZGT8lZ+wevlhwupXSZKN159404+mPKJyK920Z9VduFx1MDAxMVxmiFx1MDAxOGpn764qm4er3e/UXHUwMDAzv7DVatwsd898e3iWjeiqWVx1MDAwZdOHL2GURGLgWYvd80lcXL/oXHUwMDFmbNIoXWQw7Fx1MDAxY/32clxu+IOAQvzzSljhQMmx8Z9cXLy7PD1vbev9N3uJhlx1MDAwYrmLd1x1MDAxYlx1MDAwNfgvtVx1MDAxYe32ynmYls6LOIAz4lx1MDAwMIhHSeBE4LRgiFx1MDAxYqFcdTAwMDXwXHUwMDFh9JDAuMA5XHUwMDA3vCokrdVAhSRcdTAwMTCdx/QkUFJcdTAwMDaakCRcdFx1MDAwNVx1MDAwMEOoQFpcdTAwMDRcdTAwMTaYplKTpy3qXHUwMDAxKpBTXGYkUnZmVFx1MDAxOFx1MDAwMVaDRln9LGA1aFx1MDAwYrHqnCG2XGZkx1x1MDAwNqvaxvVSZecmxNXjk1x1MDAxYqrCadIuMtZ9gPt5MIWA0JA2glx1MDAxNFx0hFx1MDAwMZhK6yRcdTAwMWFUxlhw84QpXHUwMDA1XHUwMDAytEKrXGaw0bWDOEVcdTAwMWJoLZgvWqAmxvQgTlx1MDAxZCn2KFx1MDAxYWZnslx1MDAxZsOpmVx1MDAxNU6jJImb7eGKQkNcdTAwMTFKnbcsykpcdTAwMWNcdTAwMWKk+1uV6+279t37xsV++nqrtd3ePL+cXHUwMDA2pPB8IPUwJJBsw7RcdTAwMDX2IL0gtS5QhtFAiEJYJfqFzkQg/a1cdTAwMTIqVDhcYlCgQFxuROU08i822nJcdTAwMTChgIHSWjJcbkEgXHUwMDExylx1MDAwMVEh2bxaySrwf1xuoCbnVvpcdTAwMDCK1pFGY8dcdTAwMDcolurV++Rz+aB+bj/o9kH49cRcdTAwMTS5/EVcdTAwMDGo4oVcdTAwMTdCXHUwMDFha0AjsLPsXHUwMDA1qFxyXHUwMDE4XHUwMDExvFx1MDAxNk4oyVx1MDAwMNZPXHUwMDA06JlcdTAwMTBqXlx1MDAwMCW2v0Yw1Z5ccqD2OVx1MDAwMKpcdTAwMGI1qXKs0YhyjHxcZqDHe1x1MDAxN/d7n87XXHUwMDEy8UnvbO2+PvhcdTAwMWNcdTAwMDItOEBRXHUwMDA20kipXHUwMDE0aFx1MDAwMM1cdTAwMTBQvVxiNYFcdTAwMTGCueqIp4qVwJNcdTAwMTBcbnjGmnZeXGLl8TlhrNC/XHUwMDFlQkdqUetcbr08XHUwMDAwSZBKuPFBWj7Y3CuX3lx1MDAxZlx1MDAxZX01N1dcdTAwMWZut1Vauf70XFwgXHUwMDE1U4FUXHUwMDA0QILYO3JULpSPm7BcdTAwMDekoFxcXHUwMDAw0jlEXHUwMDA2MVx1MDAxOYbIk1BqnFx1MDAxMlx1MDAxNcQhrt5cdTAwMDdEikNcdTAwMDNyUqNcdTAwMWOePtBcdTAwMWO/SZSKjFx1MDAxNPwzXHUwMDA35Vx1MDAxZoZUasPhnZ5cdTAwMDSmWVrgXHUwMDA3ZOjhyLdi9HZfMySp8PHy+uaotH178WXjrb7/fLe+d4234yVcdTAwMTVejnrfuSYrXGZcdTAwMWJkSbPiXFxcdTAwMWHdpsPohrYw9EMjrVx1MDAwNDOBSzhxXHUwMDFm31x1MDAxZidcdTAwMDdvjqp2rXK1trL1QZVcdTAwMGWnS9M9o1Mgli0sSViyoPWhXHUwMDE39NFcclx1MDAwMz5swElcdTAwMDFI4Pqj0tlcdTAwMDV/mMuOZFx1MDAxNFP9lGLyK+/DZmj650SeR0HOVoVtuppcdTAwMDDkXHUwMDE5mFx1MDAxYfX0IL7vXHUwMDAwVfRcdTAwMWPdXGJrcXLXg4dcdTAwMGX6X3Vmulx1MDAxYaVcdTAwMDFPfzlqnfKnRb/f/Sn+lV+yduRcdTAwMGb7V9uel68mcdVcdTAwMTNmOYkqvUxK41KYdE+njWZ2tsTDXHT57Vpb5f6v1WjF1bhcdTAwMWUmh49cZm0qVlNxqFxmXHUwMDFjXHUwMDBikdMwfvJRXHUwMDFklsT2znFsWnt2o11cdTAwMGJ33ea7rV+C1Up4SrNzXCKle4NlNq1cdTAwMDEosFx1MDAxNkmwi2NcdTAwMTU2P1ZcdTAwMGZLNlx1MDAwZbKa1Vx1MDAxY0p2o5l5mSepL0vr7urg8qDkbPJ2O6xcXGxdfjibXHUwMDE5qVnTimzZflxuqWFxSVxyU5JajojehHLkI5qxSZ2YKt6n+9H1p9219fhzJV7Zw5tfgNQqQMGKmONcdTAwMTFcdTAwMTamprekhopcdTAwMDJhQLP168R4pm9gs/TUQ2K2IZ6anOPh2GdcInWj9CVsnVTfXHUwMDFlxXs7d+5g/8Jd7u/OjtSan/1cXFLj4pJcdTAwMWFcdTAwMWYh9fdcdFx1MDAxZsJqsLk0Wb+vJiU5XG5cdTAwMTg/aThaqy0qq1lWM29cdTAwMWRcdTAwMWFAzf6YSd6rwDWfVtrn45RcdTAwMDUgnJ9cdTAwMDJcdTAwMDewgVWGPOJRO8rlKLPUXGZcdTAwMDRoXHUwMDA1x2eWQ3Difznp8KNkzr5cdTAwMWOJ9CS0XHUwMDFmXGZ68eHIiKB3XHUwMDE0X0lLmspcdLfTsJWuxfVyXFyv9lx1MDAwZeyhJWRrjGCvw/DSlVx1MDAxZuWKXGKUlZJNtjI8JG0zseVnJmzyNTqwqKzzVXOSzFxioVx1MDAwNr480+3xQX1U5eZXiCvq6HiXPtRcdTAwMGWqq9VwtWBQXHUwMDA2wPB/1pLjNVdcdTAwMDNjXHUwMDAyXG5cYqWUwvjAz6EzXHUwMDAzY0rCdrreqNXilOd+r1x1MDAxMdfT/jnuTOaqJ/95XHUwMDE0XHUwMDBlmFx1MDAxNv5O+XP9VqLp37HX7GfPljJcdTAwMWF1/ug+/+vl0KuLsf39bD+qs/d7kf89uYFcdTAwMTNS9Fx1MDAxZv5h4CRcdTAwMThfUp+guDxauC6uhbNcdTAwMDFLe9ZcdTAwMDO+PFwiOf7qXHUwMDE1LiBcdTAwMDOn0WhpNIl8d8LMXHJcdTAwMWPKwFdg0DphhVOZQ8/6IJitvCp8gXS+ijOsJUghaWEmalx0mrl94zHgJL08k9q30WFvzpSIXHUwMDAwhFx1MDAwNuGs9Wl9XHUwMDBlrVx1MDAxNeSu+m5LePk77S+KULMxXHUwMDE5/OpjWbeTnd23qzWx6u7bZ7T1+vBetdebw4eEmlx1MDAxOExaW7Ik+HPNwJBAXHUwMDA0JFx1MDAxNS8w+1XprFx1MDAwNPdrm7dcImT7x8ogqGdm3vKxwEDzXGbrXHUwMDE29M1EY9u30Vx1MDAxYX5x7Vx1MDAxYrJEQ1x1MDAwNGRWSqFtb1x1MDAwZVx1MDAxNTXbN+NcdTAwMWJalCAgkvOLzNin+5KEVk5LadlMXHLpoFx1MDAwMVx1MDAxNVx1MDAwMFx1MDAwM1x1MDAwNSQrXHUwMDEydJZoiIWz1lxuMmaSTq+ZW7ipI64xLdzoXHUwMDFjQI85YelGzCpjkWWEM4NcdTAwMDbO16TYhnhcdTAwMDPoW+zUYMVmLFx1MDAwYrd1e5rcrVSPcUW3PsLnzXfNK7k3fEhsctF6YUasZyzpIUNcIoau0lxuoNP++ovbt0Jg+8fKIKYnNHCFWSco7lxyRI7IWDVOXHUwMDEyoI7U51x1MDAxM5m3wu7Actg+j2bZyc3YXHUwMDE2qHjWmY/ev/dFp8h+lGkhnWKZl/dcdTAwMDYzzzrl2oeyrFM21q7xXHUwMDEyXGZcdTAwMDRHM+yu6mJptkXQl6Ped751J2JdlKVTp8tmge452s1mZaWGXHUwMDFm2ayDtFx1MDAxNTd//087qvopfbnUfVx1MDAxMlx1MDAwNMFfXHUwMDA1SS3d8y7zTmo9MsKRpmNkP4fLtSn221x1MDAwZtZGPvKewH7EryVtv0nCS6rJXHUwMDBioq9cdTAwMWI3t7VcInm0IP1cdTAwMWMrXCKQaIT1qlx1MDAxM1xmWtvfXHUwMDE3XHUwMDA3Tlx1MDAwN1xuXHUwMDA0XHUwMDBiVO8/zdOa4Ofdz+FNvlx1MDAwNtLuacroiVxyXHUwMDFk8yxfkW9cdTAwMDM3z9PsxIFjYfLXi+lOz8H4RZ1K+0vdnF9cXH84PLtf+1irt+tuM1l4dqBwznI4L1x1MDAxOXhGqL6aXHUwMDBlSfa+TivB/6N4Ws/onLnB4ouv6Nm18Vx1MDAxM7gxzypcdTAwMTD5lqBcXNpnTk1JJFx1MDAwYiud2vq9RGqCRurRSZNcdTAwMDWVnDogjUL4rVNWXHSw/d1cdTAwMGJcIjDo87iW8WjNXHUwMDFjXHUwMDBinWNKTuLoz29cdTAwMTN6XHUwMDE2xTlX62+F31fzjzKciTIs7k8q7jpcdTAwMTSKpFx1MDAxNdaNnzNcdTAwMWKdNFhQhqtcdTAwMDBcdTAwMTUq0dnAxXymfrdnXHUwMDAyXHJW++SF1ajnV1x1MDAxMVx1MDAxOJPhljQoXprniSnn6sNcdTAwMThcXEr/XHUwMDEz+82X4a44Ka6N76PNV8dcdTAwMWYjOJhcdTAwMWS92fq0cXK11npT3r9d/bJn381U146i9zBh+yi9/T5O5dCXu5Umlvn9/Fx1MDAwZVgtXHUwMDEyKVx1MDAwZfeU0UZcdTAwMTdcdTAwMGLbMbb/j1x1MDAxMLaghm0hXHUwMDFk7FRija2cmSysW9Rd/lJcdTAwMTgzVWVwqk6l90yypdW9raXjTl/QXCJ0KPVcdTAwMGZpJINcdTAwMGIrW7r4XHUwMDBlXHUwMDA2KKRcdTAwMDSfa1x1MDAxZb+LePSSz3lX+FRcdTAwMTT2XHJcdTAwMTOGY0Gyxlx1MDAxN4Ogd1x1MDAxYlx1MDAwZVx1MDAwN4zOXHUwMDE3iDhe1Fx1MDAxMmGEh35cdTAwMWGBpVx1MDAwYoS0vqeRQ1x1MDAwMVx1MDAxNlx1MDAwNEPobGWgrZagJZtcdTAwMTOgXFz+6MF3I1x1MDAwN21mlvtcdTAwMDVGsk/lXHUwMDBiXHUwMDAys69ajXZcdTAwMDZLPUVwYVx00Vx1MDAxOFx1MDAxMKT81FHuqodcIjhcdTAwMDbGMZitXHUwMDAzniRp7WCPz1h1q9H7zHpcdTAwMDYloNM6gdJcdTAwMDdcdTAwMWHo01x1MDAxZYOjooC1oUFeclx1MDAwNpiU4tcuXVx1MDAxNULYP/rBm73Zi/zv6dJrVHxjXHUwMDBi35BmnZ5AhVT3bk72t1blUUlhfFgtl0qfP5ZnqkImtGDqUVx1MDAwYlx1MDAxNiAozfAxvu8kn9TsaFx1MDAxMCNcdTAwMDLpXHUwMDA0XHUwMDBiXHUwMDE0R8hRRv+wZpZbU96OknWK/OpcdTAwMDNcZuudfjS5ZslqY1xyPq2ncpFza7xcdTAwMDIzvOFLkTBXuW6HPkpcdTAwMTAvgGOvP75T39i5rq2vJPK02Tyrne5v7X51Z5s/OfKWj0rzwJBjny4sXHUwMDE4Ry5/XHUwMDAzle+0kFx1MDAwMet2sEjOXGIj5rjfb9x6rtTkd8o9z81cYizHXHUwMDAyk2w7/Sc6ntY7gSws/liw3v+Pn1x1MDAwM/uyefYubFx1MDAxZdy//Xik11x1MDAwZeMvd9vVm2e7XHUwMDFiw1TOybeFS/Y9yMFcdTAwMDSgXHUwMDEykEv6d7BcInRgWFx1MDAxM1xinlx1MDAwN806ySx0XVx1MDAxNFhnWpA0/43uI6sz/j4z8/YgUlx1MDAxNXpcdTAwMTDQYNCx3lx1MDAxON+FvK9cdTAwMWbbo1J8vH5v65XD083W12azseguhFx1MDAwMoaLUuyzfVecXHUwMDEzvVx1MDAxYlZcYjj08Dd11PyDr/rpXHUwMDBlRFx1MDAxOVL+plGz24Y20oFImKhE+I9cdTAwMDOZ2oFcdTAwMTTfY1x1MDAxNaThZfftx2MzcVVUrrd0RUNyrfZKXHUwMDFm4q9n9rqojPIsLuRRXHUwMDFh+lx1MDAxNFxmXGJGtkO/P6xv31x1MDAxOElcdTAwMGWjjXaC40q0+Z7kXHUwMDA19Fx1MDAxZlx1MDAxY4uSr3z9XFz34UvN83dcdTAwMWaFXHJhKCx6XHUwMDFkYMbXPXftjav7hjsyNqkmZXO6oe7fvVl098FoIHKotZ9vwVx1MDAxZaJcdTAwMWa4Olx1MDAxMP5mR75Q5XdcdTAwMGL99Po+XG7lt1SoXHUwMDE5llx1MDAwN0Z6XHUwMDEwjrqeelOQ/ztcdTAwMGby4iG5sFx1MDAxYzabXHUwMDA3Kc9o10LwWsXlh2nJPmf5Oo5u1obdWa/z8O/aobbnUNQxN99efPsvXHUwMDFht7uQIn0= widget.render_line(y=0)widget.render_line(y=1)widget.render_line(y=2)Strip([segment, segment, ...])Strip([segment, segment, ...])Strip([segment, segment, ...])Line API WidgetStrip([segment, segment, ...])Strip([segment, segment, ...])Strip([segment, segment, ...])

Let's look at an example before we go in to the details. The following Textual app implements a widget with the line API that renders a checkerboard pattern. This might form the basis of a chess / checkers game. Here's the code:

checker01.pyOutput checker01.py
from rich.segment import Segment\nfrom rich.style import Style\nfrom textual.app import App, ComposeResult\nfrom textual.strip import Strip\nfrom textual.widget import Widget\nclass CheckerBoard(Widget):\n\"\"\"Render an 8x8 checkerboard.\"\"\"\ndef render_line(self, y: int) -> Strip:\n\"\"\"Render a line of the widget. y is relative to the top of the widget.\"\"\"\nrow_index = y // 4  # A checkerboard square consists of 4 rows\nif row_index >= 8:  # Generate blank lines when we reach the end\nreturn Strip.blank(self.size.width)\nis_odd = row_index % 2  # Used to alternate the starting square on each row\nwhite = Style.parse(\"on white\")  # Get a style object for a white background\nblack = Style.parse(\"on black\")  # Get a style object for a black background\n# Generate a list of segments with alternating black and white space characters\nsegments = [\nSegment(\" \" * 8, black if (column + is_odd) % 2 else white)\nfor column in range(8)\n]\nstrip = Strip(segments, 8 * 8)\nreturn strip\nclass BoardApp(App):\n\"\"\"A simple app to show our widget.\"\"\"\ndef compose(self) -> ComposeResult:\nyield CheckerBoard()\nif __name__ == \"__main__\":\napp = BoardApp()\napp.run()\n

BoardApp

The render_line method above calculates a Strip for every row of characters in the widget. Each strip contains alternating black and white space characters which form the squares in the checkerboard.

You may have noticed that the checkerboard widget makes use of some objects we haven't covered before. Let's explore those.

"},{"location":"guide/widgets/#segment-and-style","title":"Segment and Style","text":"

A Segment is a class borrowed from the Rich project. It is small object (actually a named tuple) which bundles a string to be displayed and a Style which tells Textual how the text should look (color, bold, italic etc).

Let's look at a simple segment which would produce the text \"Hello, World!\" in bold.

greeting = Segment(\"Hello, World!\", Style(bold=True))\n

This would create the following object:

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN1aW1PbRlx1MDAxOH3nVzDuSztcdTAwMTM2e790ptOhJFx1MDAxNEIuUEJoaTuMsNa2gixcdTAwMTlJ5pJO/nu/XHUwMDE1RpIl7CBjUqd+XHUwMDAwe3el/Xb3nPNdpH/W1tc72c3Idn5cXO/Y665cdTAwMTdcdTAwMDZ+4l11nrn2S5ukQVx1MDAxY0FcdTAwMTfNf6fxOOnmI1x1MDAwN1k2Sn98/nzoJec2XHUwMDFihV7XossgXHUwMDFke2Gajf0gRt14+DzI7DD92f1961xy7U+jeOhnXHQqJ9mwfpDFye1cXDa0Q1x1MDAxYmUp3P1P+L2+/k/+t2JdYruZXHUwMDE39UObX5B3lVx1MDAwNirJ661v4yg3lkhFhGRcdTAwMWHLYkSQvoD5MutDd1x1MDAwZmy2ZY9r6vAjuvlhK+7ffPQvtlx1MDAwZcJNSo7CP8ppe0FcdTAwMThcdTAwMWVmN+HtVnjdwTipXHUwMDE4lWZJfG6PXHUwMDAzP1x1MDAxYrjZa+3Fdb6XXHUwMDBlwICiO4nH/UFkU7dcdTAwMDO4aI1HXjfIbtyNcNl6u1xy1XHX7pBcZkZSca41Zsatueh11zNKXHUwMDExx0ZLyZRcdTAwMTCMypphW3FcYmdcdTAwMDGGfYfzT2nZmdc974N5kV+MyVx1MDAxMi9KR15cdTAwMDInVo67miyZXHUwMDBiiqQglCqMXHKunszAXHUwMDA2/UHmjosgho1SynDMsJCiNMbmp0JcZnN2XHUwMDEyo4tcdTAwMWVnwmjXz1x1MDAxMfJ3ddtcIn+ybXeQKUHDJi2fy8W48S/rYKtcdTAwMDKugoN3e0dXxmNDtn/iXHUwMDFk7uyeblx1MDAxZqTvVLHgKXR6SVx1MDAxMl91ip7Pk2+loeOR791cIlx1MDAwZVx1MDAwMCk4XHUwMDExWlFDTdFcdTAwMWZcdTAwMDbROXRG4zAs2+LueVx00rXKStqxg1a2scZcdTAwMGVcdTAwMDBcdTAwMGLRilx1MDAxOf5gclxcnrw7vd5S9rV93dvjsYjt/lx0WYxcdTAwMWN0XHUwMDE2OdJcdTAwMTgk4n5ukFx1MDAwNblhkFwiXFxzXHUwMDAw1TQtXHUwMDE4QVhSMZNcclpSa7qLs4FiiaQhXHUwMDFjJuHGfVSTXHUwMDBlgiFtpm2744HEmlxuzivn80Q8mIdUXHUwMDAxu8SWhdTMXmf3gnQmRlx1MDAwNTVcZnSLPlxcwI9Aen57b7btx4R9XHUwMDE0Nt7ZXHUwMDBlg8GSXHUwMDA1fOlcdTAwMThlRFwirakmtFx1MDAwNlH+dFpNKtJbwJHyOlxmXHKmXHUwMDA0XHUwMDBiptqgcFxuXHUwMDFmrWT3evx7cPkq+nBp9/ax2Lu+Or5cdTAwMWPsLUl2OWfaXHUwMDE40lx1MDAwMswlauIoO1xmPtmc1FOt294wXGLzoyqac5SDgX91dmxcdTAwMTjGz9aP4yT0/+pUjyq1MHvtdu66zTDoO0Z0QtubpkpcdTAwMTZA8FR0Z/Go7O2CXHUwMDFkXHUwMDFl3C7Z9evriZOgXHUwMDFmRF74fpZNi3tcdTAwMTbGVL31jrWGXHUwMDEwIK3hXHUwMDBmZ+3u3ubbbSrVxYuD991cdTAwMWXTn3x/J5jB2lx1MDAxYfv+K79cIihGVIBCmqZjwVxiT7XWucsk73XVIzxcdTAwMGJhSNx5ldaeRTpBxZqV6/m/XHUwMDA1WMYwgkvFelwit0VmM4AoyjGRpEVwdfM2ftU/O9u/XHUwMDEx4uJ0g1x1MDAxZI9e71x1MDAxZmyuuuNcdTAwMTJMI6yVXHUwMDEyhlPnKNg0XHUwMDExuECSc8HInCjr0X5Mmyb4m35MSaG5Ulx1MDAxNU16Sj/2q/fJo3tcdTAwMDO6uSuOduTxsWTnLy+Wlj5cdTAwMTjNOG2B7sf5sVx1MDAxYz3fn8Wh/9P7ZGx/WFx1MDAwNT/WsGmxuLNidZ3AnEHKXHUwMDAwaWhcdTAwMTmafonAL0/UwcvRfnK+8bt9dbQpe1x1MDAxYofh6apcdTAwMTOYUYYoJNzwMVxuXHUwMDEy93K57nouOVx1MDAwMvpSXHUwMDE3PHHYjbp3XVwii+k9Low2XFyXkka6QK6N61rNaFRcdTAwMTBcdTAwMTBF9lQsJlNsdFx1MDAwNvZcdTAwMTNrsyDqo2kyVChcXIH616DwtEGL8Vx1MDAxN8/kL/gjXHR7rMpcdTAwMDFfou9V/8Ppm1x1MDAxNyPpXHUwMDFmvtl68UZEoThcdTAwMWOfrDp9hYJA0HBiZO6AWZ2+XG5RIDV0XHUwMDAwezWresVl87dCyTn8hWReSY1L2H+zTphcdTAwMTCBK1x1MDAxOfRXo2+aY2mV+Htr0VxcXHUwMDAy327vfUlkpVxmWvfAWlx1MDAwYlx1MDAwMVx1MDAxOU5cdTAwMGJcdTAwMGY8X69XlMJMXHUwMDBiJFx1MDAwNGR04Fx1MDAwZaRcdTAwMDF/PFx1MDAxZENzSZCmmFNtnthcdTAwMDVcdTAwMWKkQElcXKFEaqOwvqc8pDRSICXmLlx1MDAxY6hcdTAwMThzV7yXcFx1MDAwM6ZcdTAwMWVXu6eTlsVqlos71jTzkuyXIPJcdTAwMDHW04ZNnlHtPiDQy7ncXHUwMDFkOys3MMJcXFx1MDAxMVc9g3CSUWp4+WjG7Y03cqtFkuWlv8aqbeR/2Zr5XHUwMDA1z4o1YFxmwVx1MDAwNsPeMEyoNIRy1TCGMMjn8pJGw5rQS7OteDhcZjLY7v04iLL6tub7t+m4PrBeQ0BgNdW+uiiM3Fx1MDAxZKc1vfy2XpIm/1F8//vZvaNnYtl9Nlx1MDAxYTAub7dW/d9azqiaWclcdTAwMDYh4EJcYqVcdTAwMWUuZ/P914rKmWRcdTAwMWNcdTAwMTmqMVMu6IDMoaFmsFx1MDAwNdJhzEUkrGbX8tRcZuJcIsyNUkZcdTAwMGLIb4SsJP6lnCmEXHUwMDFkXHUwMDE1INOrXHUwMDE5M1EzJjXRXHUwMDFjyzZFg2XL2eLZ/lx1MDAwM+VsfuBblzNcdTAwMDaUYiBoXHUwMDEwVzqK8cq4W1x1MDAwNVx1MDAxMYgzdr+CPEjP5tfBpvWMXG7KNVx1MDAwMVx1MDAwNitOsKaGNawhXHUwMDAykVx1MDAxOer6LenZxmw45911JLdcdTAwMTS0WVx0lmGi3lokWFxmpuK0zZO5M9zt9a8hXHUwMDFhXHUwMDFkXHUwMDFmXnw8+qN38mv8epFcdTAwMWH/XCJitth7XHUwMDE1Tq5cdTAwMDTPdUxgXHUwMDAzwVlZpHA3oJQh8KJGXHUwMDEy6TA/O7tcdTAwMDLn7/H5YvZdr9drqph6UFWEQOKHtWStdGrxvOqJS/dSV6pQXyuvWqWMasFcXIpSQurNRfRcdTAwMDFbXHUwMDBiQCUtkqn5p7ySj+S4XHUwMDExiCqmlVCUS1x1MDAxMMKSkTldhUBaQSxuXHUwMDA0eHZiKq/GLI2wXHUwMDEwP3NcZiqtJOdcdTAwMWFCoEq4Vz6ZI65mg4VDPDZcdTAwMTJX9GzygFx1MDAwZbSVske+XHUwMDAw9aioQ2JcdTAwMTeutnlFqW3UMd9cdTAwMWJMuXnKMVx1MDAwM99GKTbCbUwz5lBIuUhAaYWpXHUwMDE0RuNcdTAwMDVjj/lv/9ViXHUwMDBmLMDlau5cdTAwMWVwSaFpwyhcdTAwMDKBMNPgrImGMFx1MDAxMoY0n5h+SyHIbGS7T1x1MDAwM9OzXCKQtclcdTAwMDRcdTAwMWRvNDrMXHUwMDAwdMVxXHUwMDAwylx1MDAwM39cIuDlKjuXgb365X76OVx1MDAwNq5N9tNcdJLNmfB57fO/MTl+mCJ9 \"Hello, World\"Style(bold=True)greeting.textgreeting.stylegreeting

Both Rich and Textual work with segments to generate content. A Textual app is the result of combining hundreds, or perhaps thousands, of segments,

"},{"location":"guide/widgets/#strips","title":"Strips","text":"

A Strip is a container for a number of segments covering a single line (or row) in the Widget. A Strip will contain at least one segment, but often many more.

A Strip is constructed from a list of Segment objects. Here's now you might construct a strip that displays the text \"Hello, World!\", but with the second word in bold:

segments = [\nSegment(\"Hello, \"),\nSegment(\"World\", Style(bold=True)),\nSegment(\"!\")\n]\nstrip = Strip(segments)\n

The first and third Segment omit a style, which results in the widget's default style being used. The second segment has a style object which applies bold to the text \"World\". If this were part of a widget it would produce the text: Hello, World!

The Strip constructor has an optional second parameter, which should be the cell length of the strip. The strip above has a length of 13, so we could have constructed it like this:

strip = Strip(segments, 13)\n

Note that the cell length parameter is not the total number of characters in the string. It is the number of terminal \"cells\". Some characters (such as Asian language characters and certain emoji) take up the space of two Western alphabet characters. If you don't know in advance the number of cells your segments will occupy, it is best to omit the length parameter so that Textual calculates it automatically.

"},{"location":"guide/widgets/#component-classes","title":"Component classes","text":"

When applying styles to widgets we can use CSS to select the child widgets. Widgets rendered with the line API don't have children per-se, but we can still use CSS to apply styles to parts of our widget by defining component classes. Component classes are associated with a widget by defining a COMPONENT_CLASSES class variable which should be a set of strings containing CSS class names.

In the checkerboard example above we hard-coded the color of the squares to \"white\" and \"black\". But what if we want to create a checkerboard with different colors? We can do this by defining two component classes, one for the \"white\" squares and one for the \"dark\" squares. This will allow us to change the colors with CSS.

The following example replaces our hard-coded colors with component classes.

checker02.pyOutput checker02.py
from rich.segment import Segment\nfrom textual.app import App, ComposeResult\nfrom textual.strip import Strip\nfrom textual.widget import Widget\nclass CheckerBoard(Widget):\n\"\"\"Render an 8x8 checkerboard.\"\"\"\nCOMPONENT_CLASSES = {\n\"checkerboard--white-square\",\n\"checkerboard--black-square\",\n}\nDEFAULT_CSS = \"\"\"\n    CheckerBoard .checkerboard--white-square {\n        background: #A5BAC9;\n    }\n    CheckerBoard .checkerboard--black-square {\n        background: #004578;\n    }\n    \"\"\"\ndef render_line(self, y: int) -> Strip:\n\"\"\"Render a line of the widget. y is relative to the top of the widget.\"\"\"\nrow_index = y // 4  # four lines per row\nif row_index >= 8:\nreturn Strip.blank(self.size.width)\nis_odd = row_index % 2\nwhite = self.get_component_rich_style(\"checkerboard--white-square\")\nblack = self.get_component_rich_style(\"checkerboard--black-square\")\nsegments = [\nSegment(\" \" * 8, black if (column + is_odd) % 2 else white)\nfor column in range(8)\n]\nstrip = Strip(segments, 8 * 8)\nreturn strip\nclass BoardApp(App):\n\"\"\"A simple app to show our widget.\"\"\"\ndef compose(self) -> ComposeResult:\nyield CheckerBoard()\nif __name__ == \"__main__\":\napp = BoardApp()\napp.run()\n

BoardApp

The COMPONENT_CLASSES class variable above adds two class names: checkerboard--white-square and checkerboard--black-square. These are set in the DEFAULT_CSS but can modified in the app's CSS class variable or external CSS.

Tip

Component classes typically begin with the name of the widget followed by two hyphens. This is a convention to avoid potential name clashes.

The render_line method calls get_component_rich_style to get Style objects from the CSS, which we apply to the segments to create a more colorful looking checkerboard.

"},{"location":"guide/widgets/#scrolling","title":"Scrolling","text":"

A Line API widget can be made to scroll by extending the ScrollView class (rather than Widget). The ScrollView class will do most of the work, but we will need to manage the following details:

  1. The ScrollView class requires a virtual size, which is the size of the scrollable content and should be set via the virtual_size property. If this is larger than the widget then Textual will add scrollbars.
  2. We need to update the render_line method to generate strips for the visible area of the widget, taking into account the current position of the scrollbars.

Let's add scrolling to our checkerboard example. A standard 8 x 8 board isn't sufficient to demonstrate scrolling so we will make the size of the board configurable and set it to 100 x 100, for a total of 10,000 squares.

checker03.pyOutput checker03.py
from __future__ import annotations\nfrom textual.app import App, ComposeResult\nfrom textual.geometry import Size\nfrom textual.strip import Strip\nfrom textual.scroll_view import ScrollView\nfrom rich.segment import Segment\nclass CheckerBoard(ScrollView):\nCOMPONENT_CLASSES = {\n\"checkerboard--white-square\",\n\"checkerboard--black-square\",\n}\nDEFAULT_CSS = \"\"\"\n    CheckerBoard .checkerboard--white-square {\n        background: #A5BAC9;\n    }\n    CheckerBoard .checkerboard--black-square {\n        background: #004578;\n    }\n    \"\"\"\ndef __init__(self, board_size: int) -> None:\nsuper().__init__()\nself.board_size = board_size\n# Each square is 4 rows and 8 columns\nself.virtual_size = Size(board_size * 8, board_size * 4)\ndef render_line(self, y: int) -> Strip:\n\"\"\"Render a line of the widget. y is relative to the top of the widget.\"\"\"\nscroll_x, scroll_y = self.scroll_offset  # The current scroll position\ny += scroll_y  # The line at the top of the widget is now `scroll_y`, not zero!\nrow_index = y // 4  # four lines per row\nwhite = self.get_component_rich_style(\"checkerboard--white-square\")\nblack = self.get_component_rich_style(\"checkerboard--black-square\")\nif row_index >= self.board_size:\nreturn Strip.blank(self.size.width)\nis_odd = row_index % 2\nsegments = [\nSegment(\" \" * 8, black if (column + is_odd) % 2 else white)\nfor column in range(self.board_size)\n]\nstrip = Strip(segments, self.board_size * 8)\n# Crop the strip so that is covers the visible area\nstrip = strip.crop(scroll_x, scroll_x + self.size.width)\nreturn strip\nclass BoardApp(App):\ndef compose(self) -> ComposeResult:\nyield CheckerBoard(100)\nif __name__ == \"__main__\":\napp = BoardApp()\napp.run()\n

BoardApp \u2585\u2585 \u258b

The virtual size is set in the constructor to match the total size of the board, which will enable scrollbars (unless you have your terminal zoomed out very far). You can update the virtual_size attribute dynamically as required, but our checkerboard isn't going to change size so we only need to set it once.

The render_line method gets the scroll offset which is an Offset containing the current position of the scrollbars. We add scroll_offset.y to the y argument because y is relative to the top of the widget, and we need a Y coordinate relative to the scrollable content.

We also need to compensate for the position of the horizontal scrollbar. This is done in the call to strip.crop which crops the strip to the visible area between scroll_x and scroll_x + self.size.width.

Tip

Strip objects are immutable, so methods will return a new Strip rather than modifying the original.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nOVdaXNcIkmS/d6/QlbzcZuccI97zMbWdKFcdTAwMTPd6GB3TMYpkFKAOCSktv7v645UXCI5XHUwMDEyoYvKYtVtqiqSI4h87v6eh4fHX38sLf3oPDbLP/619KPcK+bDWqmVf/jxJz9+X261a406XcL+v9uNbqvYf2a102m2//XPf97mWzflTjPMXHUwMDE3y8F9rd3Nh+1Ot1RrXHUwMDA0xcbtP2ud8m37v/n3Xv62/O9m47bUaVx1MDAwNYNcdTAwMGZJlUu1TqP1/FnlsHxbrnfa9O7/Q/9eWvqr/zsyula52MnXr8Jy/1x1MDAwNf1Lg1x1MDAwMVx1MDAxYWVHXHUwMDFm3WvU+4NcdTAwMDVcdTAwMTRotdBavj6j1l6jz+uUS3S5QmMuXHUwMDBmrvBDP453K1AvX/dcdTAwMGWLK6nWib/e1M3LjcHHVmpheNx5XGafp1wiX6x2W5FBtTutxk35rFbqVPnTR1x1MDAxZX99XbtBszB4VavRvarWy+320GtcdTAwMWHNfLHWeaTHpHh98HlcdTAwMTL+tTR4pEf/SjlcZjRcdTAwMDJ4kFx1MDAwZZXE14v8auVcXKC0tVJIROE1yJFhrTZCulx1MDAxMzSsf4j+z2BghXzx5opGVy9ccp6D6FxuZTN4zsPLlzUmXHUwMDEwylqtUGlnhFevz6iWa1fVXHUwMDBlPcVi4KQ2wlx1MDAxOIHCOTdcdTAwMThnu9y/XHUwMDFmXGJSKbruXHUwMDA3N5Q/vrlV6mPjP9FcdTAwMTmrl15mrN5ccsPBiPnCelx1MDAwNE+D13SbpfzzbVx1MDAwN2O1Rk9DoLl5vVx1MDAxZdbqN6NvXHUwMDE3Noo3XHUwMDAzpPRcdTAwMWb9+89cdTAwMGZAlO5KXHUwMDFjRK1cdTAwMDaNXHUwMDAy3OxcYl3LVm6ue8t7d1x1MDAxOXl2eFxcObnPZdeWXHUwMDEzjlBtXHUwMDAy0MpcdTAwMWGp0ErrnDfjXHUwMDE4ldJrXHRcdTAwMDRTacEkXHUwMDE2ozR85b0wesEgaoWLgyhISSZcdORDZsZo+WRzm1x1MDAwNl8olNTRQWbL7excdTAwMWPcqIRjNFx1MDAwNTJcdTAwMTDgnfZgwVx1MDAxOedgXHUwMDE4pNrIQFx1MDAxOSfoXHUwMDEyoFOj40pcdTAwMGVEQUvp6D9YOIjqOIhaa5SQxsLMXGI93y/s7VxcaFdoXHUwMDFjXHUwMDFj3ubP7zeJ37R/LUJBvOlGVWC1oWl3XHUwMDA2vFFgh1x1MDAwMGqkXHUwMDBmiFx1MDAwM3hPXHUwMDFlSmilMbFcYkWwVtFQXHUwMDE3XHUwMDBloVx1MDAxMEtFUSjiNqD97IH+NpXLy3RxOXucq96U16pcdTAwMDfrXHUwMDFiISTdiTpcYoiFKidccjqg71xmw1x1MDAxMFVcIiBcdTAwMGaqtFx1MDAwMkVE1erEQlx1MDAxNCyRXHUwMDE0TTFg0bio9bFcXFx1MDAxNFx1MDAxY01cdTAwMTdFNz27XHUwMDFi9TZvdNi4vu5cdTAwMTazcFx1MDAxOXZLjX27l3CMXHUwMDAy6sBcdTAwMWKhKcxcdTAwMTNcdTAwMDR8RFx1MDAxMD2HeU1cXNRcYutQ9mNKciHafy25lPm5UT9cdTAwMWa5JHxcdTAwMWNEtfDOXHUwMDAy+plcdTAwMDHaetquLtv1PVxym5uynFp7Slx1MDAxZlRPk1x1MDAxZefpzlKgJ1x1MDAwNmdR05/aXHJLesJtgI6UvDGoPGKSqahw4KxcdTAwMTBcdTAwMGJcdTAwMDdRXHUwMDE5n3RykmKbfIdcdTAwMTNdllx1MDAwNd+53Xpyjerm8Xr2XGbyp/WkK/pcdTAwMTRgIDmvRLLeoSBHaUYwqomMXG5E68lFXHUwMDE5n9ysXHUwMDEzgDWk6oWaX6SfXHUwMDBmRq0wcVx1MDAxOPXgPOiogHhcdTAwMGKjK141fKdzs1Jo3+CtKUH7ot5MOEbBXHUwMDEzXHUwMDA2lVx1MDAwNVx1MDAwMUpcdTAwMDLFe1x1MDAxY470ylx1MDAxOYKGoD+EQYr3oJKLUUnwcdZEXHUwMDAy42JglD4pXHUwMDBlo8zMlNJmdozuhL3qWlXmMlx1MDAxZFFcdTAwMGXXzcNj2OydJFx1MDAxY6NSyoCYptBcdTAwMTTKhTM4mrwniFxuY+mOWCFRJDgxSjZGMUBoOb/M6JzcqME4iFwiKE30Rjo9M0b1zXpYKFXu08f3WpRy5+FlfT1MOEbRqEA68j9cdTAwMWWU0TKShntcdTAwMTZMXHUwMDEwOMKuQk5MYYKdKPlcdTAwMTJA4qNcdTAwMGKHUFx1MDAxZJu7d2hQ0q2ZPeu0eqMuOnBdusruXHUwMDE1W6dcdTAwMGYtma3m80lcdTAwMTdM7EWV8kQzXHRcdTAwMDFcdTAwMDCRtYyfWSdA4ywvjoIzXHSGqJVcdTAwMTL418JBVMk4iKKj2dL+XHUwMDFkq0v6+mJze9dm95dVuYHb+6enzZxNOkTBm4Cw4STxXHUwMDFhXHUwMDAxUo5hXHUwMDE0XHUwMDAzLSjWkGJcImbuklx1MDAwYlGyMbpjOMdF+jlRUVx1MDAxZFx1MDAwYlFtke5cdTAwMWH42SU9bi7bo3C7lrtfbV5f7eNtRVxc51x1MDAxMlx1MDAxZeaVkIEha1x1MDAxNN4rXHUwMDBmVo2GeVwiXHUwMDAxSMhcdTAwMTDSeUy0oDfaWWW1XTQn6lxcbF6UvIvxRkaXSN+C6F7n8r6mbnqnzZW73HLnMXN8VF3/XHUwMDFkIGqEJS6qnZMjqXvOOVx1MDAxMVd1gqhcdTAwMDCr/eSKJS6FsVx1MDAxNtTCIVTGMlFcdTAwMDM0a1rI2X3oxe7DRqHaq4Rdd7ZV92erjfr+RdLDPNHMwFiHJJPAOI3KjkDUXHUwMDA2XHUwMDAyXHUwMDFk31x1MDAxOGe4jiixXHUwMDEwReEleVHQi5ZysjLWiyryoUJqNbtcdTAwMTNcci+ecvumqFx1MDAwZm/NQ72zv3N73To/TLhcdTAwMTNNOZJD9D3BSclcdJtcdTAwMTGEelx1MDAxMVgwSoNcdTAwMDaLTiVXLCktkWJeRFksXHUwMDA2QJ1cdTAwMTdxXHUwMDAwXHUwMDA1Vlx1MDAxMHTv9OxcYr16KKZPa21wl9nH7VpVmePc+i9Ois5Q6GRJXGZJ+qaEQZr3KD6eMVxugdJkrFZcdTAwMDOp/YiLSlx1MDAxYUbpXCLxXHUwMDE05Vx1MDAxNm3908ZjlL+wgWhZxZvVou2KKp6c7Oid5mrT5DLd6nphM+lOXHUwMDE0REA6wztcdTAwMDOoud4uUpPAb+BcYj2kokjqk2Am8CS3jMRcdTAwMTlnhV84JlxusVFcdTAwMWWFXHUwMDE2Xoj3rCx5XHUwMDExVjvdwoYt75ZqXCJzXuymdlx1MDAxYlx0Ryj5UGk8ek1cXFR7gcNy3itcdTAwMTF4hSRDXHUwMDE0XG6Z4Fx1MDAxMlx1MDAxMpLxxEXkwiXtnYivXHUwMDE1JXySdphcdTAwMWSehUZcdTAwMTWuWts7t08rXHUwMDA3t1f3ze398/VMwuGZsi6w1pIs1I6IJrnRXHUwMDExfMpAopTOoqbJwORWiqIgLi1cdTAwMTDEwvFQXHUwMDFiW2+PLJSMtbPT0OPMRerk5nxv67540d3aL4dcdTAwMGZcdTAwMGb791x0hyigXHJcdTAwMTR7SFCS5LBcdTAwMWGN8Fx1MDAwNGAgnU86RHlcdTAwMGLJLbe3aLVfQFx1MDAxN+ohNtukpLXo4Vx1MDAxZEp+fcs9XFyd7tQz9+XD7PLKSemyW096OpRcdTAwMDHKy1x1MDAxMp5cdTAwMWOQ5FxyIUNcdTAwMDAl7qlcdTAwMDMhNTlZrZXxNsGLSob/035cdTAwMDG1fHwps1x1MDAwNE9cIvZcdTAwMWRL8/dcdTAwMWJcdTAwMGad86Otq8xarry6fX2xXazmz1x1MDAxM1x1MDAwZdFcdTAwMTS5UMF1MsJpY6xcdTAwMWYpZSaM2sAp+lx1MDAxMUT0SEol14tcdTAwMDJdsDSChcuHmtgw7zQ4XHUwMDE33Y79pk7avYHbTmpzt3l3XHUwMDEwbuyk6j19eZD0ZFx1MDAxM3jL2+eFQCSkRpcwnnNNXHUwMDE4KND9XHJ3hGKdXFylXHUwMDA0QCYk1OIlRH188VxiOO2dXHUwMDE3XHUwMDA2Zlx1MDAwZvS5Wlx1MDAxNcqNVLpWz2RrOrO50fBcdTAwMGa/WMvPUuBkXHUwMDAyQqDXllx1MDAxM0p+dFWJMCqJXHIxepBcdTAwMTdtXHUwMDEyi1FjUJOkc4vmRJ2MdaIgOIXt5DtcbpzOT1x1MDAwZotccn9YujqrXHUwMDFkZU+0bj21Vs5cdTAwMTJcdTAwMWXo0fDKJil2pYiWXHUwMDEy3Vx1MDAxY07ZO7qsSMqT4JdcdTAwMWGT7EXRWuJcIkYvmlpyJjbhpFhcdTAwMWPCe7YsZc+3m1x1MDAxYrWtjaNMa2tXn3SOz07bSW+TI6VcdTAwMGVcdTAwMWNcYqKhxEhp1ofVkldcdTAwMTA4a/t1I+RpXHUwMDEznFx1MDAxMVx1MDAwNcnKwZk59iCZV1x1MDAwNV5spT2QNDBWKJw9zvuV8/rpffWyU8BcdTAwMTW3WXkqrJVcdTAwMWbuXHUwMDEyXHUwMDBlUfAuIDFvhffaaFx1MDAwMGlGMKpcdTAwMDKKn2iIqYJEmVxcQe/J0Kywi0dFMX7fp1x1MDAxMZr3jNvZNydcdTAwMWbs7dhuSp7ByeN1/cGmsVPYLCdcdTAwMWOiSlx1MDAxMDxIXHTT/UUhhVx1MDAxZNlcckJyMTCaVD1xVMNro8mFqDZcbr3Ri1bH7OPT9oCO63tRzVx1MDAwZdH1Qlhv3e1cdTAwMTfSXHUwMDBltleObjZuXHUwMDFl1sKjxEOUXHUwMDAzPVFRYclcdTAwMGaBXHUwMDE4dqL9tKi3gvyr8kbK5Fx1MDAxNo9cdTAwMDCS/Vxit4iCPt6LUtBcdTAwMTDc9WB2Lrpmd+5ztfW1p/AqbNbq4DauNm5cdTAwMTNcdTAwMGVR8p+BIMmOdPspkFx1MDAwYmdHMcqro16C1nR7olQvcSBVvDuVSfOCgVSa+I54zit05Edmz91X7/Nb6V1cdTAwMTf2Llxua2m7nXdHO+W4XHUwMDFhp1x1MDAxMbBcckN0tFx1MDAxZfP1VaV8u1p+XHUwMDA3Ru0sbUVZXHRpZTn1NJq6573JXHUwMDA2eEcl31x1MDAxOTDuUyVOnVa+3m7mW4SBcZxqrVx1MDAwMmOs05ZmXurIkskrTlx0oUG/o5TWRJ9hUs8xxWu5qL5cdTAwMGWnL1x1MDAxN1x1MDAwNsCK3O+Gvdjcv8Dl5ulx+7RSa/m6yVxmmndccqEw32o1XHUwMDFlfrxe+fvPae+bOd+9XHUwMDE2+TOzVz0uioNmoVx1MDAxM+7ordne9+VvybAucDZ+M4vgLXJcdTAwMTbf0c5v+bKS71xcXHUwMDFk7Fx1MDAxZjdSVVkopo+PN1RcXD5iqnmNrj99azM/bblcdTAwMTZNXHUwMDExTyalN8yjjXRcdTAwMDFcdTAwMDV+0CwriG/Hl2HTs/LqjVx1MDAwMFCpVMatSipHI1BI4ZY+30VcdTAwMTbTX62KXCJT4HkvXHUwMDAzyU2DUX3+umHVaMlcdTAwMWJWv45FT7Wq89LpKt5V7vaq+96Yxoo5yJz8juhcdTAwMTdcdTAwMTCf6rA0o85F6/LfQr8tbu7Ui4VO53Hn3pnlp9Ku6vaSvqShdSB5r1x1MDAwNVx1MDAxMoSUVsRyhlxyQEEgyd+DI1x1MDAwNjy0pTtpXGbIglRCLdy6MFxijMcoXHRJ40HNzn+2lrP3XHUwMDBm4U1W5S5219IrWbVij4pJhyh4XHUwMDE1oNFWcl9cdTAwMGIyWTWGUFx1MDAwMVx1MDAwNlx1MDAwNXlp3tGdZI7OXHUwMDA1XHUwMDE4Xi9cdTAwMWNH91x1MDAxMYowuqbRryrRZvZUhzvdyJz27uzmyurxzr06XHUwMDBm5ermL27DNsvCMFx1MDAwNqSYvSdcdTAwMDLurbejPYFcdTAwMTVcdTAwMDaSN1x1MDAxYzpP0TvJzVm80t5TXHUwMDE0WDg3qlxcrFx1MDAxYjVWSVx1MDAxMlc4e6TvPS7fhPlwfTt31MvlTn0h3HWtxPNcXCG04jVGr7mkeyTOXHUwMDEz0SWKS1x1MDAxNJIgqpz93I7YXHUwMDE4omtcdTAwMDOtaFx1MDAxMJKbwGk1QT7CM9tGYl7cKFx1MDAwNqL7dV52XHUwMDFiXG6Dyir4ukW3aURcdTAwMTex97jTbsPxg6rD9uPBtj+utWcjulPl429EoMthWGu2J9tcdTAwMTTxxjibQsKIXHUwMDA2Zdzs4nE1tfm0dbyXlb1cdTAwMDPZvs4101x1MDAwZvthXFxcdTAwMDJxqlHNz+1cdTAwMWKiXHUwMDFlfI5cdTAwMDa3XHUwMDFmXHUwMDEz0c6Fz05fXHUwMDEzMUHJus1+cot5Ja9R47hNsXS0xtHryb+p6IlcdTAwMWSvNuUoKqDtb9Hs96yXoyZcdTAwMDVcdTAwMWVcdTAwMTVyt/D5iMdP2FSSsK+mtPXkXHLsxsvZXHUwMDE3yVx1MDAxZkXtonq/bVx1MDAwMVx1MDAxZXZ6XHUwMDE1n013u+eYbOhbctVAmDFeSEUqeVQ2qn5iXVvlUVxu87l64kq+QN7ke9BvKfJcdTAwMTn9lfnIZMBTx+f1nOZ2rO9pUrPSOzg9Or9q1zOt+onRqdst7dLJxqc3XHUwMDAxeUyJwnpBXHUwMDE4XHUwMDFjQadcZlx1MDAxY0hcdTAwMDOWa+GI9Hxcbp2AXHUwMDA15ybw8a9AJyrlXHUwMDAwo0dcdTAwMDH8Nuh8K/NcdTAwMTa79qiNJOeJcnbJeLeVrm0yc7BcdTAwMTdP9e4y3jzI9C9u8zVcdTAwMDMhN4RBL/uiUWvrRurd+SwuuiOSdJxcIsKb3I3rXHUwMDAwaHjBZ+G6KHlcdTAwMTWfeFPsRH20XHUwMDEx9FtcdTAwMThN27ua9duVxure6nlnuXZb3ln7xZ27Z0trXHUwMDEwj9GOlCNnf4drNblHXHLvXHUwMDE1XCKhJrj1QoJrNWl00jrxO0b56aduxm9sXHUwMDAzT1x1MDAxOFx1MDAxNVx1MDAxNFneUU58mOnKXZu9W1x1MDAxM+srOzv32Vx1MDAxNYzd2fblYV58XGKh2lx1MDAwNki+UlxuirROR49cdTAwMTjtl8FpRVxi9U5JQb/d51LDxXJJlfLjXHUwMDAwlcR1pVx1MDAxNUSB0ViylkhkXHUwMDFiJDYwMF4765ltSHKYalx1MDAxNKLIXUeV/Uo3+nJholxuM2m9d5KRT1ukXdcvU3p143LvcDZcdTAwMTX257T33czVTHhSf+q08+Ji9XBv/ab31PgqdUdwILo+XHUwMDFm51x1MDAxZp/aIOZsXHUwMDE0yvdcdTAwMWPEuFJO5eHo2nWaZ49yZb1X2c2sJZ6gXHUwMDEwOVx0LFx1MDAxYc3HL1x07q0w3v1JXGLD59xcdTAwMTlF/jW5PfRcXL9kxSyc85+6Ic9xu1x1MDAwZfOOKuiLsL0s072101x1MDAwYpHN71x1MDAxYtvJp5dlsp0/qdxcdTAwMDClXHUwMDA04tDcjlSPIJS9v6BcdTAwMTBoiK55Jz/ZcFxcXHUwMDE1TbkyIVx0XHUwMDAx3lx1MDAwN9JbXHUwMDAwj9yHSkyo3yDvT+BFQzpUOOWjNVx1MDAwNT8hXG5cXH6iXHUwMDE27oCmaD/xXHUwMDExiEq2SVx1MDAxZF1cZn1cdTAwMTOhd729w4vl/HZnZ2VHnl2J1XVI/uI16lx1MDAwMKxcInhwP3mMXHUwMDFlY/HcidRcdTAwMDSSXHUwMDBiMFx1MDAxNOk8XHUwMDFmLeBJmlx1MDAxN1x1MDAwNU6kWLt4zSHIaOMwalx1MDAxNUU4mozZI31ntbZXrbWrdrmi0o3Udc2ellx1MDAwYknHKFx1MDAxYVx1MDAxOWiB5JpcZoAzY/2cTUBcdTAwMTSagihywjfBzXKl5lx1MDAxM7VcZs4tXHUwMDEzXHUwMDExab/4rdkygFg2yrkh3nw/O0Srayd+67GweplXXHUwMDA3T83zfPmweFx1MDAxOXcqeEJcdTAwMDK9dC6g6GmRmDc5Slx1MDAxY85DXHUwMDE4TVx1MDAxMVihUJI7zmvxPTJcdTAwMGZkoIhE8mJcdTAwMDFZgvGRj1x1MDAxOVx1MDAwNHqv+Cwzy1x1MDAwZV8hUZLxxTbB8mHo1d8p875Vjs3TXHUwMDAyplfBWURcdTAwMDPvqDG6gHSx2Mvu11x1MDAwZdPiuuuf0ie4XHUwMDFld1x1MDAxY2lC1jPYXHUwMDA0LHeC9rxcdTAwMWRFXHUwMDFhXHUwMDFjWXAjscY9KZSxRDC9/tySRizXhcCQtCBkU5RcdTAwMTAmmltcdTAwMWGsaSA3vFx1MDAxNMJb0d8zM8Z1uaU198iYz3Lz91pcdTAwMDBxh1xiw/2kXHUwMDA1dMq9zkTw2/hcXFx1MDAwNDvDfmO5mbF/WMrc7V9cdTAwMWTpjebVQ+8207kut+1jsrfAXHUwMDEw5lx1MDAwM97agqiJXHUwMDE1Kj16yKly/eI7XHUwMDE0ktgpfvKs6Im1S6gn8JHoyVx1MDAxNi+bWYWWnjuKzVx1MDAwNdrftbnlNH1oiqliPsy1ZK+3msvmunj8y4LGXHUwMDAwmY1657j21K8uckOPpvO3tfBxXGJdfVuiXHUwMDAx3tdanW4+vGzTXHUwMDBig5c7XHUwMDE3uf3tMlxyoP+Oeuily2Htik3vR1iuXGbbZKdGXHUwMDEz83q504is4Vx1MDAxNGkoeXq71lZp9Cs1WrWrWj1cdTAwMWaeTFx1MDAxOdZU3/A80Vx1MDAxM5yDjO9cdTAwMWLG68rkqmdcdTAwMTfY01x1MDAwMfVcdTAwMGLKXHUwMDFh3/RcZlrYgMNcdJLxK1x1MDAxN21o1H+51IHU5Fx1MDAxYVR/Z4P83Fx1MDAwMtXkosZAcDrfSevpfy0mXHUwMDE0NVwi14EpxedcdTAwMWIrjUpGT1x1MDAxNXshhY6rXHUwMDAwXFy0XHUwMDE3cZKES2TW8q3OSq1eqtWv6OLAdfwoP3/01lxmXHUwMDExpm+zxS6PMiVcdTAwMDLPRf2aj6ZWpP5cdTAwMDbHXFzwXHUwMDE05JvPtFuS6lx1MDAwM+2lN8R7lHt5xqtcdTAwMGb7Ua6X3lx1MDAxZdT0nZ/RQUFAZkM3RIO1Rlx1MDAxYTM+Jkk0qK+6pNbWXHUwMDFib8aGXHUwMDE05tud1cbtba1DU3/QqNU7o1Pcn8tltupqOT/mL+grRa+Nmn+T33E4Plxm/rY0sJH+P17//p8/Jz47XHUwMDE1i+H+1XH4XHUwMDBl3vCP6J/vdl3g4lx1MDAwZsxcdTAwMTQ0rzp6XHUwMDE4+Fu+a3rQSqTvXHUwMDAyXGacMoq4hTPK4OjiOlx1MDAwMZ9cdTAwMTNcIkKTojTuk1x1MDAxNXRcdTAwMTNJTUBKQTjuXHUwMDEyQoJcdTAwMTYnkXn0JvBoXHUwMDE0N1j2Q2vJr9VcdTAwMWbgSVx1MDAxMkj8f+a6ROCclyTDvFx1MDAwM7qFkSbdr16CXHUwMDBi6lxy3TuCOadcdTAwMDOMn+64hr/F7+Q/YnHEP2NcYnqn94hcdTAwMTNFXHUwMDE4aVx1MDAxYzbqPKRA8lTvKM5pZFx1MDAxZs9ubaPcLGbO2/uq53Xq9EOFXHUwMDBm89NEzlx1MDAwNzTd5LWVdlx1MDAwZYRcdTAwMTjO2fJOzUDxjk3pnJRcbj/XXmWy+1AzaVwiXHUwMDEwXHUwMDEyrHZfeLTJK4K+dmP+VE10eUfM/na1dLV2SVx1MDAxYXPz5GB/q1hdXHUwMDA0TfR8N5MmiZ5H9TFagT5eXHUwMDExkSP22pnZN8xOx1NcdTAwMTJphVTE5/hcdTAwMDRuzV1cdTAwMDEs4qhnQKJcdTAwMWRcdTAwMTSUXHUwMDE09y6Unz1cdTAwMTJpomdcdTAwMDBhXHUwMDAyR1x1MDAxZu1IgFx1MDAxMmsk/TXuKIhCoyT2Yzw3K3U+kjP5mUyxzlxuObfVxm/lXHUwMDE101x1MDAwM8wrZfBcdTAwMDF6a9FYllx1MDAxZdxcdTAwMDd7smhcdTAwMTKcXHUwMDA146JcdTAwMWPBZbdK/oypXy2Jnlx1MDAwN4UuXHUwMDAwzjtznLGSXHUwMDBmMpqsmoRcdTAwMTe8QEKciIZlzLhO+51ITTyE+Sc1jt530pp474XxR7p5x4fcvKPubHrUSqL3sjJwJHZI+TG43UhCXHUwMDA3+KhccuuE1EZ5bsvyXHLbVEFDXHUwMDAwXHUwMDE0XCJcYsaOTFxm3KR1voC7oDrSqJJbbMrIdtSfzotgQaFmfodf/1r3XHUwMDE1cU5AhFx1MDAxNEi1XCJcdTAwMGIk7uw35lC48Vx1MDAxZlqFkmjhc9Ob6f7r91VG8Vh6vjxcdTAwMDajd3qROHFkpm3MRXDeR1Olb/ZHrW489MJcXObx8WlVy+WM0vXK2q/0XCL+LS+S0nyomXL9XHUwMDAzd4zGkcMktFS8nmRRXHUwMDBiukNexi+WXHUwMDE2PeYx/43iqH/ajv9KdTStfppl+mBcZt+tM0hJVIJ2sdVcYsPLRqXSLidi6WXCqD5cdTAwMTapdXzrNG1cdTAwMDRZdZS8vmVj03dFJzFSp6znXHUwMDEzqsnXW66vJa8/ZGOqf4C1JetcdTAwMTJGO4VTemd+2MhcdTAwMDBVYJXnXHUwMDE0XHUwMDFjSC8hsmFrXHUwMDEwqo1cZri3XHUwMDA3elSWrHIsg8l1Q4pcdTAwMGL95tLyXHUwMDA0rDPqK0L1aFB7O4hPb1oywuWl53RcdTAwMWVqcpXCXHJK/pZcdTAwMDbJTU/S0mrF7W608vL3ZvuxQOpfXHUwMDFkhdBcdTAwMTdF6eh+37H6fetcdTAwMWRzg9mj9NHZzX796v60dt29yOS1PCnBdpjsKI3OkGrkQ1x1MDAxNLRA6e1o3SmRJ6GF8WSeXHUwMDE433huXHUwMDA29zG1rSmYXHRcdTAwMWK2JoRqkoOMdfi6XHUwMDFh/VdcdTAwMWP9ilx1MDAxNjFzTDg+Lv176SXiPiaBXHUwMDAyXGaN52PB30yR6aRrjFx1MDAxNPJcdTAwMWR1XHUwMDE3U+91XCJNXHUwMDE3hFxulCNpY6Xpn1x1MDAxYTmyOYyrZT2FXGLe4m7UlMqLT1uv8oHzvHvGcz9cdTAwMTIhJ3BcdTAwMDBSYVx1MDAwZcBcdHR82ICa0Fx1MDAwMseK/smyZi6rmGA1XHUwMDA1ky/gXHUwMDAwcZF+eihYiq5iUpwnXHTCfcOF9laPXHUwMDA3elBcdTAwMDFdI3fBrWelNm682OFr2Vx1MDAwN9FGQYzCsjDiqr5J3IN71HE9oHRa0vB/b+5cdTAwMTFcdTAwMGJg/kmNYfeLyIeRscVcdTAwMTc0XHUwMDBloKDrZ+9cdTAwMGaDXHUwMDA3qdZaeLjse2F2I1u73c6Bc8l2YOSZXHUwMDAydl7kw4zhxZAh/+UlXHUwMDA2SE5cXDllLZeifJ//slx1MDAxM1x1MDAxY9Yk9tE/1O1cdTAwMGKXQ6aRj0/sXHUwMDBlf5t8XHUwMDEwnfaRpv7fTT56g2DfS1x1MDAwMvlcdTAwMThcdTAwMWHPx8hcdTAwMDeXp8VcdTAwMTkvzbAw7zr+aPrNTqTxXHUwMDAy9yVBpygmeTJgXHUwMDFjyT2QTlx1MDAwYlx1MDAxMNGT/Vx1MDAxYe99fO1DibSHqHzCeJ1cdTAwMGZQsCZUxnKX7HFTNuTdjdJeKvLfWsFY41x1MDAwN7DAgfUri8a/xfhm5Fx1MDAxZdNDQSSGXHUwMDBiJTiuXHRyr9xBYFx1MDAwMlx1MDAxM+CVTor0XHUwMDFj4ZWy3HOByPHH2Mf0ziZcdTAwMDOyM2FcdTAwMTTc5Vx1MDAwMFx1MDAwNaBzgLxs/nuTjTjA8k9qXGarX8Q1tI7lXHUwMDFhUvSLe9/RwnzdbKz2uqc5dyGOb25qVX1cdTAwMWFmk801NK9cdTAwMDKR7XnBp11ZO9xcdTAwMWKaZjvgynVNXCKK7suUXHUwMDBl+5+lXHUwMDFhOKk63Y4lRHmbL1x1MDAxOdxcdTAwMTdcdTAwMWVcdTAwMGU8jWp8b8NcdTAwMThUVnxIaX2aaiz91//Wn5dcdTAwMWGmV1mZ4beZJ/uYNMRcdTAwMGZcdTAwMTJcdTAwMTLw8VvUKOKAtO85pWA6Jlx1MDAxMmni3lx1MDAwN55z4ZJcdTAwMWKB2JGaK1wiXCKBRd7VzkdcdTAwMTR4XHUwMDFiT0g+rSbIv5NIXHUwMDA3329RJGDSKqTl4lx1MDAxNW208dKSjejx7Shk/PRiO5+OfsinWOiP2OiMjGR6wFiKZlx1MDAxZZDPylx1MDAwND6uwFC8x/GdXHUwMDFmKuBSW3IqgMhV31x1MDAxZt2MMnVcdTAwMGL+0JCI8lx1MDAxMNEl5Vx1MDAwZp6X7u3YkFxcoPlwXGbNjVx1MDAwZpif/N51V6l4XHUwMDA09y+PgfeLKFxuQPwxcyRcInjh/Fx1MDAxZOdg1dxGXHUwMDBmM+kw18hgunS0JY5cbvvbiXZg4HSgNVx1MDAxZj/FvJDsYPR4XHUwMDAwXHUwMDEzOEt6SkpyXHUwMDBicko70lx1MDAxOc7BmurB7IQ2uZFcXPvPTlx1MDAxZlxc1+Hn2C/p06slkfa/QyxiXHUwMDEwKX6yiJVGvlVabjYn0oXI28yDLryO5dmo/nix21x1MDAxZvlm87hDs/Tq42j+a6WXrzp4x1x1MDAxZve18sPK5OV8XtH/48VQ2VwiynxcdTAwMWL++vuPv/9cdTAwMGZcYuGAVyJ9 virtual_size.heightvirtual_size.widthself.scroll_offsety = scroll_yx = scroll_xx = scroll_x +self.size.widthBoardApp"},{"location":"guide/widgets/#region-updates","title":"Region updates","text":"

The Line API makes it possible to refresh parts of a widget, as small as a single character. Refreshing smaller regions makes updates more efficient, and keeps your widget feeling responsive.

To demonstrate this we will update the checkerboard to highlight the square under the mouse pointer. Here's the code:

checker04.pyOutput checker04.py
from __future__ import annotations\nfrom textual import events\nfrom textual.app import App, ComposeResult\nfrom textual.geometry import Offset, Region, Size\nfrom textual.reactive import var\nfrom textual.strip import Strip\nfrom textual.scroll_view import ScrollView\nfrom rich.segment import Segment\nfrom rich.style import Style\nclass CheckerBoard(ScrollView):\nCOMPONENT_CLASSES = {\n\"checkerboard--white-square\",\n\"checkerboard--black-square\",\n\"checkerboard--cursor-square\",\n}\nDEFAULT_CSS = \"\"\"\n    CheckerBoard > .checkerboard--white-square {\n        background: #A5BAC9;\n    }\n    CheckerBoard > .checkerboard--black-square {\n        background: #004578;\n    }\n    CheckerBoard > .checkerboard--cursor-square {\n        background: darkred;\n    }\n    \"\"\"\ncursor_square = var(Offset(0, 0))\ndef __init__(self, board_size: int) -> None:\nsuper().__init__()\nself.board_size = board_size\n# Each square is 4 rows and 8 columns\nself.virtual_size = Size(board_size * 8, board_size * 4)\ndef on_mouse_move(self, event: events.MouseMove) -> None:\n\"\"\"Called when the user moves the mouse over the widget.\"\"\"\nmouse_position = event.offset + self.scroll_offset\nself.cursor_square = Offset(mouse_position.x // 8, mouse_position.y // 4)\ndef watch_cursor_square(\nself, previous_square: Offset, cursor_square: Offset\n) -> None:\n\"\"\"Called when the cursor square changes.\"\"\"\ndef get_square_region(square_offset: Offset) -> Region:\n\"\"\"Get region relative to widget from square coordinate.\"\"\"\nx, y = square_offset\nregion = Region(x * 8, y * 4, 8, 4)\n# Move the region in to the widgets frame of reference\nregion = region.translate(-self.scroll_offset)\nreturn region\n# Refresh the previous cursor square\nself.refresh(get_square_region(previous_square))\n# Refresh the new cursor square\nself.refresh(get_square_region(cursor_square))\ndef render_line(self, y: int) -> Strip:\n\"\"\"Render a line of the widget. y is relative to the top of the widget.\"\"\"\nscroll_x, scroll_y = self.scroll_offset  # The current scroll position\ny += scroll_y  # The line at the top of the widget is now `scroll_y`, not zero!\nrow_index = y // 4  # four lines per row\nwhite = self.get_component_rich_style(\"checkerboard--white-square\")\nblack = self.get_component_rich_style(\"checkerboard--black-square\")\ncursor = self.get_component_rich_style(\"checkerboard--cursor-square\")\nif row_index >= self.board_size:\nreturn Strip.blank(self.size.width)\nis_odd = row_index % 2\ndef get_square_style(column: int, row: int) -> Style:\n\"\"\"Get the cursor style at the given position on the checkerboard.\"\"\"\nif self.cursor_square == Offset(column, row):\nsquare_style = cursor\nelse:\nsquare_style = black if (column + is_odd) % 2 else white\nreturn square_style\nsegments = [\nSegment(\" \" * 8, get_square_style(column, row_index))\nfor column in range(self.board_size)\n]\nstrip = Strip(segments, self.board_size * 8)\n# Crop the strip so that is covers the visible area\nstrip = strip.crop(scroll_x, scroll_x + self.size.width)\nreturn strip\nclass BoardApp(App):\ndef compose(self) -> ComposeResult:\nyield CheckerBoard(100)\nif __name__ == \"__main__\":\napp = BoardApp()\napp.run()\n

BoardApp \u2585\u2585 \u258b

We've added a style to the checkerboard which is the color of the highlighted square, with a default of \"darkred\". We will need this when we come to render the highlighted square.

We've also added a reactive variable called cursor_square which will hold the coordinate of the square underneath the mouse. Note that we have used var which gives us reactive superpowers but won't automatically refresh the whole widget, because we want to update only the squares under the cursor.

The on_mouse_move handler takes the mouse coordinates from the MouseMove object and calculates the coordinate of the square underneath the mouse. There's a little math here, so let's break it down.

  • The event contains the coordinates of the mouse relative to the top left of the widget, but we need the coordinate relative to the top left of board which depends on the position of the scrollbars. We can perform this conversion by adding self.scroll_offset to event.offset.
  • Once we have the board coordinate underneath the mouse we divide the x coordinate by 8 and divide the y coordinate by 4 to give us the coordinate of a square.

If the cursor square coordinate calculated in on_mouse_move changes, Textual will call watch_cursor_square with the previous coordinate and new coordinate of the square. This method works out the regions of the widget to update and essentially does the reverse of the steps we took to go from mouse coordinates to square coordinates. The get_square_region function calculates a Region object for each square and uses them as a positional argument in a call to refresh. Passing Region objects to refresh tells Textual to update only the cells underneath those regions, and not the entire widget.

Note

Textual is smart about performing updates. If you refresh multiple regions, Textual will combine them into as few non-overlapping regions as possible.

The final step is to update the render_line method to use the cursor style when rendering the square underneath the mouse.

You should find that if you move the mouse over the widget now, it will highlight the square underneath the mouse pointer in red.

"},{"location":"guide/widgets/#line-api-examples","title":"Line API examples","text":"

The following builtin widgets use the Line API. If you are building advanced widgets, it may be worth looking through the code for inspiration!

  • DataTable
  • RichLog
  • Tree
"},{"location":"guide/widgets/#compound-widgets","title":"Compound widgets","text":"

Widgets may be combined to create new widgets with additional features. Such widgets are known as compound widgets. The stopwatch in the tutorial is an example of a compound widget.

A compound widget can be used like any other widget. The only thing that differs is that when you build a compound widget, you write a compose() method which yields child widgets, rather than implement a render or render_line method.

The following is an example of a compound widget.

compound01.pyOutput compound01.py
from textual.app import App, ComposeResult\nfrom textual.widget import Widget\nfrom textual.widgets import Input, Label\nclass InputWithLabel(Widget):\n\"\"\"An input with a label.\"\"\"\nDEFAULT_CSS = \"\"\"\n    InputWithLabel {\n        layout: horizontal;\n        height: auto;\n    }\n    InputWithLabel Label {\n        padding: 1;\n        width: 12;\n        text-align: right;\n    }\n    InputWithLabel Input {\n        width: 1fr;\n    }\n    \"\"\"\ndef __init__(self, input_label: str) -> None:\nself.input_label = input_label\nsuper().__init__()\ndef compose(self) -> ComposeResult:  # (1)!\nyield Label(self.input_label)\nyield Input()\nclass CompoundApp(App):\nCSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    InputWithLabel {\n        width: 80%;\n        margin: 1;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield InputWithLabel(\"First Name\")\nyield InputWithLabel(\"Last Name\")\nyield InputWithLabel(\"Email\")\nif __name__ == \"__main__\":\napp = CompoundApp()\napp.run()\n
  1. The compose method makes this widget a compound widget.

CompoundApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e First\u00a0Name\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u00a0Last\u00a0Name\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u00a0\u00a0\u00a0\u00a0\u00a0Email\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

The InputWithLabel class bundles an Input with a Label to create a new widget that displays a right-aligned label next to an input control. You can re-use this InputWithLabel class anywhere in a Textual app, including in other widgets.

"},{"location":"guide/widgets/#coordinating-widgets","title":"Coordinating widgets","text":"

Widgets rarely exist in isolation, and often need to communicate or exchange data with other parts of your app. This is not difficult to do, but there is a risk that widgets can become dependant on each other, making it impossible to reuse a widget without copying a lot of dependant code.

In this section we will show how to design and build a fully-working app, while keeping widgets reusable.

"},{"location":"guide/widgets/#designing-the-app","title":"Designing the app","text":"

We are going to build a byte editor which allows you to enter a number in both decimal and binary. You could use this a teaching aid for binary numbers.

Here's a sketch of what the app should ultimately look like:

Tip

There are plenty of resources on the web, such as this excellent video from Khan Academy if you want to brush up on binary numbers.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nOVdaU/jyFx1MDAxNv3ev1x1MDAwMjFf3pMmnlpvVY309MTSQCCsYe2nXHUwMDExMomTmCTOYidcdTAwMDRG/d/frUBcdTAwMTNncchcdTAwMDKM0400PVx1MDAxMNtJueqcU+de36r8/WVtbT16bHrrf66te72CW/OLbfdh/Xf7etdrh34jwEOs/3fY6LRcdTAwMGL9MytR1Fxm//zjj7rbrnpRs+ZcdTAwMTY8p+uHXHUwMDFkt1x1MDAxNkadot9wXG6N+lx1MDAxZn7k1cP/2n+P3Lr3n2ajXozazuBDMl7Rj1x1MDAxYe3nz/JqXt1cdTAwMGKiXHUwMDEw3/1/+Pfa2t/9f2Ota3uFyFxyyjWvf0H/UKyBko++etRcYvqNpVx1MDAwNDQ1lHP6eoZcdTAwMWZu4+dFXlx1MDAxMVx1MDAwZpewzd7giH1pXHUwMDFkzppcdTAwMTdnwVOnSW865dxWZn9jc/dm8LElv1bLR4+1frNcbu1GXHUwMDE4ZipuVKhcZs5cYqN2o+pd+cWoYlsw8vrrtWFcdTAwMDN7YnBVu9EpV1x1MDAwMi9cZoeuaTTdglx1MDAxZj3amySvLz53xJ9rg1d6+Jek2lFEU05cdTAwMTRcdTAwMDHDxetBe7XQ4GggWjNcdTAwMTCEMdAjrdpq1HAwsFW/kf7PoF13bqFaxsZcdTAwMDXFwTmiXHUwMDAwXklcdTAwMGXOeXi5VymoQzhjnFHNKSHy9YyK55crkb01RVx1MDAxZInNUJJcdTAwMTEmqVx1MDAxMmzQXHUwMDEyrz8mXHUwMDE0NKGcXHUwMDExMlx1MDAxOFT7+c1ssY+Pv+I9XHUwMDE2XHUwMDE0X3os6NRqgybbXHUwMDAzX2OYXHUwMDFhXFzTaVx1MDAxNt3o5WOUUoZQrYlcdTAwMTn0Rs1cdTAwMGaqo29Xa1x1MDAxNKpcdTAwMDO09F/9/vtcdTAwMDIwpYQn41RcdTAwMTNcclx1MDAwMsDMjtM9dpJxm72oVT047Fx1MDAxYz1cdTAwMTGze8C/Lo5T9k44xWF/XHUwMDEzqMrBW+VcZlxiUcA4XGYjVVx1MDAxYYdcdTAwMDHiR1HBXHJ2i1pcdTAwMDaqUdtccsKm20YkTIKrcLBcdTAwMTlcXHBcIimlXHUwMDEz0MqBOJpcdTAwMTIjXHUwMDA0cI5cdTAwMDRjMIpWMJJIzpn8NLCaT1x1MDAwMWucmKNY5VpQI1x1MDAwNZiZsZrb2bvZolx1MDAwZlutWq8rRHGDkqfrIFx1MDAwMasjePsnUUqIXHUwMDAxhVomULFGQKpcdTAwMWSthcbxXHUwMDAw0Fxu4CNBylx1MDAxZEJBMi1cdTAwMTVVRulxlDLtXHUwMDAwXHUwMDEwjlx1MDAxYU9cdTAwMThwS6xRlCrNSJ9Sq4dSr1bzm+FEjIJcdTAwMTKJXHUwMDE41YIowbWSM2PU69ZPN77dXHUwMDFmXFzlstmb6kaudbW3XV5cdTAwMDSj7zXjz4BRXHUwMDFjeUMkXHUwMDEwoVx1MDAxMa7EsGGQXHUwMDAygpRLg8rGjIXOUnN+yZVMsnF8Uu5YRyFccuCEzpBcdTAwMTJiXHUwMDFjoJQ5XHUwMDEyQFx1MDAxOJRQwlAshVx1MDAxYVx1MDAwNShcdTAwMTeUMWpibfxcdTAwMTlcdTAwMDCqmE5cdTAwMDIo+jBcdTAwMWMxodTM+DSsV7nNRlx1MDAwN7nuUfd8v51vcdVopFx1MDAxY59SXCJcdTAwMDJcdTAwMDFcdTAwMDEqXHRcdTAwMDe0pSP4VFx1MDAwZVCBRGVUSFx1MDAwNVIsXHTQO3ScXHUwMDFmXHUwMDA1UIZyYlx1MDAxOFx1MDAwMbWCXHUwMDEz/TSE6uRpXlKMXCJcdTAwMDSfXHUwMDE5oHv5ulxmy2euOLmqXHUwMDFmh8F5buN6p5NygGqOYVx1MDAxMZM4W0itXHUwMDE4lyNcdTAwMDBcdTAwMDVcdTAwMDdRY7hmiFWtR83HfPik7E5r+Dh8XHUwMDEynFx1MDAwNriWn1x1MDAxNzV9jlx1MDAxMdUyXHUwMDExoFx1MDAxOERcbowk+excdTAwMTBccjJnV0FwuXta7mZ36MZGePh0t5duiCqG8ZA0SmBgLVx1MDAxMYxmXGKiUjJcdTAwMDSwZIDgXHUwMDExeFAuXHUwMDA10VKpNFx0n1x1MDAxM1x1MDAwMFx1MDAxOWPKj1x1MDAxOVxccpzCcVx1MDAxMp9cdTAwMDN/P5AwwFx1MDAwMn955XsyLF+vXHUwMDE5XFxcdTAwMWTDUuT1XHUwMDA2Jjo28s2Th92jTKV0obY297fJ6WbLO9Drr+d9/33y2z5f/PW47rmt/Xz1kGarXHUwMDE33lGzenFcdTAwMTJcdTAwMGV/yo/Pd9vtxkPsfV9+S+KSxrBSsHgotSSXhu4/RiNhXHUwMDEyaaRBgVx1MDAxYVxuYd+i0eTOnEyjiluodNpe+omk3o9IU1x1MDAwMzrGx+nExuiEXHUwMDAxNjZCXHUwMDEzmc6QbTDWjSDK+099S0uGXt1x637tcWi4+uDE/lkz8e5cdTAwMGI9/LxnJFx1MDAwZZ25UfPLXHUwMDE2uus1rzSM6cgvuLXXw3W/WIzPXHUwMDFkXHUwMDA1/HBcdTAwMTffsZ2dRfNcdTAwMWJtv+xcdTAwMDdu7XzQtsUnK5qciTZCXHUwMDEzYSPSmVnmV0/Oj2vlzMP+9l6tK+tHJrpkqWeZXHUwMDExyDLGNNpcdTAwMTFcZnHYsOVHmXE4xnnKXHUwMDE4jFx1MDAwYthcdTAwMDfm9lx1MDAwNHdsXHUwMDFhXHUwMDFjuKVcdTAwMTBIOSm7x5hcdTAwMDPKSGyyTeRQYsbzJngpXHUwMDFlg3nC0oUmtXR4LWqSXHUwMDEzKqibIONcdTAwMDH6W/DduoON6FwiMsXb++vTXFzwcHK2XHUwMDE1h9o/9Vx1MDAxY+VtXGJrKlx1MDAxZI6ux3CDs7PUZlx1MDAwNMLCMTgmXHUwMDE4XHUwMDFhUUYwMljKcVx0UiBSTZokXHUwMDFj++Rcbj9cdTAwMDQ7XWpKJ6CXOlJcdTAwMTJF7dRtjb9cdTAwMWVcdTAwMDUvN4JK8vGGLFx1MDAxZNhlsXFcdTAwMWRPWHOFgVx1MDAxM5394cp+b/eCXHUwMDA2N1t1cXpPj8J6JnNZ99OuvUZcdTAwMDFcblx1MDAxYdpw9DFCXHUwMDFiMVx1MDAwZVxcITHg11x1MDAwMiNdJZeKZj9Fe5F7XHUwMDEy47t/XHUwMDE2v/Fu/1D8isRAVzFcdTAwMTQhXHUwMDE5XHUwMDFmz7fgXHUwMDFifVx1MDAxNU2/c1jyXHUwMDAytl1RXHUwMDE5kTO79HZcdTAwMTW0XHUwMDE3J1x1MDAxOFx1MDAwN4HKudFcdTAwMTKNOGUjXHUwMDEwXHUwMDA2h0hcdTAwMDGaXHUwMDE5gtJrlkHwh0qvfZzLmFwiep5s4TtGw88oyOe/XHUwMDFkdO/Pe3dunlSOXHUwMDAyUz2JVP3dwlb0d7FHt1x1MDAxZkpccpmYRqdAUTw4mUPaN/d6XHUwMDA10T5Sgp1vfDvdr9Jet1dLu7RjbzuUY8dcdTAwMTNcdTAwMDFExaPCV1ttn1NcdTAwMTP0JahasFxmMT5B2pmy8bdcdTAwMTK/iLRzQVx1MDAxM/HLJEhOyVx1MDAxY88p71xcssd446RVydz1mt6J37qst1ZB2y2G0XegfFNcdTAwMTR3zSeEhuhaKMNAg/LlQsPfNGjPTEi2v4e4o61cdTAwMDai8YRfXHUwMDA0vclVS1x1MDAxOP9cdTAwMTjCXHUwMDE5mT1cdTAwMDHf2rlcdTAwMTf+dnBcdTAwMWJcXJU293dP+d7N8U1SIUhqxFx1MDAxNyiaXHUwMDEyXGb4lFbow1x1MDAwMMRcYnCFI21lndBcblx1MDAwNZGnPadhn2VRXHUwMDFlL877qeGLTE9cdTAwMTRfyYnSqFx1MDAwNLPj9+yqVbx42pG128JZVVx1MDAxNKNm7f6gsFxu4lx1MDAwYlQ7XHUwMDE0XHJcdTAwMWJojFx1MDAwZlxy3vN4bKhcdTAwMTDZwI1SiObUii9lXHUwMDFh8Dao+UXgy03yXHUwMDAzepuZxLBRzVx1MDAxZVx1MDAxNz5cXFb35bedyq6f9erB8VngPVx1MDAxY6Q+rYF65lx1MDAxMCptvVxmXGLsj9G0XHUwMDA2c9D14pQsXHUwMDE1pfFAIJ3ySzlGK4LJX1x1MDAwNcBCJeY1NKP2QVx1MDAwMZ1dfm9Ubkfz4lG1ZIr5Mjts1VtVs1x1MDAxMvKr7MNHNPpoXCK0UWN5XHLEsCRGM2LQbi1ZaPKh8quFXCJGxiOan1x1MDAxYr06Oatsi4Ml9sbsqYf7w1x1MDAxM9K4PLy99MONh43QXFxH+fOktFxcauRXceUwjNgwNiNcdTAwMDTFS41AlztSoCnGIaGGkbRnle1iIFxmt3+Z3IOEZP+A3Vx1MDAwNMJQmD33kMtcdTAwMTauvcr1TeusXrjNPmRcdTAwMGav+fHxKuivXHUwMDA1sTRacS2YfVx1MDAxNqTHQUxt9lx1MDAwMf1cdTAwMTSVii9cdTAwMDPij9VfyVx1MDAxOcM2/ir6K6eU+INcdTAwMTbGMJgjdVbee3wqXHUwMDExt37VaoS3hzdcdTAwMWI8W8Omp1xcf7VE4bOVSURb68BGrUNfXHUwMDE2gSphUcGXeibyXHT6y4kkiH4+T5nqXG7jXHUwMDE3ZGL2gffLis1cdTAwMWNld1ftzVMhzEGvfXfwKFx1MDAxYrVcdTAwMWJcdTAwMWFcdTAwMTb2V0F90Tw4VKBNsE/0uIxVwP2AMChbXHUwMDFiheBcdTAwMDK0walVX4bmXFxY+/CLgJcmi68xVGCwMrv3velcdTAwMTVOdffkXHUwMDAyrlx1MDAwZnfunlqd/cvk1Vx1MDAwManRXjSLqL3CYM8zNbR+91V7cUKWiFrN4s44ndqL5uHZ//xcIvCNXHUwMDE1q43AXHUwMDE3XHUwMDE1XHUwMDAwea7nSPxcdTAwMTa7VffxdrtcdTAwMDS3j3fu08XxiV9/zK6C9lwiT1x1MDAxZEJcdTAwMThcdTAwMDDKL1omOlx1MDAxZb4pZp8/UvtoWaU48Vx1MDAwYii9gPj+eZxvUq0+Jcm6S23yXGKHi8yM3MKW2cs8VkpcdTAwMTc7ep9lg9Pc0Vx1MDAwZZUrgVxcw1x1MDAxY3RcdTAwMGKEXG4lXHUwMDEwejCCXFxcdTAwMDKOXHUwMDEyNppcdTAwMDctUZ2TXHUwMDBi9lx1MDAxMTeuWFx1MDAxMLmxRN1cdTAwMDCqZFx1MDAxNJv9dYHA4lx1MDAwNYbLXHUwMDE2679cdTAwMDJ3Ql1P7mJcdTAwMGZuu7RLOlx1MDAwN/eiXFwq34WN2FQ6XHUwMDA0sbnrelx1MDAwNKNcdTAwMWFiOYPFllx1MDAwMsQqUN5YXG4wNC6DlVx1MDAwMGLoxFlXXHUwMDAyRI1m0jKAoVx1MDAxYlx1MDAxOK35J9NL/pN4XG7JZUmKISbto6qZaXr+UKrlNk0+XGLK7u5Fdvss4zG6XHUwMDEyNLXPXHUwMDBlKWFcdTAwMTjWac3VyMNxXCLt7MPxP1xmX5VKtkjLsHSSXHUwMDFiXHUwMDFhI6mdgDBcdTAwMTD5+PU0n09cIppcdTAwMDZcdTAwMTLRxUhERTKLXHUwMDA0XHUwMDAyRtrtNWZmUY5+bfaK1b2C2JK1m8JJt83yuVVgkVbELoanXHUwMDA2JzOiOZ1AI0BJsfGzkmy0XZ862VEmXHJX8bTxz8Mkllx1MDAwNiaxXHUwMDA1mVx1MDAwNIllhuhcdTAwMTjR/+NcdHpmJu2dbXSzl5fhrp/fyarr6Lq3ebGzXG5MwlDGYVLYXHUwMDFiRnuoKYwxSYKtz5bckPhjoHdl0qRcYmeMSZRcdTAwMTOpOPC5kqGrwiSRXHUwMDA2JolcdTAwMDWZlFxcs6BcZlx1MDAwN61cdTAwMDWfPf66XHUwMDBmz13aOFx1MDAwN5/Ip8fbsOh+O6+vRNZcdTAwMTZcZjhcdTAwMDLjL4M84dyIcVwiXHUwMDAxKENcdTAwMTkwW8r9z1x1MDAxMkmjs/uUzS8+n0gyXHJEkouau+RtXHUwMDA3XGLaXGKGnTk7k85uy3tQb3qV7Y2n3YjSXG5/MKvBJC5cdTAwMWRFtX28rFx1MDAxNWej1T9cdTAwMDRcdTAwMWPWL4RcIooyPeVcdTAwMTHexzOJc6FcdTAwMTVTn7BR3OdcdTAwMTNcdNJAJFiQSDzR22HQIKyhmWNKOi+b/MmFyLiFfWRJJLdcdTAwMWW71XBcdTAwMTWIJFx1MDAxNbo3YiTBXHUwMDE5WFx1MDAwYi1GyjhwSuKcUlx1MDAxMFx1MDAxY12VgOQyjqWIRGYhkt1yz65cbvhcdTAwMTlnJJVcdTAwMDZcIqlcdTAwMDWJZJL34Fx1MDAxNVx1MDAxYeNaXHUwMDEy31XyLVwi8XLQpHukWij1NiumSup1fytpOUuqiKQpdZRdUkw44lxcqdFcdTAwMTlJONTuzStcZmg2bUnLx6dcdTAwMWJcZlx1MDAxMcauqvlcdTAwMTmJxNNAJL4gkZLTdlx1MDAxMuyihDm2XG7ptIS/V2td3T1G3V3voFx1MDAwYse3Okqg0WLbslx1MDAxNd2w4r3zdlLaIVx1MDAwNqdcdTAwMTljjJB0JDrCOdlcdTAwMTFcdTAwMDKhi7GRYYpM2XyVgyhcdTAwMTXUdFxuTdyYTc6U9pbcSPGu+1a+XHUwMDFjWIGd0pZjZzZodqJ//Ttcclx1MDAxY/3RlKlMfe7QXHRUTa5BXHUwMDEz9lx1MDAwYlx1MDAwM0Do2esgplx1MDAwZnA6d1CU4IDk6Fx0cUJDYzi8fkhcYuZQTe3G2VxiMymSXHUwMDBi2Fx1MDAxNyYqY1x1MDAwZfpVQFx1MDAxOVDcME5iNVx1MDAxOINcdTAwMWQ+ibGb4jNb1Gp3XHUwMDAxZWPfi1x1MDAwMFx1MDAxYVBr9Fxc+/ksOlx1MDAxMVwi1WQ8mzJcdTAwMDfVwshtR5t+UPSD8vpQXHUwMDA1xsu3fGRnkPw+OVx1MDAwYlx1MDAxZNtK4lx1MDAxMG1QSXH0XHUwMDAwhUzSwY5Ltlx1MDAwYtymXHUwMDFkYUdcdKLQunGFWGaoxi9nXGaqPLyg+Hajpm9cdTAwMTVcdTAwMTdrVIZYzFx1MDAxOIyUXHUwMDE5XG68xMbJsUZRh1GluMBwXHUwMDA1zyGcwVijam5cdTAwMThtNdA0Rtj5J1xyP4hGO7nfm1x1MDAxYpbYXHUwMDE1z1x1MDAxZJNcdLyp+LFRXHUwMDA1aNp3XHUwMDFjluzBb2tcdTAwMDOW9P94/f2v3yeenVxmYvszXHUwMDBl38H7fYn/f26bkVx1MDAxOPditCdcdTAwMTiXc+z+ynVR+81vN6Ql/bPcXHUwMDE27+RPL5LyRynRLiPAkYaAfC4vieVl7PVcdTAwMWGYY5fzckSixDebkjlaVLtgtlx1MDAwNKyiknE6X1n34i4jXVx1MDAxYtss5zLyXHUwMDBmPkaE6bBcdTAwMTmvbVnMZ0Dy5pdGg/0yhtlr3adcdTAwMGZxOrmqwG4wK5GLz1x1MDAxYt5cdTAwMGZzVWpHKk5sZa4ynCfv0bMwVzGmt3swo1ZcdTAwMTiceDSZQF1cdTAwMDNcdTAwMGVcdTAwMTdcdTAwMTLDfi2pxKhg/NuXXHUwMDA04OVcdTAwMTBflPuRNmNhrs1oM6ZL/vCMjlx1MDAwZVx1MDAwMnWMK+A4hDqW9Xid0rmDc5/hKLtcbm9cdTAwMDAnw8VsxvTd2WKNsjWxXFzbXHUwMDFkXHUwMDE57JdcZlHB2ViTKDpcdTAwMTFA8ae2JsNcdTAwMTiQK20yXHUwMDEyXHUwMDExbH8yY+B9L4+RuMaXXHQjQZHZM1x1MDAxObXC3XaVX3tX8p7C42XuW/787C7dskWtdzOGXHUwMDExm1BcdTAwMTdGjqyNXHUwMDA0YVVNc1uCi1xu9/6qJSdsLz8xXHKo3u/LYaaZi4+srkVlofEt8j/aXFzk3Duvllx1MDAwZW/xoylcdTAwMGJaiymlTVx1MDAwNsNcdTAwMTJcdTAwMTLfjuvNXCLBqSOcVpIyW9XE7ZbwXHUwMDA02Ti8f1x1MDAwZqC5QFVcdTAwMTRMKrvNLUwphl+UpoY7dr9jw6xcIlxuPcFbyH5cdTAwMGLt14xxobiKnfLDW6CEXGJit8H4XHUwMDE0b7Ew1Wb0XHUwMDE207V+LZ7CwFHhxm6/ZPc+l3w8WWB3PVWKgTJcdTAwMDKHj6nFjMX0pVx1MDAxZSNuXHUwMDA3m4KjiZG60XbhsFx1MDAxZW+UcLTRXHUwMDAynaxcdTAwMTREYYipV9pbZJJcdTAwMTBsf8awm2Qtvry8/7rbbOYjRNzrcCCW/eKLQlx1MDAwZm5yvet7XHUwMDBmm5NJZnn25aU7rep4/SVL3798/z/RhT5iIn0= 901245673Input()Switch()Label()

There are three types of built-in widget in the sketch, namely (Input, Label, and Switch). Rather than manage these as a single collection of widgets, we can arrange them in to logical groups with compound widgets. This will make our app easier to work with.

"},{"location":"guide/widgets/#identifying-components","title":"Identifying components","text":"

We will divide this UI into three compound widgets:

  1. BitSwitch for a switch with a numeric label.
  2. ByteInput which contains 8 BitSwitch widgets.
  3. ByteEditor which contains a ByteInput and an Input to show the decimal value.

This is not the only way we could implement our design with compound widgets. So why these three widgets? As a rule of thumb, a widget should handle one piece of data, which is why we have an independent widget for a bit, a byte, and the decimal value.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nOVdaVNcdTAwMWJLsv3uX+HwfJlcdTAwMTdx6VtZlbXdiFx1MDAxN1x1MDAxMyDAYEBcYiPWXHUwMDE3XHUwMDEzjpbUWkBcdTAwMWLqXHUwMDE2ICb831+WzKDW0kJcdTAwMGLybVx1MDAwMSYw9KKurjqZeTIrK+s/nz5//lx1MDAxMvXawZe/Pn9cdFx1MDAxZYt+vVbq+Fx1MDAwZl/+cMfvg05YazXpXHUwMDE07/9cdTAwMWS2up1i/8pqXHUwMDE0tcO//vyz4Xdug6hd94uBd19cdTAwMGK7fj2MuqVayyu2XHUwMDFhf9aioFx1MDAxMf7L/cz6jeB/261GKep4g4dsXHUwMDA0pVrU6vx6VlBcdTAwMGZcdTAwMWFBM1xu6dP/j/7+/Pk//Z+x1nWCYuQ3K/Wgf0P/VKyBio9cdTAwMWXNtpr9xlx1MDAwMkMtwGqAlytq4TY9L1xuSnS6TG1cdTAwMGVcdTAwMDZn3KEvT9XLm28n33vfOrlCrprJt2VcdTAwMTFLg8eWa/X6adSr95tV7LTCcKPqR8Xq4Iow6rRug4taKaq6XHUwMDE2jFx1MDAxY3+5N2xRT1xm7uq0upVqM1xiw6F7Wm2/WIt67iXZy8FfXHUwMDFk8dfnwZFH+ktZjzPNJVx1MDAwN8tBoVQvZ93tdNBDg6BcdTAwMTQ3hiGzI83KtOo0XHUwMDFh1Kx/sP7XoGFcdTAwMDW/eFuh1jVLg2uwqIKyXHUwMDFjXFzz8PyyXHUwMDEywWOCc8HBXGJgTL5cXFFccmqVauRcdTAwMWGiwZPGcC0541x1MDAxMjRcdTAwMGXGLlxm+oNcdTAwMDKSziM1cXC3e357v9RcdTAwMDfIv+Nd1iw9d1mzW69cdTAwMGaa7E7sxEA1uKfbLvm/xlx1MDAxZZTWRiDn2sKgXHL1WvN29OPqreLtXHUwMDAwLv2jP/9YXHUwMDAwp1x1MDAwNEZMXHUwMDA0KpeaXHUwMDAzXHUwMDA3XHUwMDEwM1x1MDAwM/VwL7NxcJcrlc+yqnfUqu9njoVcXFx1MDAxY6j8jYBKw/5cdTAwMWFStWeVRNRouZExXHUwMDAw9G9X6Fx1MDAxOSOBsGxBW7RmXHUwMDE5pEZcdTAwMWS/XHUwMDE5tv1cdTAwMGVcdTAwMDFhXHUwMDEyWtFTglx1MDAwYlx1MDAxNExcdTAwMDLAXHUwMDA0sFxuxTxcdTAwMDPMXCIqISRcdTAwMTiuxsDKjdBKk5Z5Z2DVXHUwMDEyXHUwMDEysVxuKNFqasrMWL3LXHUwMDE2dq5cdTAwMThuPcHm4VZWl0s3XHUwMDE3359cdTAwMTKwOoK3v1x1MDAxMaWSXHUwMDE5bYVS1oBRYyils6TMNKCyTOtcdTAwMTWiVHhcZpQkUaGnWW3GYcqNp1x1MDAxNFx1MDAxM2hcdTAwMTTjSlx1MDAxMKTHYGolkKDhOqrUoF6vtcOJXHUwMDE4VUYkYdRcdTAwMDJaxpTUM0P01lx1MDAwNPzkoFx1MDAwMPmbrd7RiVx0ermjy5NFIPpWXHUwMDE2/3WIaushXHUwMDExXHUwMDFjXHUwMDFhc6GsiGnK/u1cdTAwMWE9lFx1MDAxNmjotbJWxPpqXHUwMDExk1/2JXGLcXiCoDZwLq1cInvOkSz3XHUwMDA0m889qVx1MDAxNFpSoYxcdTAwMGLBUY/iXHUwMDEzJFx1MDAxMVx1MDAwMuBkXHUwMDAy31x1MDAxNUC10ElcdTAwMDBcdTAwMDVcdTAwMDKv5ZbPXHUwMDBl0KenzUaNXHUwMDFkXHUwMDFlbaszWVx1MDAwNX9v92t19yzdXHUwMDAwXHUwMDA1pjwynMqANcYy4GJcdTAwMDSiwuNkPi0zilx1MDAwYkviuiREXHUwMDBiRDlXXHUwMDA1UU28lIGAd4ZQm2jmOelcdTAwMTR6aTE7QkWb1W52oqPsTvXmQF1cdTAwMWbnXHUwMDFizcJWylx1MDAxMSrQXHUwMDEzXG40k2RnLfkvOIJQwlx1MDAwNYGT/Fx1MDAxOYtkXcUo/5hcdTAwMGahwFx1MDAwYsaoVSGUXHUwMDEzOrVUxM7WXHUwMDBmoq84TixcdKRgiXxLLYWcXHUwMDE5pfXNzon6epCv6G+t81x1MDAwNopM/qmeSzdKqas9TV6xXHUwMDAy8kaI5ckhkHJpyLcnT8pcdTAwMWHpPEi1XHUwMDE0Rsvl8iSATkBkrM//a8aJf1x1MDAxMvNcItdtXHUwMDBlXHUwMDA0/lx1MDAxN1x1MDAwYlx1MDAwMzSI5yM/k4H5cs/g7lx1MDAxOJqi4HHApGNDn+/ulvf38o9cdTAwMGbly1x1MDAxM7v1vWefXHUwMDBluzdfXq77+fzbb0P9UDtjgJc2XHTvhlx1MDAwNpuTzzl7mGDyO0+Ge9UvVrudIPWAV+LtXHUwMDAwP9X5ilx1MDAxMZaBtzVcdTAwMGV74thcdTAwMTaRpzRcYjBcdTAwMTjrVjM6rT31jTtcdTAwMWI6uus3avXe0HD1sUn989nGuy9cZuh5fcVrhq7crNcqzb52XHLKw5COakW//nK6USuV4kq+SFx1MDAwZvfpXHUwMDEzO/uz6OZWp1apNf16ftC2JazKlLCxZuQsky2dnfucibNN0auD3GiI7c7DztNcdTAwMDO/a6ZdzFx1MDAwNFwiiVx1MDAxOSlsziwoXHUwMDE0w6E4XHUwMDE0xrMo+yFcdTAwMDUgeK8uXHUwMDEyh8LTTFklXGaToKScXHUwMDE0i+PcU9pKbUnyXHJcdTAwMDNmx2NxKFx1MDAxNbXXmnnEcCHrk1x1MDAwZVrEWWL4XHUwMDAzhJRcdTAwMDaQ3P6ZXHUwMDAxfHC2KcOL6Nthpn2OXHUwMDA15V/dV6r3f/+8x1xmIFbSM8BcdTAwMTVcdTAwMTB7N9rYYVuByDyJZCVcYlrWWFx1MDAwMtdS7FxiWZFJPclQeIzcXFxcdTAwMTIoJS11PUxcdTAwMDAweORBanJ6ibxcdTAwMGIjzSh+OWlcdTAwMWOpXHUwMDE5zuNhrjV8XHUwMDEzWVx1MDAwZVjqSnLI2OzwbTX09fZ2ttjL3NbuNkVcdTAwMThcXFx1MDAxZUo/7fpXcu2Ri0lcdTAwMWVcdTAwMWYpNOByXHUwMDFjuoygXHUwMDA0XHUwMDFjOCNcdTAwMDeUL1x1MDAxNcH7XHUwMDFkXG6YMcmN4UZ/XHUwMDEwXHUwMDA0y+T5PMWJunJtZvdLg+2Tk30ondejnVx1MDAxZvLxurXFi9dmXHUwMDFkXHUwMDE0sETjcSFQgVx1MDAxMJprMCMoXHUwMDA2wo22StF1xDFSq37pXGZcdTAwMTNcdTAwMTaIQ3xcdTAwMTD0qsTgtJFSxVxy6WvQLdT3KmdcdTAwMDeb96fFQGm1n+ncdjN3aVe+XHUwMDAwzCNqa1x1MDAxOTjSKKRcdTAwMWVcdTAwMGVOo7BcdTAwMWVjKDRKXHUwMDAxXHUwMDFh5OqczDdSvmQwrTBcXHxcdTAwMTD4XG6ZXHUwMDE4XHUwMDE0JFx1MDAxMWdO+9qZXHUwMDAxvHt4e3pZzp0/tJqN/ePK9SPb7lx1MDAxZa+D7nUgNlx1MDAwNFFitprQg6PK17lwmoNVTFxuS65cdTAwMWMug+J/XHUwMDE4ZVx1MDAwMjshfv1cdTAwMTbql8yHm1x1MDAwMjRcdTAwMWaE/VxuTI4+SO2yo+RcdTAwMWNJa2fnu5eH29vlXHUwMDAzfVx1MDAxNlRcIlZu4o/N9CtgRbzBcMVcdTAwMTjXqK1cdTAwMWVGLpFfXHUwMDEwXHUwMDFjmJFutmOFXHRcdTAwMTZvo35dhlx1MDAxMCNcdTAwMDb/QbQvxnyVUfKgLLWC4eza965Z7+BBuVuA4kE2f3GQffpRTprZTpf2VS7zhli+1eSxas3GMcystYJ4XHUwMDA0MSrUy8VcdTAwMWVWqX1dviZIXHUwMDBlXHUwMDFmxHNDlpzdpvv5fHaOXHUwMDE5xd5pMTRFznd2s51K5nLv5OvGblLKcGq0L+fWI8IghZtHUVxc4Fxidok5SKZcdTAwMTDRzX2DhKWYw+/gv1x1MDAwMP08zY+igE1y7EGgy5+Nx9deQzDkq6adLVx1MDAxN24u9+5cdTAwMWHb19nKg9pPQdL7bCgmssQ5aWF0UbIxXHUwMDEwI1x1MDAxZFdSo1Nvy2VvrFL/KlwiOvTNPlxi+0WTXHUwMDFj+yW/jaivkLOjt3rQyYeNw/bOWXi3u5FcdTAwMGZt8VtcdTAwMTZTr3+N9lxiXHUwMDEw5LZcdFx1MDAxN26B8ehcdTAwMDO4XHUwMDE4XGZotDruLKRU+yrSOZqA9EHUr9TJXHUwMDA0wjDUXHUwMDEwp1Kv4fe+yO5OK7WwsbeFm9cnd5FfbZXXQvtcdTAwMWFDXHUwMDA0V1x1MDAxYmktM1x1MDAxNuOZXHUwMDE4LyBcdTAwMTY0NuS/uUVYfLnMpJXyX+E+wX6U2K9MzpyHflx1MDAxN7mMlpnxe+37aifb/da5rCt21oyAIT9Ku/5Fhlx1MDAxZVfoXCKmjjnwUeZgPcFcdNZklC1PfeqDXHUwMDEyaDR5mlx1MDAxZlx1MDAwNL5KJUZcdTAwMWaASa5cdTAwMTXOXHUwMDEzfrjZle3vR+qSseuef3WV2z82lfN1UL9cdTAwMGXC5KGRLTJWS8PNuPrlVmtcIsdcdTAwMDC4ZHL9SpWvZkyTz/1RtK/iidqXM6Vccqp4mspr8O12evuHXHUwMDE13Fx1MDAxMVet4F5vZXgzb3qp176qr32ZtVx1MDAxY7hcdTAwMTAxr/1F+2olhVx1MDAxNkwxxLRcdTAwMDd/wdL7WMnYXHUwMDA3XHSfUZcl6l9LbJBjXFzdvFx1MDAwNuDHvdtKeHPh8+vHXFw1v3lzXHUwMDFmXT9l10L/9leBgjbKXG5Ffc/lOIpcdTAwMTGFW+5O/2C5OYzVTr4xg1x1MDAwMqx8P5mTSan1XHUwMDAwidDlXGbBXHUwMDE5ojnm3W7bX4PM1VnQ/V45yD49Ni/CcmEt5o0lck9cdTAwMGLnsVx1MDAxMWqJXHUwMDAxj0RcdTAwMWaAk/7lXGKSVLNEmMJcdTAwMWRcdTAwMDRcblx1MDAxZlx1MDAxN0RubLRcdTAwMDdQZaPYXHUwMDA06Vx1MDAwMtBcdTAwMTg3mqvMrid/NVx1MDAwNpHFsutjXHUwMDFm8Ep2/VDPXHKS63HowlmT66NWOymzfuhcdTAwMDVG0+jZ9Cz6JFHSyYtbXHUwMDA10VFrxFx1MDAxY9lvW6J9b3PhUfugoDG30WlcdTAwMWSZncw6SFx1MDAxMlx1MDAxYfQss8LNYVx1MDAwYsVwNInTZVx1MDAxOXEpXWcoMlx1MDAxMlOWYS8jSpNoy7gkUWPcYtx5WMq6XGJcdTAwMTKkQZBgMUFcdTAwMDKZKEnkyoJi8VmtV1NB7ptf4ewqd1x1MDAwMUenbV3M5i+KrcZaXGJcdTAwMTKxXHUwMDE0cNEnRiBlio+LkbHOPLtcXDzF/1aLhIKcXHUwMDEyXHUwMDFiX4L7flx1MDAwNImnQZD4goKkk1x1MDAxN1xuK7DkW1x1MDAwM5+d3PXC6sPj7db53fX33Yvz5tXF1d7xWqSlXGJcdTAwMDaelsI4v1x1MDAwNInLjYpcdTAwMTLJmZGMPG9cdTAwMTJcdTAwMDNjkr2SpSRpklx1MDAxYjIuSeSSXHUwMDE4l1x1MDAxZP5cdTAwMWW5XHUwMDFkpkGScEFJSk4vUORcdTAwMGVYXHUwMDA1c1QuMVdcdTAwMDf4taDOiz/OXHUwMDFh55nocKdcXM111kGQOCrPOs+USVdcdTAwMDVCglx1MDAxYZMkblx1MDAwMazi2lx1MDAxNV5YlZs0kyhx5Vx1MDAxMu90LMfs/UiSTIMkyUXJnUmSJFJ+Uiszz1wiocvDo4ctc7b740T29i5yZYjqtbVcdTAwMTAlsjVcdTAwMWVcdTAwMTlcdTAwMWNcdTAwMDZE4MhcdTAwMWRcdTAwMDJtxkTJoGJcdTAwMDY12Wlr2GrcpFx1MDAxOa1cdTAwMTJcdTAwMTlPRoZxrnDYusiSSoMsqVx1MDAwNWVcdJP5XHUwMDFk00R7mJhjyX6mlM1sh+a8cZMrXHUwMDA03ZNWbvPworJcdTAwMTayxJXHXFzNLGt5v0TTqCwxz0pE1NZVfF2VKLFcdTAwMTlFXHRcdTAwMTmq91x1MDAxObzTaVx1MDAxMCW9mChxlrz+RLmiySDZ7GZcdH50wzB6aLDj8z14ZFmf5UpJ1S9SJUpCcY9cdTAwMTO3XHUwMDAzMJqTXHUwMDBlMSPRO2Y90iuuQFx1MDAxZFx00rRVKKuPOmhUXHUwMDFhLYjfXHUwMDEzdEAyw/DbJEmkQZLEXHUwMDEyxWRscja2XHUwMDE0ynKNZnbD1Ny/vTu4lFulzrm82snrS773mH/jXHUwMDE5/ZJcdTAwMWZWg1x1MDAwNFFiXHUwMDBiiZJcdTAwMDTjITdMMlx1MDAxN1x1MDAwZZdsJOrAhMcsXG6ppNJkqKfU4Sha7nN/qihNndN3XHUwMDE1pN1cdTAwMDNcdTAwMTQ3rmrNpNJlWnmayKawQjBqT9xxe/amXHUwMDE4J1x1MDAwZahQvZ3dej4xUrLsOdRkbOPh4TJ7XFyyt5uV3Yub3o5ccl/ebFxihn6n03pIYc0ySF6PS+4zMVx1MDAwMFx1MDAwMDV7XHUwMDE0+yravc0yntssfisp2/leOdkpJk1cdTAwMDelR1x1MDAwNJRcdTAwMTSeXHUwMDA1REt9bp1cdTAwMWIzJFx1MDAwMsJcdTAwMTJt45qTOpBcdTAwMTIlT15Rs6xcdTAwMDRALD9jSt0yJVG4+mlvZ1HWXHUwMDA345Ps1exF0bZq0elDjVx1MDAxOMw//2ey5VqsOtqilivenKnC+6tjJ0hvPFx1MDAwM2tsa1x1MDAwMtMvkzBHStr0oU5pSppC6TF62X6VXHUwMDEylzI5Yr9cXD14QSBcdTAwMTP0rfiUelx1MDAxMEvbL+Zxi1x1MDAxMo0w9CBcdTAwMTNPm1x1MDAxZlxis/Ikama50UowXHUwMDFlr/k6qMVJhpjFd5RIZVHCMPI70VatWao1K1+Gsoqet4XZn8Eg9CW22O1rbk+7jG3JXHUwMDE5aMuJdsjYRVx1MDAxNb9Nl1x1MDAxOI+4LVxyN/F+RaOtn89cdTAwMGbSloJm6fVcdTAwMTZNZ2lDLVx1MDAwMueZ0oOk1Vx1MDAxY21sqeVLk8Ajb11Zolx1MDAxY9KQbVx1MDAwMDueS1X3wyjTajRqXHUwMDEx9XyuVWtGoz3c78pNJ+LVwFx1MDAxZlNcdTAwMWH0UvFzo7qg7T5xWIlcdTAwMGZ++zyQlf5cdTAwMWYvv//7j4lXbyRj2H2No3fwgZ/i/y9W2FFPKSymjVuKPUf91Ivi9cHTRifs7FfK/KZ519RVm5RcdTAwMWSeXHUwMDFlJmKkJ1x1MDAxNaGNSctcdTAwMTSPr2v5xUS0p6VcdTAwMDRgVkvNpuSkXHUwMDAwKUO/sLguI5XpKdRu6p5+XGJpJySqXHUwMDE4XCLj5NhKrV1cdTAwMDaeVLFuet69QjO0Kp4/uUqeclx1MDAxNvpPcHxRr1x1MDAwNyivRKF1nrHifM24uJ1qzSWNylx1MDAxY8vbj/Ld7t2PTDPfXHQvXHUwMDFl7cVcdTAwMTM8QEGnXlx1MDAwMiR6iEBcdTAwMDZbWaflcZiLS4PkXHUwMDAxXHUwMDEy+pnb1YdjMlx1MDAxN19WXHUwMDAyJm4sNKGGsHSRUivecCZ8fUC+JFx1MDAxOe9FwX6z3Y1SQsZjzVmMjHNIXFzexFx00W5XojnSWKZcdTAwMGZ1Ssk4MOOJX8XXmDDkTVx1MDAwZlenkFp5XHUwMDE2hVx1MDAwMSO1ZdM2XHRbVnyJqlCPc86MsYbHp/hfhNlcdTAwMTJ5c3tHWGO4dcHkMdnWxEJcdTAwMTU19H1Q8en2IEZ8N4j5up3C0FxyoUFr9ThcdTAwMTfXnuHSXG43TSU1qlx1MDAxNyY4J1x1MDAxOZ/O0obdXHUwMDAz0vi/Qn9cdTAwMTJVrNrugIuvNfVOXHUwMDA0rPvaXHUwMDE4w+qbXHUwMDEyb56Y6kBcdTAwMWTNNeBcdTAwMWMxhLMrv36+fdPbPtBfe49cdTAwMGa7ufvd3lsvKp7GOlx1MDAxNtySS3jo0qxcdFmCSzWyuSFnrqKv1lx1MDAwMiXTgvh3Mu2QRYustFx1MDAwNPFcdTAwMTbcI34vgLqd+t5MmGJcdTAwMTJgPCW0KzFM9IjJ8Z04ibZcdTAwMGJt4uuaVslJXG7lw9ZtXHUwMDA3mtunnfLpcclcdTAwMDS3N42t9Vwi3skxcCBcdTAwMTJKqnCeXHUwMDE4+OZjq7jTuS40N+Do/vvtzs2VLrdTL1x1MDAwMZxxr79lXHUwMDAwMuVKQlxme56ESE/2tylSbqNcIm2SXHUwMDBim/CC8UWwXHUwMDA08eaTplUnXHUwMDEwb7fRXHUwMDE1zLVpwHvB+PK8e6e/jXN6iPdLe1x1MDAxNmPekFxck95tXHUwMDFlIIycI+11+linlHiTufBcdTAwMTi3RJtcdTAwMTVze9uO7NiHzPFcdTAwMDdcdTAwMGJCMUcweHJCxNLia0hRKCPIXFxcdTAwMTJNIco2gXlL44JLxlx1MDAwNbpcdTAwMTUqsmJcdTAwMTOkW1omh2pcdTAwMDGvM/Weblx1MDAxMoapN2Ouklx1MDAxNtFu5crzgIs7j1Nd8MiddDXPXHUwMDA1uVOAOFx1MDAxZXaeiX5PJ2tD9Jsxji40LJjWNGh2kFx1MDAxZftOXGJ4MnDd1zhkk1x1MDAxOPin5yd88dvt04iA9tL/XHUwMDA04VrpWV1cdTAwMGZe88t9LXjYmryjm9vU7dNzhzrFXHUwMDEz9Fx1MDAxN3D//PTz/1x1MDAwMcOHY8gifQ== 901245673BitSwitch()ByteInput()ByteEditor()

In the following code we will implement the three widgets. There will be no functionality yet, but it should look like our design.

byte01.pyOutput byte01.py
from __future__ import annotations\nfrom textual.app import App, ComposeResult\nfrom textual.containers import Container\nfrom textual.widget import Widget\nfrom textual.widgets import Input, Label, Switch\nclass BitSwitch(Widget):\n\"\"\"A Switch with a numeric label above it.\"\"\"\nDEFAULT_CSS = \"\"\"\n    BitSwitch {\n        layout: vertical;\n        width: auto;\n        height: auto;\n    }\n    BitSwitch > Label {\n        text-align: center;\n        width: 100%;\n    }\n    \"\"\"\ndef __init__(self, bit: int) -> None:\nself.bit = bit\nsuper().__init__()\ndef compose(self) -> ComposeResult:\nyield Label(str(self.bit))\nyield Switch()\nclass ByteInput(Widget):\n\"\"\"A compound widget with 8 switches.\"\"\"\nDEFAULT_CSS = \"\"\"\n    ByteInput {\n        width: auto;\n        height: auto;\n        border: blank;\n        layout: horizontal;\n    }\n    ByteInput:focus-within {\n        border: heavy $secondary;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nfor bit in reversed(range(8)):\nyield BitSwitch(bit)\nclass ByteEditor(Widget):\nDEFAULT_CSS = \"\"\"\n    ByteEditor > Container {\n        height: 1fr;\n        align: center middle;\n    }\n    ByteEditor > Container.top {\n        background: $boost;\n    }\n    ByteEditor Input {\n        width: 16;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nwith Container(classes=\"top\"):\nyield Input(placeholder=\"byte\")\nwith Container():\nyield ByteInput()\nclass ByteInputApp(App):\ndef compose(self) -> ComposeResult:\nyield ByteEditor()\nif __name__ == \"__main__\":\napp = ByteInputApp()\napp.run()\n

ByteInputApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258abyte\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u00a0\u00a0\u00a0\u00a07\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a06\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a05\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a04\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a03\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a02\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a00\u00a0\u00a0\u00a0\u00a0\u00a0 \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

Note the compose() methods of each of the widgets.

  • The BitSwitch yields a Label which displays the bit number, and a Switch control for that bit. The default CSS for BitSwitch aligns its children vertically, and sets the label's text-align to center.

  • The ByteInput yields 8 BitSwitch widgets and arranges them horizontally. It also adds a focus-within style in its CSS to draw an accent border when any of the switches are focused.

  • The ByteEditor yields a ByteInput and an Input control. The default CSS stacks the two controls on top of each other to divide the screen in to two parts.

With these three widgets, the DOM for our app will look like this:

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN1daVPjSNL+3r+CYL/0Row1WXfVREy8wdnc0OZo4O1ccsLYXHUwMDAyXHUwMDBijG1kmWtj/vtmXHUwMDE5XHUwMDFhSdZh2fiiPUPTLclyuiqfzCezslL//bKwsFx1MDAxODy33cW/XHUwMDE2XHUwMDE23adqpeHV/Mrj4lx1MDAxZvb4g+t3vFZcdTAwMTNP0d6/O62uX+1dWVx1MDAwZoJ2568//7yr+Ldu0G5Uqq7z4HW6lUYn6Na8llNt3f3pXHUwMDA17l3n/+yfe5U79+92665cdTAwMTb4TvghJbfmXHUwMDA1Lf/1s9yGe+c2g1x1MDAwZd79//HfXHUwMDBiXHUwMDBi/+39XHUwMDE5kc53q0Gled1we2/onVxuXHUwMDA1VET0XHUwMDFm3Ws1e8JSXHUwMDBlVFx1MDAxOcPDXHUwMDBivM4qflxc4Nbw7Fx1MDAxNYrshmfsocUr9nK7d9N9rJtm6eB43Vx1MDAwZs5L5+vhp155jcZh8Nx4XHUwMDFkiUq13vUjMnVcdTAwMDK/dev+8GpB/dfARY6/v6/TwkFcYt/lt7rX9abbsd+fvFx1MDAxZm21K1UveLbHXHUwMDAw3o++XHUwMDBlwl9cdTAwMGLhkSd7hWFcdTAwMGVQQY0xRIDR6v1s7/1cblx1MDAxY6GZltQoKpRhfXKttFx1MDAxYThcdTAwMTMo17/EXHUwMDE1q1V5KNllpXp7jeI1a+/XXHUwMDA0fqXZaVd8nK/wuse3b0xEONB117uuXHUwMDA3eFCa8PPc3rhcdTAwMTOliFx1MDAwNk01fz9jP6W9WeupwH+iI9OsvY1Ms9tohILZXHUwMDEza/1qXHUwMDEzVZ2Y+lx1MDAwNO5TKG1ksluVdndcdTAwMWJMUN7erizz48btaaVZXny/7p8/0m/7+ubO3vdye31cdTAwMWb2dlx1MDAxZb7V+MWe/H7sPsU/5dfnV3y/9Vj0vjv+wVx1MDAwZn61U/7O+ePekeeXvNXvlWL3fftbOIDddq3yqutEKlxyXFyDUSZcdTAwMDKXhte87Vx1MDAxZttGq3pcdTAwMWLC40tE4Fx1MDAwNCxj41x1MDAxYUGkzEakXHUwMDAwbrSRQlx1MDAxNkZk+iSNhEgyNURSMI6IIDJcdTAwMDKC3lxyXGadXHUwMDE2JClNQpKKXHUwMDA0JKlihlx1MDAxMEXk2CCZoYVKXHUwMDBiqUBLoYbQwnC2W83g0Hvp2XaIXHUwMDFkXa/ceY3n2IT11Fx1MDAxM1x1MDAwN2j5OXDXev7m67+jI9lx8ZPtrYiOvWep4V1bNV6s4ndx/ZiGXHUwMDA3XHUwMDFlurD3XHUwMDBi7rxaLeqUqihIXHUwMDA17+lvXHUwMDE2cSYt37v2mpXGUZqcucB7XHUwMDA1flxu8lxiRIal31x1MDAxOVx1MDAxMqI1IyxyxSDs5du4ecWe0Fx1MDAwZYBUgilKXHUwMDE1NSBj2KNcXDqCXHUwMDE4oVx1MDAwNbUjolQ2+KD3XHUwMDFhXHUwMDFkfEY5QDgxXHUwMDA0lOT4k0SiXHUwMDEwXHUwMDBl2lx1MDAwN4WAXHUwMDEwXHUwMDFhgCnSXHUwMDBmTHtKgpHDuMrQqfxSXHUwMDE4+nbknynDtVx1MDAxM1T8YNlr1rzmdVxcsDfOV1x1MDAwNCf2O1fa1q04hlx0nFPKcUg0ZyxyxVWr2rXfo1x1MDAwNFx1MDAwZcd5XHUwMDE30nAj0c1IymTiy7vN2mChLq7u/aP9l2Zru37hi43qQ1N8f0xcbmVcdTAwMWOCdp5cdTAwMWHG0KArSXm6UIJcdTAwMDHHadRIzigorlx1MDAxMjI1Kp1gpXV351x1MDAwNTj2XHUwMDA3La9cdTAwMTn0j3FvMJcs8utupdZ/XHUwMDE2v1P0XFy/iWjbO8apUvi3hVx1MDAxMEG9f7z//T9/pF5dylJs+0qodHi3L9HfWbYtl+tTKmX/4Xf7XHUwMDA2SC6oXHUwMDA2XHUwMDE1erxB9i1/jmdi39Qg86aII1x1MDAwNdVUcEMojm+c7DNcdTAwMDKOMlx1MDAwMq1cdTAwMWLHWUDjJ/rkXHUwMDFhn3UjOiQ171x1MDAwNi1cIv4vXHUwMDBihryTcsZDXHUwMDFmO1x1MDAxYrJ/6W8/updcdTAwMWKb9eP6snfmnrfXl1fOZk32L7rPOytwt/tcdTAwMWPcd8pAXHUwMDFloHJTL81zXHUwMDEwMZI/XHUwMDE4KYigVOssqGtmJEFcdTAwMTdQnMmkz/6cI13mI11MXHLpTCWRLpNIXHUwMDA3gaKCoWND+qRjiEi4MyCGWPnF7L/+bNpcdTAwMTNV9NdcdTAwMWS38/fPxaDV/rn4s5lcdTAwMWVZXGJcdTAwMWW703vg0HCvgtHjilx1MDAwMW6rP64oIPtcdTAwMDc8slxmeVg/TFx0Rr+AlEdcdTAwMTfH6crhnqqdbHzb89ae2lxcb7i+u+TNe/6NXHUwMDEx9MlcdTAwMThYIf9AXHUwMDAyQkGyXHUwMDE4UjlnjqaMIDUhXHUwMDFjgOpMoH442jeQXHUwMDA0qkpE+5Io4JJFvtdsXFzyzfpccl8+u1/deKhcXH6/a+12N48vr4q6uJuLk12x3azed4+f6jdUXHUwMDFk3d22XHUwMDFlxuA6XHUwMDBmbsr153OvfHrbeVhT61stf3XDXHUwMDFiw323r6ube+tcdTAwMGZPasv9UTnaa+/drWP8MS6XzFx1MDAxNGOhXk3KJUtNM7HOuOGaUiiO9fTpn6lPLoB1XHS5WEd6zlwiWFdcdTAwMTPDuklL7CV8sjXBlKE846Pf8+OUbcJss9nuXHUwMDA2WXm9XGbv+9G83lx1MDAwMCeVltf7JebojpYh581cdTAwMDKfMMC1UkNk9nboXHUwMDBm/2bHXdl9WuvSMrku7Z+Wm/PuZylTXHUwMDBlXHUwMDE4xJbgXHUwMDFh/zPxxJ5cdTAwMDTiXGKNJopcdTAwMTjFpFx1MDAxMlx1MDAxM3SzJMXNykTujjD0sdJwOevQl2JQeVjdO395emr5vPHy/HJ/dTRrP3tx841cdTAwMWVtPK2xTX/l4rl+u+5cdTAwMDdwPob78v3DXHUwMDA3s7NcdTAwMTI86lx1MDAxMu8snWnXK2k1Lj+rqWZcIlT4XHT5WcZ19pI2hnpcbqO+SEZ4XHUwMDEw1tOnf879LOU8XHUwMDBm65Q6MFx1MDAxNayblKx90s1SQimyXHUwMDAxXHUwMDBl41vSXHUwMDFlp1x1MDAxNn7QzXrB4aNcdTAwMTdU619hun52gJNK+NmInFx1MDAwYlx1MDAxZlx0aVx1MDAxNWR6WoywMJzT0lx1MDAxNF/AfvK8yr1/e3dSWq+wXHUwMDE3ft899PZmvIgmXHUwMDA3pp7AXHUwMDExxiiOP4wzwXhcZn2cI8lVlFx1MDAwMVd4XHRcdTAwMDOSzXI/nHoyPIk/naS5XHUwMDA06ZEydIy5p9FcXO3a/snN7qM8XT2g66XT1WD7eUOuTSFcdTAwMWI82HVNKWurc7CjqUKFkUOkg9KHc86xI3KxI+LY6WfP41xcnyFJ6CSztkhmmcJIVY4vXHUwMDFiND9cdTAwMTHia9j1mvbslSrWW42a6//9c/FcdTAwMTKDss60XHUwMDEzt1x1MDAwM1xcQb9DKyR9LkwzXHUwMDBiRVx1MDAxNCGZMLWL2ZrpyIrdwHXUXFzDNadcZpNI5lx1MDAxMFsqXHUwMDAwXHUwMDA2tZ/RXHUwMDE4TJm2oSaA0JZeXG5cdTAwMWVZdlx1MDAxZTdOwaFcdTAwMTRxITV+XHUwMDE0YsPoXHUwMDE0jyeYXHUwMDAzQjFcdTAwMDCNYT5cdTAwMThcdTAwMTFBwHuih4PNtqthKrg+XaVIwaJcZulQw7mixtbgXHRJZFpVXHUwMDA2OFx1MDAxOEpJW4kqUG6NLMYkvnyhSpF8VL9cdTAwMGJFcJ654UJcdTAwMTBbmWKkyKhfXHUwMDAxblBcdTAwMWKkoTjjSlFJXHUwMDEyUn2qWpFs9bavpGKHN/xcdTAwMTL9PbSFo1xcZK5cdTAwMWZcdTAwMTODNIUoPVx1MDAwNIvPT4PMqYljWjvSXHUwMDE45F2ao1wi0ThcdTAwMTVBaDiK4yBQsGUz0ULtcVx1MDAxN8JRXHUwMDA3yZ8gqM5MUqHCiVx0iVx0d4iU1GhGgWiTXFy00ohcdTAwMGIzY/uGcpFIiD9++5afWY7bXHRcdTAwMDaaIZxcdTAwMTjYKmI0/ilW0NixJoxwUFx1MDAxY936aOYtP1xuj8uEQ8RcYrJeXCJ6dc9JmexKqVwi1iRTYVx1MDAxZFx1MDAxNv/cxi1Tse0rodJDmrb8JFx1MDAwNc+u9MVcdTAwMTkgXHUwMDE0lVx1MDAxNYrnXGJ3L1a+lcs3zD+puZetnSXxtNahc15cdTAwMWbDwDjoVDRDXHUwMDAzJ1x1MDAwNbKPOIMj3DGMgFx1MDAxMpzgXHUwMDBmzsLkXCKtVINcdTAwMTZJirwlXHRcdTAwMTFcdTAwMTNcdTAwMWMjv1kvXHUwMDA3bJ2s3DWvK8es/qw3ZGX95P6YXHUwMDA2s15cdTAwMWX/XHUwMDFkKtY4z2RcdTAwMWNI5jgyXHUwMDEyXHUwMDE4XCL3kT5Nc1x1MDAwZUkh8iDJtMOnXHUwMDAzSV0obY9cdTAwMDabXHUwMDAzj/Ki3yf1XHUwMDExVn1NN2k/wJNkXHUwMDE3p4285yW6kbBcdTAwMGZ2XHUwMDFj41x1MDAwZWLZbWHU5du3OeX5XHUwMDFjtFx1MDAwM5pcYs21pHh5nOczbVx1MDAxYyEw1jWogISTSEZy/LlcZoZ2TuGAI8pcdOEqJVx1MDAwNckxOqfCbv/UWuNcdTAwMTU6uVx1MDAxYk1cdTAwMGKD0IzuZfpccnNcdTAwMTn5QInzalx1MDAwMkJyYFx1MDAwMlVcdTAwMTlcdTAwMDNpmuTVysGJ14ZzrnrUO5k0KMT1XHUwMDBix1x1MDAxZuBgyI7ScMlQ+YxkJpbOeFx1MDAxNVxuldIuliE1XHUwMDA2IGj4PzfXz9Zt+0pq9ZBsP9u+oYPKsm+Ca/u/LJ6qzedZc2rfcMxcdTAwMWSltVwiXHUwMDFjObTk0X3mr1v6LOtAdkVcdTAwMDBpXHUwMDA3V5NcXFSh1JGglOQ2KWzT5CnMn6HeS6KYZFRcdTAwMTiI7C59s29cXKJcdTAwMWVcdTAwMTk6VLHApzNvhTf1UVx1MDAwMTiQhNg9myotJ1xuXHUwMDBlx1x1MDAxM1xubZvhQFxig1x1MDAxMXf0XHI0uL8kslx1MDAxYslcdTAwMDBQjVx1MDAxOLHqlCqSoIg80au8xVA8ZZPhZ7Jt2VptX1x0fVx1MDAxZdKy5Zc1yki7ikSaXHUwMDE2Z5zhNFx1MDAxNN/Rx5fXuydrm62V04p7UX95MK2no/05XHUwMDBmmohgjpBMqN6ym2AsXHUwMDFlNSlGXHUwMDFjXHUwMDA13O5lRf+OqjY541asrlEjhVRy9u076lx1MDAwZlx1MDAxYvewXq67bOX2qnbaofWDy4PfvfwwXHUwMDFhNE+q/FBFdnb2QVx1MDAxMqmcXHUwMDEyisjiXHUwMDAxVfoszTlcIqXMRaSgXHUwMDBlnVxuXCLT1oKTaVxmLa2QZjo1/kPrYDjVo6QxdiqXbuPrz0X4ufjvhakmMlx1MDAwNriS/kRGXFzQXHUwMDBmOEStMkszKJJcdTAwMWZi2DBcdTAwMWLqXHUwMDBlzrfcs+f7Yyit7m6elPeuyrf1kzmHXHUwMDFm1cg4rLZJXHRUXHRcdTAwMTJcdTAwMDdcdTAwMWa6Q0OQiFCMXHUwMDAzlJi5N1Q4I2Cj4Fx1MDAxOXvD/bVcdTAwMDefL61cdTAwMWZcdTAwMWRstq+6Tb3JO+V9PVx1MDAxZl7LkGhcdTAwMTOWSXktozKz78hsXHUwMDE1V2irXHUwMDBiwyZ9NOdcdTAwMWI2XGYsj8yCXHL6LD5cctjIlK5cdTAwMTApLsvG8kzy6dTLXHUwMDBmq4BcdTAwMWZzWW9F6FN2V1x1MDAwM1xmfb+7XG6FzEVbdlbKiEw3hbGxJU5miEYsuWx8XrNSXG5pIJpcdTAwMTZGXHUwMDE1XHUwMDExOtqAoVcnrLjDNZ5lXHUwMDAwljH3yzU+xFx06ShJXGIyM2X7f6mUjaE2O8mRI1wiU1x1MDAwNIPxNEvm3Fx1MDAxNcG36qhlmEF9jaJ6tI0tXHUwMDA1k1LD1LIwu5hiXHUwMDA3lnDbtimSXHUwMDEyXHRrWVxmgM1cXDGMXHUwMDA2XHUwMDAwVT7x5Vx1MDAwYuWl8olmXFwornBcdTAwMTJccqBMVEguaVIocIxdf+VS4JByopNCfabMVClTue0rqdbh/b5Ef49QPZhTXsOBMsX0XHUwMDEwW93z+dW82jejUd9Q4yjYoSe0b1x1MDAwZp7CIJlcdTAwMTnCJVx1MDAxOE3BTM7AcVxmXHUwMDA3mN1Qj26FoUFlKVEx5460+1x1MDAwNFx1MDAxNcFYWEqW4OnUNiuj3JjZXHUwMDE2XHUwMDEwXCJcdTAwMTOZZNa9sIGzffKQMFx1MDAxYZtXt/tyeKxz3ZstYY5cdTAwMTFcYitbISGYXHUwMDFk99FcZlxcPjWJXHQlXGJcdTAwMTFcdTAwMDS0XaCgXGIxkpRcdFx1MDAxY2XLqI0t39WA4n1q+5at2r2z/Uo9pHlcdTAwMWKQeYfsblx1MDAxZb2F5KF6hZ7cXHUwMDFmXFysLYlD+dgodVx1MDAwZk42Lk7rL4ejmbjpdVx1MDAxNEDn4dhcdTAwMGVcdTAwMTmcXHUwMDBiTanS8ZhcdFx1MDAwN8hRXGZcdTAwMDRcdTAwMDBnyI50dtA0rY5cdTAwMDJcdTAwMTKhoW2XlbGFTe9cdTAwMWE1VLKhc7ZVW/Jqa+Z4/9ErLZeO5OXZ9lx1MDAxNDrh5N634ZLzXHUwMDFk0Tl9MFtcdTAwMDcvXHUwMDE1v96+WSndjeG+3aPmcX1rp7y0XHUwMDEx1O9fSof+j63zsW3L1EyISMg8qeSINJlduoSxLbmH6VxunDr5c05muGF5SKfMoVNBetp+5pS2PcCR3JNxds1cdTAwMWOnXHUwMDBlhnP9sX5cdTAwMDJquqWJXHUwMDAzXFxUdj9cdTAwMDE1cpqEyuwulkTbsihFSPE8Sb7pnFfoMeIwRjFcdTAwMWVlxvbcVvE4QlDuYEBLMNzoXHUwMDE5oslVJ2JcdTAwMDTg2MpcdTAwMGWjgCkqIa2Fllx1MDAwMlx1MDAwNykos5V0XHUwMDFhf6KlRG9xXHUwMDA0Tlx1MDAxYTBOh+rqM/Y4QlxiSkfKaI57I1x1MDAxMjhUXCJZx+jbprq0MskwQjjEXHUwMDFlN4RcdG2f/ZBk7IWiiHz0xqNcYklt/Vx1MDAwZVx1MDAxN7ZcZl0xlVx1MDAxMEk7XHUwMDEyY1x1MDAxZmVcdTAwMWK5aUWJTM7GZ1xuXCKy9dq+XHUwMDEyXHUwMDFhPdYgQqvMVkVWJ1RsZ/Ug81Zb3VIrRy+Px7v3z3urm9v+9f7jjDtcdTAwMTVcclxcdeFUOVxmXGZD48VRoYyMW7de9Vx1MDAwZdo1MDaLRcRcdTAwMDStW7FcdTAwMTiCXG6NTidatDCbXHUwMDEwgtzfXHUwMDFjlLdWa+X25Vn1uHPaZJuXT7831Udzwya+XHUwMDBiXHTNWnZBXHUwMDFkKG2fVjBE5jJ9muZcdTAwMWOSnDg6XHUwMDA3krZ8ZyqQTOvAkkL2kVx1MDAxYtk1hOl0YFx1MDAxOVpcdTAwMGI/RvZ/lcWoqdfvXGZwJln1O+qj9TsmO9TWUvPeWlFh+Fx1MDAxZEO9Jsjyj4fjhzOoPnWPbneuunNcdTAwMGU/ZF9cdTAwMGUyfeRV1G5cdTAwMDeH/vpcdTAwMWS7tIaUXHUwMDEx9dDk9Medkju0JVx1MDAxMchcdTAwMTWVnnVKbf3+XHUwMDA2bpmskcf79o/d5TX329Nz4c5hXHUwMDEz9Vu2gymfuN+yj8fJ9FtcdTAwMTTokHVv6cM558DR1NHZwFx1MDAxMeDoKVx1MDAwMKdYXHUwMDAxXHUwMDBmwUBdXHUwMDE5JulUMlRDq+DHnNZsKnhcdTAwMDbY+vFX8GSXeWtkioxcdTAwMDEp3lx1MDAxZiefkc9pZsp2wLG8XHUwMDBiXHUwMDE0aIK0MN5cdTAwMDNMKoaAY1x1MDAxYc+B4Vx1MDAxYbKbzH54gdtcdTAwMTJSYXu54MdQalJcdTAwMTDIpW0mQoxcIr0nN5DEU1W17fIuh2s2Pe60lEGNXHUwMDE56jFcdTAwMDSRXHUwMDExLZSWKpxcdTAwMDOybaeEwbhcXFx1MDAwMtJdXCIjnfhcdTAwMTdcIkvJtm8oKIlRkaIjPykun2jGheJ2kLTtXGbJKfL+lDV34iiGsVx0Z0Joxpn55Fx1MDAxZHIyVdu+XHUwMDEySlx1MDAxZN7uS/T38Gl3XHUwMDE2cZ79xcD24YHUXGaxZzafXc2rbVx1MDAxM9ox9slrRCvOXHUwMDE19Fx1MDAxN+9oRzImeoVcdTAwMWaCkcl14kBcdTAwMDVcdTAwMDD7uC6tKUFcdTAwMWIlU9a/mHGQ/YHtQsqZ3Vx1MDAxY9tv26Tdi4k2epZcdTAwMWJmR6cg47ZtNn6SYG2IsrUhXHUwMDEwZmBDK8JcdTAwMWRj60okXHUwMDA2o4xSKZJcdTAwMGacLGTa8ilJXFwmxlx0t9V46E2FTjFsXHUwMDE4XHUwMDE0okhUo1x1MDAwMUT+ysTnzrlnqrV9JVx1MDAxNHpIu5ZcdTAwMTUjXHSZmWy3T3lcdTAwMTmi39fz877ZbW3uXpOT/c7SycqZ9+1ged5NXHUwMDFhU8ThvTJPaqjdnFx1MDAxZDdpgCaNgCUhhGnOJ/dQbZPymHuRiJC4ffpcdTAwMTmHKVx1MDAwNUh2q/pIzGukR2o7jvOz+ZUsXHUwMDA0db+7INNX8SNcdTAwMTMwXFyYXHUwMDE0tNpZMVLsXHUwMDBi9Vx1MDAwN0T9Qr1C7MtcdTAwMWKEXHUwMDE3K+32YYCj9m7ucD682ttXXHUwMDBmb7z44LmPy0mN+NdV72Xv2oOtxYjbczT/fPnnf+6uXHUwMDA3ViJ9 ByteEditor()Container( classes=\"top\")ByteInput()BitSwitch(0)Input( placeholder=\"bytes\")Container()Label(\"0\") Switch() BitSwitch(7)Label(\"7\") Switch() ...(1 thru 6)

Now that we have the design in place, we can implement the behavior.

"},{"location":"guide/widgets/#data-flow","title":"Data flow","text":"

We want to ensure that our widgets are re-usable, which we can do by following the guideline of \"attributes down, messages up\". This means that a widget can update a child by setting its attributes or calling its methods, but widgets should only ever send messages to their parent (or other ancestors).

Info

This pattern of only setting attributes in one direction and using messages for the opposite direction is known as uni-directional data flow.

In practice, this means that to update a child widget you get a reference to it and use it like any other Python object. Here's an example of an action that updates a child widget:

def action_set_true(self):\nself.query_one(Switch).value = 1\n

If a child needs to update a parent, it should send a message with post_message.

Here's an example of posting message:

def on_click(self):\nself.post_message(MyWidget.Change(active=True))\n

Note that attributes down and messages up means that you can't modify widgets on the same level directly. If you want to modify a sibling, you will need to send a message to the parent, and the parent would make the changes.

The following diagram illustrates this concept:

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1bbU/byFx1MDAxNv7eX4HYL3uljTtvZ14qXV1cdTAwMTHabmm7QFx1MDAwYrS3vVpVJjHEJYmzjlx1MDAwM6Wr/vd9xkDsxHFcYmkoUF1LpYk9XHUwMDFlXHUwMDFmz5znec45M/n70draenY+iNafrK1HX1phN26n4dn6b/78aZRcdTAwMGXjpI9LXCL/PkxGaStv2cmywfDJ48e9MD2JskE3bEXBaTxcdTAwMWOF3WE2asdJ0Ep6j+Ms6lxy/+P/boe96N+DpNfO0qB4SCNqx1mSXjwr6ka9qJ9cctH7//B9be3v/G/JujRqZWH/uFx1MDAxYuU35Jdcblx1MDAwM6W2bPr0dtLPreWGXHUwMDE5LomsXHUwMDFht4iHT/HALGrj8lx1MDAxMYyOiiv+1Hp7I1x1MDAxOVxmXlx1MDAxZf4uP71qXHUwMDFldk5cdTAwMWJCfNz5XFw89yjudvey8+7FWIStzigtWTXM0uQkelx1MDAxZrezztXQlc6P71x1MDAxYiZcdTAwMTiG4q40XHUwMDE5XHUwMDFkd/rR0I9cdTAwMDBcdTAwMWafTVx1MDAwNmErzs79OVa84MUwPFkrznzBN6VYwPFHSSdcdTAwMWOXjunxZd9cdTAwMDHOXHUwMDA2TFx1MDAxMmdCa+Gc4VOGbSZdTFx1MDAwNlxm+4WOZLulXG7TXHUwMDBlw9bJMezrt8dtsjTsXHUwMDBmXHUwMDA3YYopK9qdXb4yJ1x1MDAxYmjmpOKKlDFE41x1MDAxNp0oPu5kaGJN4Egz6yRcdO6kdoUxUT4rVlx1MDAxYiVcdTAwMWOZYvi8XHUwMDA1g6127iF/loet375cdTAwMWO2/qjbLYz2XHUwMDE3nk17VdmzJrwri75cdTAwMTRvUvKEdCdcdTAwMWWdb1Bz1Njdf7v59Vlj820nXlx1MDAxZrf79tvsbi9ubkafs99Pn37gpn9+4I6V2Op3pp5y9fwwTZOzRftcdTAwMWQ4+vzcbDe/dj+yNOp8lVx1MDAwM2lcdTAwMGZW0O+zt9m2ftE8dPZsZ1s8c3uvKVmFve+fvZJvXHUwMDA3XHUwMDFmtYuy5qe9/us/WKPXWazfy0/FhI9cdTAwMDbt8Fx1MDAwMrhcXFx1MDAxYsuEs0qRLlxcvVx1MDAxYvdPpn2hm7ROXG6sPypcdTAwMTlcXGGZXHQ/KFx1MDAxM4w1NH16TDDWOSGVYHZhgpntVktcdTAwMTHMNI5vkWBcZlx1MDAwYpSyhitNmoQuXtffr7hccpzj3Fx0xom70mCsml9cXNH1mFCo8IBLXHUwMDA2IW1cdTAwMTiXJTO+l0BW6YPFTCf9bC/+6lx1MDAwN1vIwFx1MDAxOKOc0lJcdTAwMTitmJto9Dzsxd3zibnLfVx1MDAxNYO1m4/Tr/8qj+gwglx1MDAxMb5XZSfab3TjY+/P6y3cXHUwMDEypVx1MDAxM66exZDmcYPDJMuSXtGgXHUwMDA1I0L0mW4tXCKRSVx1MDAxYVx1MDAxZsf9sLs/beNcXPTN1XjFLK+FoDRcbjOxOFx1MDAwMF80P785y87Tk8M/PvRenGyop6/l2d1cdTAwMDLQXFyHP2FUoJ0w0mjOlZN8UuDJWK//Vlx1MDAwYoDTcuFqXHUwMDAxyPLjblx1MDAwNVx1MDAxZfZJRCpOr1xmn8tcdPxr2j7/LyUv9ejAfvncO2Bb7tPxzyrwbz5svd9I3iE0fNVP4iMxXHUwMDE4ZsasSIitI0xp4di3JMSIXG51XHUwMDFkXHUwMDBiQJtcdTAwMDRBf1x1MDAxNmaB2ZN/v1lAQmeVM2BDKa21JZnwt2tGXHUwMDAxlFx1MDAxY3PBXHUwMDE4Mabp1kiglEDMUWEr4Gyal7Kz25NheKDmdyfDm5242/7BKnyNjE2r8JWJ3yHC4PNaXHUwMDExhi44y1x1MDAxMFx1MDAwMS5cZkDqf+2evmxcdTAwMWOIvZPPO/uHZ+HWwba551x1MDAwMNRcdTAwMDJcdTAwMTCDe8DhhFx1MDAxNFx1MDAxNVx1MDAxMTZwXHUwMDFmidRVO01GqlvD30pEWCHFdkqy1cFzOVx1MDAxNY7C072NdP9Mmv2/tt8kfGh3mns/QNV+wnSYXHUwMDE0XHUwMDE38tZVXHUwMDE42VUtXHIwqaWU7lx1MDAwNvW22dN/z2lA+3zXh+NETDNpJnhAM1x1MDAxNVx1MDAxOMRDllx1MDAxOKJcdTAwMTI9p9r2Y3TYOKRcdTAwMDZCKLUyoK/SXHUwMDA3XHUwMDFmvFx1MDAwZV+jY0vp8Fx1MDAwNfZngE9oUa/BllxmyF6oxVPh+UnMfS12W1x1MDAxNzhcco+2UivLidRcdTAwMDT8oGtcdTAwMTBH4sxoq1x1MDAxOLPThlx1MDAxNfDjVqvw8DuKUSpAXHUwMDFjYJhcdTAwMDLtcTzRVNFoRIDQXHUwMDE00TozTlx1MDAxMKeKXGZjvlx1MDAxMFCQpZtUu1x1MDAwYmG58lx1MDAxOHF55ttyoF26hjXMwjRrxv123D+eNOxyVWeRilGO69bIW9lggVx1MDAxMFx1MDAxY1x1MDAxY8KJWbJWXCKyLzU7XHUwMDBlXHUwMDA3efhcdTAwMTNoLlx1MDAxZIfN1nKjuKy8fdRvX2/V/FxietIqXHUwMDEwkZPge8hcdTAwMWLhqFx1MDAxYeVcdTAwMDKtlOKwyFx1MDAxMUlesahcdTAwMWJcdTAwMGWzzaTXizNcZv1uXHUwMDEy97PpIc7HcsMjv1x1MDAxM4Xt6at4o/K1aYpcdTAwMTj4XHUwMDFlJ6Ow4tNagaD8y/jzn7/NbN2QNmCOw3G5VVx1MDAwZTGk1OX7XHUwMDE1hoOUVYj3cZVLc21/tUjxR1x1MDAwNSNFd4/K/9+YLJWoXHJUyFxi6Vx1MDAwNWbxOGV+pLlcdTAwMWGqbCfeOVx1MDAxNudKupYqSVx1MDAwNcz4ckH+skpPZiyai0AppoEgXHUwMDAxPiUzZdgqM1x1MDAxNlxuYIxcdTAwMTKet5mRZlx1MDAwNlfCXHJcZlx1MDAxN4asYX6lUlbL+iSVsfpuqXLp+GZBqrxcdClcdJJcZrNmpDbMKWZLsLpkJcFcdTAwMDLIoCGlreRcdTAwMDaR6XJMOT/GKVx1MDAxOcWCXHUwMDFj4dxIJpnmXG6onknfXHUwMDFhyavQxLWDUfSgybLetf1RcepcdTAwMWKSW1x1MDAxZbzO4DZuarnNK1x1MDAxMIaWXHUwMDE3VYHruK3RODxcdTAwMTl0e413nz703n462uc7NnyzXHUwMDFjt01cdTAwMTc9bi9cZoRPXHUwMDA3ym92IORcYkK5qWJcZiSHXHSGLFxmLlxi4qintpZcdTAwMTOhXGKXpzZJXHUwMDAxQWl8XHUwMDFjKiGYppSAjalccnpqXHUwMDEwqXCutJLWQVx1MDAwMitxoECw6inmzrhccjKAaEtcdTAwMTdcdTAwMTO4PLdNY7HmyopRPnFtxfFQ7Vx1MDAxY/ujOrsrXHUwMDAyuWD1q57EuGWalVxue9eh/JXZ33p3tN//a3evXHUwMDExd3Y/7vY2yzi+pyjnXGahKHNcdTAwMWF6XCJcdTAwMWSjyVJcdTAwMGI5XHUwMDFl+HqTXHUwMDE2mlx05Ma3XHUwMDA3cul11GJCuJRCilx1MDAxOeFcdTAwMGLoXGJcdTAwMTk4poWk1UaUmlxcVVxckUEohqt3inFokSpM+z/Gr47aXHUwMDE59kdlbm9cYvD6XHUwMDE0xcnps+NcdTAwMTRFwFuM0IunKPOXju9pNUdqXHUwMDFiKM9kklx1MDAxY1x1MDAxM0pNXHUwMDE2c0grjDyzSmryy1x1MDAxOfVrKpE25nsyXHUwMDE0K1x1MDAwMzyFM1x1MDAwYiVcdTAwMTZOmFx1MDAxOWsqXHUwMDA2TSxcdTAwMTfqosTNbaWY44zz+UkxXHUwMDEwP2Ep5yb5XHT32039tjHpk49S4lbkXHUwMDAy0pfymN9GZ1x1MDAwNMPs28rbL5Sg3KjAhEArz4c1hEXzqk0skPA0MLVcdTAwMGaJyIxcdTAwMGJcdTAwMTJcdTAwMGYzP/HFdMF93VxuaVwiXHUwMDFj1JXvbihcdTAwMTko5vNIvKt23OrruqvFSd5dXHUwMDA1XCKrXCJKcHLtJkxcdTAwMTCHVCRcdTAwMTYve89fhbunREkqr0JcdTAwMTJcdTAwMTG3xu+PniBK5D+B4yRcdTAwMTTmXHUwMDA1woHxqGXK7y17I71Hwmu19lm8sULPqnv7QoBcdTAwMTVcdTAwMDC+c1x1MDAwNKtLw3RcdTAwMTlcZjGEyoIxW9j5XHUwMDEzXHUwMDE2c1x1MDAxNqYlsJIzoCUlXHL+Icx1pVxm44qXMMPgLtKCIVx1MDAxM1x1MDAxMXrJqvdccmo5/udcdTAwMDJKYKqM407MsIhcdTAwMDXgUWNcdTAwMTCngDyMIFMx6SExpVx1MDAxNFx1MDAwMahcdTAwMTDqIJj0xepcdGqTKlBcdTAwMDZcdTAwMTFcYsf0cKmEvK63epj4o1x1MDAwMpBVXHUwMDExJWZrzs9hpDQgS7Z4TDl/I8Q9pUptKPDcXCKl31x1MDAxZlOiwjykJFx1MDAxYvhanZXI11x1MDAxOVx1MDAxOODWQkpHgfNLQlpcdTAwMTE8h5XixTFPalx1MDAwM6+CjVx1MDAxMpE+VNNUat7cec1G9Fvc/Fx1MDAxM/LkTerLXG40o1x1MDAwNVx1MDAwNtRxSZzPiCldQIjsuEDCRuAtW2Wl1caULIBNiLBcdTAwMWPYm/zCs6pcdTAwMTbiPVU643dDglx1MDAxMrhGXvOgqbIhXHUwMDFj0mXn02XwpCPi5dtcdTAwMWKKw/MtoCfhv1baa8myUYuV/GpcdTAwMDUmN2TLur1Mtv6HPbBcdTAwMDQxp6XC7a+jyk7nTTM92n16MNJnh9REYrT//PzeUyVcdTAwMDZcdTAwMWUyZIB0+G15xfTqh4O+oCm130RcYlx1MDAwN741ruRyxi97RKVMzpGpSFx1MDAwZeYuLLm1zUxcYscs5Hupgnh5M1x1MDAxM5s4O2f3Ulx1MDAwZuaGx9Fw7dfRYPZcdTAwMWUmXrOHqVx1MDAxYlx1MDAxZE369+RcdTAwMGWmLFx1MDAxOdRtX5p4mem9SpNcdTAwMDYthTBobFx1MDAxZMSQYlvos1i8gp18jJ/2qdGmdCS2XHUwMDA2zVx1MDAxZGaH2+9cdTAwMWVcdTAwMDLCXHUwMDA09EprST66nMrbiFx1MDAwN2SsX1xi9Vx1MDAwYqeO32LeZtRCXHUwMDEw035DkrP6x0DMr1UuVY9eXG5iYZal8eEo8z7dTs7691x1MDAwMmZVoy6g9uhSLtfDwWAvw7iNg1x1MDAxNcxI3L58+aLr9dM4OmtWveKXo/zwvebw9UCJ8jjx26Nv/1x1MDAwMC2LavoifQ== Parent()Child()Child()messages (up)attributes (down)"},{"location":"guide/widgets/#messages-up","title":"Messages up","text":"

Let's extend the ByteEditor so that clicking any of the 8 BitSwitch widgets updates the decimal value. To do this we will add a custom message to BitSwitch that we catch in the ByteEditor.

byte02.pyOutput byte02.py
from __future__ import annotations\nfrom textual.app import App, ComposeResult\nfrom textual.containers import Container\nfrom textual.message import Message\nfrom textual.reactive import reactive\nfrom textual.widget import Widget\nfrom textual.widgets import Input, Label, Switch\nclass BitSwitch(Widget):\n\"\"\"A Switch with a numeric label above it.\"\"\"\nDEFAULT_CSS = \"\"\"\n    BitSwitch {\n        layout: vertical;\n        width: auto;\n        height: auto;\n    }\n    BitSwitch > Label {\n        text-align: center;\n        width: 100%;\n    }\n    \"\"\"\nclass BitChanged(Message):\n\"\"\"Sent when the 'bit' changes.\"\"\"\ndef __init__(self, bit: int, value: bool) -> None:\nsuper().__init__()\nself.bit = bit\nself.value = value\nvalue = reactive(0)  # (1)!\ndef __init__(self, bit: int) -> None:\nself.bit = bit\nsuper().__init__()\ndef compose(self) -> ComposeResult:\nyield Label(str(self.bit))\nyield Switch()\ndef on_switch_changed(self, event: Switch.Changed) -> None:  # (2)!\n\"\"\"When the switch changes, notify the parent via a message.\"\"\"\nevent.stop()  # (3)!\nself.value = event.value  # (4)!\nself.post_message(self.BitChanged(self.bit, event.value))\nclass ByteInput(Widget):\n\"\"\"A compound widget with 8 switches.\"\"\"\nDEFAULT_CSS = \"\"\"\n    ByteInput {\n        width: auto;\n        height: auto;\n        border: blank;\n        layout: horizontal;\n    }\n    ByteInput:focus-within {\n        border: heavy $secondary;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nfor bit in reversed(range(8)):\nyield BitSwitch(bit)\nclass ByteEditor(Widget):\nDEFAULT_CSS = \"\"\"\n    ByteEditor > Container {\n        height: 1fr;\n        align: center middle;\n    }\n    ByteEditor > Container.top {\n        background: $boost;\n    }\n    ByteEditor Input {\n        width: 16;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nwith Container(classes=\"top\"):\nyield Input(placeholder=\"byte\")\nwith Container():\nyield ByteInput()\ndef on_bit_switch_bit_changed(self, event: BitSwitch.BitChanged) -> None:\n\"\"\"When a switch changes, update the value.\"\"\"\nvalue = 0\nfor switch in self.query(BitSwitch):\nvalue |= switch.value << switch.bit\nself.query_one(Input).value = str(value)\nclass ByteInputApp(App):\ndef compose(self) -> ComposeResult:\nyield ByteEditor()\nif __name__ == \"__main__\":\napp = ByteInputApp()\napp.run()\n
  1. This will store the value of the \"bit\".
  2. This is sent by the builtin Switch widgets, when it changes state.
  3. Stop the event, because we don't want it to go to the parent.
  4. Store the new value of the \"bit\".

ByteInputApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a32\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u00a0\u00a0\u00a0\u00a07\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a06\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a05\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a04\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a03\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a02\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a00\u00a0\u00a0\u00a0\u00a0\u00a0\u2503 \u2503\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2503 \u2503\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u2503 \u2503\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

  • The BitSwitch widget now has an on_switch_changed method which will handle a Switch.Changed message, sent when the user clicks a switch. We use this to store the new value of the bit, and sent a new custom message, BitSwitch.BitChanged.
  • The ByteEditor widget handles the BitSwitch.Changed message by calculating the decimal value and setting it on the input.

The following is a (simplified) DOM diagram to show how the new message is processed:

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1daXPiSNL+7l9BeL/MXHUwMDFiMdbUlXVMxMaGb7vb99l4e8MhQLZlZKBBPjfmv29cdTAwMTZ2I4FcdTAwMGVcdTAwMDTGgHteOtrYkiilqvLJfDKrsvjvQqm0XHUwMDE4Pre8xT9Li95T1Vxy/FrbfVxc/N1cdTAwMWV/8NpcdTAwMWS/2cBTrPt3p3nfrnavvFx0w1bnzz/+uHPbdS9sXHUwMDA1btVzXHUwMDFl/M69XHUwMDFidML7mt90qs27P/zQu+v8y/7cc++8f7aad7Ww7UQ3WfJqfthsv97LXHUwMDBivDuvXHUwMDExdrD1f+PfpdJ/uz9j0rW9aug2rlx1MDAwM6/7ge6pmIBcdTAwMWH44OG9ZqMrrSGEK0JcdTAwMDTtXeB31vB+oVfDs1cos1x1MDAxN52pdVx1MDAxZs9cdTAwMTfl/ZPjQ7ax/0Rf/MOL5tI2j2575Vx1MDAwN8Fx+Fx1MDAxY7x2hVu9uW/HhOqE7WbdO/dr4c3PnotcdTAwMWTvfa7TxF6IPtVu3l/fNLyO7YBI0GbLrfrhsz1GSO/oay/8WYqOPOFfXHUwMDAwwpHAiFRGXHUwMDFhQ0x05+7nJXWEUExSpoXgQOSAYKvNXHUwMDAwx1x1MDAwMlx1MDAwNftcdTAwMDdcXPFaVUSiVdxq/Vx1MDAxYeVr1HrXhG230Wm5bVx1MDAxY7House3R6Y8uvWN51/fhHhQRlx1MDAwZtXxulx1MDAxZFx1MDAwZlx1MDAxYSRKZFTvhL1Ja7vW1YH/xHumUXvrmcZ9XHUwMDEwRHLZXHUwMDEz64N6XHUwMDEz150+/Vx0vadI2NhgP1x1MDAxZl3723opuPpa11+vvJ1rslx1MDAxNJ4v9q776/f0Zl8/zF42audnXHUwMDE3x0vN3X3plVx1MDAxZla/3Kq1/rv8vL/bbjdcdTAwMWZj7b79XHUwMDE2Peh9q+a+6iSVSlx1MDAxM6FcdDdMR4NcdTAwMWP4jfpgXHUwMDFmXHUwMDA0zWo9UuOFmMBcdPz0PX9cdTAwMWM6hoos6FDNmEFRiC6MnfTuXHUwMDFjXHUwMDBiO3R62FE0XHUwMDBmO5o51ExcdTAwMDU7RiehQ80gdKhcdTAwMDZcdTAwMDZcXFGYXHUwMDE4dLK0kDJKiVx1MDAwMjKKXHUwMDE2RmPdbITH/ktXkWTf0Vxy985cdTAwMGae+4arq57YPSvPobfedVxmv/1fvFx1MDAxZjtcdTAwMWXeudtcdTAwMTTv+8xy4F9bNV6s4rN47T5cclx1MDAwZn30Nb1cdTAwMGLu/Fot7j2qKIiLbba3i1x1MDAxOP1m27/2XHUwMDFibnCSJmcu8HJcdTAwMWRcdTAwMTdHdGWhj1FcdTAwMDaGoz6qwujTle3d1fKjvOzcbLeCSiB2XaPn3nNJ7lx1MDAxMK0oVcSAVMz0oY9p6aAx1Fx1MDAxYzVeXHUwMDAyZVx1MDAwND5cZn2o60U8XHUwMDE32lx1MDAwMkEkxaGZsetqy/UtVl7+tvTilqv1tc7S4flyY1xuriu3XbF3V79cclx1MDAxZaTim+et2y/N8mXdrU+g3VBcdTAwMDSbT4dXXHUwMDBmXHUwMDE1oYOV2vPm84/bJz1cdFerNChD0PhHivVBrpZrk8lSKTIhTlx1MDAwNep3YbCnXHUwMDBm/ydwtdlg50Q6MFx1MDAxZLBcdTAwMWKRxHqKq1XGXGJ0+zGv81x1MDAwYvlaPzx+9MPqzW9qur52iJtK+NqYnKV8Z/tcbv007IEhmdijVCilmCpcdTAwMWVcIuZbzznFnqTSQTZhXHKe6uKv39Ey6ShcdTAwMTDCWkJqiFx1MDAwMJ2JPdJ9jY894qBcdTAwMDWQXHUwMDFhKbVcdTAwMDEgRpuUiFx1MDAxMYgjXHUwMDE41VQrJVx1MDAwNIYgbFx1MDAxMJqGI2Y4k6NcdTAwMDSQkV/5qTHs7chfo1x1MDAwM/ZniEbHXHUwMDAxbCd02+GK36j5jet+wd5SIUVYqX1mt4XXXHSHXHUwMDEzjFx1MDAwN1xml0pII6L+7Fx1MDAxYYHqfafb6Tiw1t1cdJAouFx1MDAwNtCJZ/dcdTAwMWG14TLlo7cnk8ZBRlx1MDAwMov/0LlyXHUwMDAxXCJdJlxmuqjSXG7sf1x1MDAxY26iXHUwMDEyQlx1MDAwNW4nXFxt3t35Ifb9QdNvhIN93O3MZVx1MDAwYv1cdTAwMWLPrVxynsWHip9cdTAwMWK0XHUwMDExLdtiP1x1MDAwYot+K0VcdTAwMTDq/tH7/T+/p1+dqdn2ldDpqLmF+PtYoVx1MDAwNDbHXHUwMDA2XHUwMDBm9yxcdTAwMWNcdTAwMTJcdTAwMGJcdTAwMWNwXHUwMDE5c7fDLFxc5UTedbbWn/ZcdTAwMGVEsFX/8m19tbV1PFtcdTAwMGKnhpJcdTAwMGJcdTAwMDKOUlx1MDAwNrhcdTAwMDTgQotcdTAwMDFcdTAwMGKHps1yXHUwMDBmSz9cYmp/pJFcdTAwMTM3cKmRXHUwMDA0RMfeTFx1MDAxOGPaUFxudNY5sCevfHZ5e7N50ZKrrfrB8u7LVc2fXHUwMDAy4Z9cdTAwMTdijujIzoFcdTAwMTmUwlx1MDAxMCOL58DSu3POoYPYYFx1MDAxMXRcdTAwMDaAw2FKwIk501x1MDAxY1ZutFx1MDAxMFIjaZtcdTAwMWEpXHUwMDFmy8ePRcp33IpcdTAwMTf89n1RfV9EsjtNVj7E4lx1MDAwZrLyfkHf4bdoXHUwMDBlM1x1MDAwNyqBXGJcdTAwMTGLm4eBr3a6t7dzclY7P0TQ3Vx1MDAxY79UV59cdTAwMGXUnINPcqTehHGMM20kovvzz1x1MDAwMmk7kUxQJJyKXGIjs4PiKfktqlx1MDAwNMbnJJ7OmI3jkpvkaUM9fttvXHUwMDFmeU+rXHUwMDE3661V9+jub5RRXHUwMDEyLCejhOacauQ8xTNK6d0559hcdTAwMDHlsGzscDot7EiThE7SdXHKXGaGXHUwMDA1ZnpzN9NzXW9Jmim7rSFcdTAwMDZ/0G1FQuZcIi4zkcQgliZcdTAwMWGAnNboy/goUVY+eZ7XPFx1MDAxMoZZVDFUY2lcdTAwMTBYnPZBjlx1MDAwMzhcdTAwMWPhyKnmiEnGP1xmcYDYNpJcdTAwMTjOXGbjUsTuXHUwMDE0+S7pUINcdTAwMDNiU+tKMMVcdTAwMDfxSFx1MDAxObOeN25cdTAwMTimm0ZcdTAwMWHLWcS6tFBcdTAwMWGpcMpcdTAwMDZcdTAwMDNSMFJcdTAwMDB6d8K0jl3xM2WzZONcdTAwMDTCKJGK2excdTAwMWJRSiZcdTAwMWW+UFx1MDAxZSmfb8aEXHUwMDAywOFlaDhcdTAwMTUnPDJH/UJcdE1cdTAwMTWnwIBcdTAwMThhM2BcdJk+U1x1MDAxYWkpU7XtK6HUUXNcdTAwMGLx99FtXHUwMDFidlxcpm0jXGJ5SiFS02G2LZ9fzatt48xBViVcdTAwMTRcdTAwMWE3XG5aRqHuq20zXHUwMDBl2jeBJyQ3mis1INhkjVx1MDAxYva3wpDbKIRaynSVQLyitlM0XHUwMDFlXHUwMDA2JY5f8zPBRJTQXHUwMDA0Ufr/1s2aeodcdTAwMTMp0WppJVx1MDAxNGozxC6JMtJcdTAwMWOQMnNjKHInKWQyXHUwMDFmXci45bOSmHFcdTAwMTNGXHUwMDAxt/8p1TLVuKFInFx1MDAxYqIpJ8SumDNJe/uZbFumYttXUqVHtG25qVx1MDAwNqpoJndD5ibRmlIovtqGtOvbX7dcdTAwMGWDXHUwMDEzctmRrdN6eHrS+jL3XHUwMDA2zlx1MDAxOIcx27WMaWl0xO67k4CGOVRqVDc8XHUwMDBm6G6z7VtFMVxyufbt7Vx1MDAxYSGS1o2ylGhcdFx1MDAxMvZcdTAwMGJVRFxuytHQvst+0eH2q/eZkfJcdTAwMGbaf1bNaq3R2Vx1MDAxNPvqWMOB6bxcdTAwMTROnMPJ9cph58vt3XVjjSw/XHR2tr4rP1f+gVwiWcxcdTAwMDJcdTAwMTRGQtTOXHUwMDAwysJ4Su/NOceTXCJcIlx1MDAxN096cnjKT92RlPCHJ/JcdTAwMGZMXCKbVFx1MDAwMqa3noWNoITRWMfyXHUwMDBm1EHDzImWTKBKkdhUTV8+oj+9sNhcdTAwMGL1ndVcdTAwMWJcdTAwMWM5r/bb94Y9++BcdTAwMDb33j9P2vfe90b6qlx1MDAxNy77WurlIVx1MDAwMu+qXHUwMDFmXHUwMDA0I6UphjiL9DRFruzjcXyusyk+XGJcdTAwMGV8hEKJfFx1MDAwYjZcdTAwMTnA1tzOjTdRxGqGPk5TXHUwMDEwXHUwMDEyQCPp6l+CJoh0XHUwMDE05UA018hR9MchXHUwMDE2hIO3oFx1MDAxOHNR5OiKcEhcdTAwMDJcdTAwMThcdTAwMTRyViosYUJahOBcdTAwMWTEM0ib1Fx1MDAwND5cbp4nTvC54eojXHR++NU/OHKXbzfPt063j2twpra93TiZjqgyM2huKTfUXHUwMDE4ZVikyz3GXHJcdTAwMGXaarxCXHUwMDAypZxRNuZcIph8OPeLpDhcdTAwMTeW7uJQXHUwMDAz4TIhXHUwMDEyd5jgikpk91xmLTP/7LmLTL1+PT2o0pNk+JzFqOLgenpAXHUwMDFlaVx1MDAwN7+wfSt7QeXg7GqLPD7Cycry/ZHoXFzuz/1yeux/ooFcYlx1MDAwNmBcZiH9XHUwMDE5XGbQxulcdTAwMDaTimMsqSE7gfH+1fSRsYrsWYKQaHTsXFxyMevF9GflzfJcdTAwMTKtrHjV1epcdTAwMGZf3vDypbgqSuXp4cF6cNsgq/7h3kZwtXfyrHaCidSBWVx1MDAxNtWN/j+aynNmMpFDQWpcdTAwMDPoXHUwMDE0WWHopHfnnHP510qUXGI6/VxcXlx1MDAxMuOg3ZpcdTAwMDJ0XG6WgVx1MDAxMYw7qITY/MtHcvlRtTCVy89/XHUwMDFk2Fx1MDAxMJv/QXVggtHMRTCAPotJw4tcdTAwMDfSO/ri6kbpx1v/oHy+fPz88Lgmzubeb0nmXHUwMDEwZsBcdTAwMTBKpCZU94FcdTAwMGa7XHUwMDAw/Vx1MDAxNmBcdTAwMTQtKFKKWFx1MDAxMDOrKjAlJGKPTbAyZDzH1VDrbnAsXq7Kl1qrs8ZdsIy+5+NcdTAwMWRXbruHpzWobZRltVx1MDAxNXYuqD5mlaO9/Vx0tNtoP/JNODva6ZgnODhZXHRX7vjW53K0gsVcdTAwMTZ/JCpRrOtcdTAwMTkpaZY+/PPuaFx1MDAxNcnDumCOnlxu1otcdTAwMTWBcTvNSmBKa3am7GdnVVx1MDAwMzbER31EXHKYkCxz+sdmgjhBT1tcdTAwMTh4+aZzToEnKXGEsrVCXGZcdTAwMTksVf25L1x0zFx1MDAwMZDKXHUwMDA2yoxzPSjX5Ga3hWNX7DFcdTAwMDWKo1x0gFx1MDAxNFx1MDAxOErqoFx1MDAxOVSaaWWUMDpJf1FAjpxcdTAwMWNmVlx1MDAwMNZFq2CxXGJ78qmvfDpaXHUwMDFhLO/SXHUwMDFjiNLG1lGp+Oqdn2ViXHUwMDE4Llx1MDAxOIGBg1x1MDAwMmCCQ+LZXHUwMDBi5b7yoVvKLO/CuybzcdpB6ElDqVx1MDAxNFx1MDAwNohmn3tyO0uv7Suh0VFjXHUwMDBi8ffxXHUwMDAyXGIhs6e2hUShsG8jRVx1MDAxZGbc7k9b6ujBe3jY2Hrcba1cdLPjXHUwMDFkzdi4XHUwMDE1qP7iaNuwbzU3nLBcdTAwMDFSgdbOXHUwMDAxQrStuELkfmB1a9FF9MxWwaNGzDqA8I8qa1crtdtccvdCfDup1Fx1MDAwM1J396dA9OeGkEM2dFBPXHUwMDEwWECL04L03pxz5FDlmGzkSDkl5Fx1MDAxNKv+UoxcdTAwMDDGSXJyXHUwMDE54/mh47Or/lx1MDAxYWLxP6z6XHUwMDBiVOZcblx1MDAxMqQo1Fx1MDAxOGGKXHUwMDA3w99u7veO/fLVKWeN3VbraYtvm4c5XHUwMDA3XHUwMDFm+mZHcIyDpTJMx6ffX91cdTAwMTaSKGq3YEPk0fhsznTcVjLvhUxcdTAwMDOMgZnvfnS6slG5fdg/uEJd9Y/LnFc3Ksd/pzySjFx1MDAxOH5yMaOmwG1qpTB00rtzzqFcdTAwMDPaMdnQkeDAVKBTrPSLMqa7ZWi/4nzNbGq/htj7Sdd+cWmyIVx1MDAwN5JQu1a1eJCVz53nNYNEmMNcdTAwMTlGuJJcdTAwMTDDgffvXHUwMDE0oFxmdaRcdTAwMTZcdTAwMTjjaoVRp1x1MDAxOZw+mmBcdTAwMDbJOCCMUZJwgy5Jpm0hhFx1MDAwMbe0Y4ZgQHljXHUwMDBiP3r7b0hQo/ixT5c+KpyqsWVd0ih0XHUwMDFjSPPRoEZ7kpSyqsN44tFcdTAwMGJlj/KJZim7qFx1MDAwYjiFXHUwMDE0ofqqw0RCps+UPVrKVGr7Sqhz1NxC/H10s2ZU5s5BjFFcdTAwMWPzUTh4PrWaV6vGucPRbFx1MDAxMSYpKvfAKm5lwDFCITaowivIx+XFke1TxolmVlx1MDAxOMVT4mFcdTAwMDFcdTAwMGW39Y1GIErBJEsmUH2UlCNFx7+uWXst51x1MDAxMnainiB2XGKw5PJLaic9bEZcXOFVaEjUmCVf+WSklF3QlVx1MDAxNEk7SGmFIYBcdTAwMDZQXHUwMDEzwz73itAspbavQXVcdTAwMWXRpuV/K1x1MDAwMIPsJW2ca6kkXHUwMDFiIUI65N5cdTAwMGVpiy1cdTAwMTnenZZbT5VvlVx1MDAxM3ExW8Mmhld7Scfmy4TQXHUwMDAwXHUwMDE4gPTvjqFcdTAwMTh3hMUqdoPdXHUwMDA0M5utuZJ6opZr1/7hSWE3i0+r9lx1MDAxMilcdTAwMGLaIEHHMEzjwtD3bun4YdVe9y7dXGb8h/37L/5hsKb3g43640rRjFx1MDAwM/9xcv+1/nxSP/56015fuffPrlduP1fGgdHsxdVUgFGCY1RQXHUwMDE4T+ndOd94wuA9XHUwMDBmT5xOXHUwMDBlT/nJurTNLpLVXpTYRJ31NCMgasYph7HLvXpLROak4muIv8hcXOEyftFXvjfkMnPdXHUwMDE59jVcdTAwMTOKj7DHobtz0Vxcalxcr9HTXHUwMDAzXb/wNm+bZ8/e3LN8Y3fO0EaBVlQgw+pDL1x1MDAxOEQvVcyWR1x1MDAxYVx1MDAxZV/sPlx1MDAxYm8ojWGESP2+vVx1MDAxYj7MXHUwMDFidiqHoVe+8E7MQV2dtFdum5Xdtb+TN2RcInu/XfRcdTAwMTNcdTAwMTKdoSjuXHLTu3POXHUwMDAxZd1hXHUwMDFloMzkXHUwMDAwNVx0d8hcYjVcdTAwMTQ0+URcdTAwMGI5f1x1MDAxOW84xGF8gDfMzHdRqTPT+Fx1MDAwNlx1MDAxOLWhd/F8V74pm9dcImgllUOUNFx1MDAxYdWKoaPpX1xiqqh0tOXz1NhccnNIzjZH7922wFx1MDAxMVozSbjmWktqTErRoOSv+11cdJTHriZMbuJmP6pcdTAwMTSwWVVBTyXplc8nS32Jc8LRJNtcdEfVXVwiK2M5l7dcdTAwMTSTdIBLpYjm3e9bgTHXgubDutS3XHUwMDE2XHUwMDE0PVx1MDAwMZPC1lxc21R+MuslXHUwMDFjprlB3VVgR1MlJ1x1MDAxOD5T1mspW7W7p1x1MDAxM1pcdTAwMWQ1uFx1MDAxMH9cdTAwMWZzp6PY5PlgSlx1MDAxZs2fVYziu1Tm19/PhJvAcFx1MDAwM8fsvrCCXCI/wchnoJZTULtcdTAwMDeE5Izh8NiKz5x9Yaex0Vx1MDAxMVxiYlBcdTAwMGJgXrn+16PDcufMO79i4erezsHDt6WD2nzsczTq3lx1MDAxM2NxfVtlm8n1XHUwMDE5t7KoXHUwMDExguf07px3PKlcXDzpXHTiaVx1MDAxMlx1MDAxYlx1MDAxZFEgqFx1MDAxZDJe8vPRO1x1MDAxZI3l/H+9nY6GeItJ73SUidlcdTAwMWNcdTAwMGaImJVSk1x1MDAxMVxuXCI6wU1l47D98uOyeaafVn/slDfXZuxcdTAwMDJcdTAwMGKUWVxu4oDgXHUwMDE4fVx1MDAxYrtiU/av61x1MDAxNkY51FZZaqT5hKpY0elkfSCPfcFNXHUwMDBmsbEvOowqqi3t0yNtVjo+Ylx1MDAwMVx1MDAxOYJ6L2JcdTAwMGKvj1t2Sq+KXuogXHTtlPq1vnSHT+Nee6kojX0/8ygoXHKbrSyI9j3lIFx1MDAxZVx1MDAwYko6XHUwMDE2JmVOkZKynnyETVx1MDAwZfjT5Y/VJ7qp1lx1MDAxZlr8eV08Xla/VudcdTAwMWWRhDukXHUwMDFidFx1MDAxM40/419waVx1MDAxYjDMfoFcdTAwMTVgPIbRpCTZlVx1MDAxNkUy0NmAjH+bXrSwJPn1VGg60EqS6UxcdTAwMWVJSsW7PWjxumen1Esxldpep9W0ul557io9hrylRFx1MDAwMup7I2yW/LBTXHUwMDFhJCZxhzpdqL7/IV5RvPBGsVx1MDAxN91W6zjEcelFXHUwMDE3OOJ+7a1zI1FcdTAwMTZcdTAwMWZ873ElReWuui/batcyWFx1MDAxMHrdUOWvhb/+XHUwMDA3KtdcdTAwMDdDIn0= ByteEditor()BitSwitch(7)Label(\"7\") Switch() Switch.Changed( value=True)ByteEditor()BitSwitch(7)Label(\"7\") Switch() BitSwitch.Changed( value=True)BitSwitch.Changed( value=True)Switch.Changed( value=True)A. Switch sends Switch.Changed messageB. BitSwitch responds by sending BitSwitch.Changedto its parent"},{"location":"guide/widgets/#attributes-down","title":"Attributes down","text":"

We also want the switches to update if the user edits the decimal value.

Since the switches are children of ByteEditor we can update them by setting their attributes directly. This is an example of \"attributes down\".

byte03.pyOutput byte03.py
from __future__ import annotations\nfrom textual.app import App, ComposeResult\nfrom textual.containers import Container\nfrom textual.geometry import clamp\nfrom textual.message import Message\nfrom textual.reactive import reactive\nfrom textual.widget import Widget\nfrom textual.widgets import Input, Label, Switch\nclass BitSwitch(Widget):\n\"\"\"A Switch with a numeric label above it.\"\"\"\nDEFAULT_CSS = \"\"\"\n    BitSwitch {\n        layout: vertical;\n        width: auto;\n        height: auto;\n    }\n    BitSwitch > Label {\n        text-align: center;\n        width: 100%;\n    }\n    \"\"\"\nclass BitChanged(Message):\n\"\"\"Sent when the 'bit' changes.\"\"\"\ndef __init__(self, bit: int, value: bool) -> None:\nsuper().__init__()\nself.bit = bit\nself.value = value\nvalue = reactive(0)\ndef __init__(self, bit: int) -> None:\nself.bit = bit\nsuper().__init__()\ndef compose(self) -> ComposeResult:\nyield Label(str(self.bit))\nyield Switch()\ndef watch_value(self, value: bool) -> None:  # (1)!\n\"\"\"When the value changes we want to set the switch accordingly.\"\"\"\nself.query_one(Switch).value = value\ndef on_switch_changed(self, event: Switch.Changed) -> None:\n\"\"\"When the switch changes, notify the parent via a message.\"\"\"\nevent.stop()\nself.value = event.value\nself.post_message(self.BitChanged(self.bit, event.value))\nclass ByteInput(Widget):\n\"\"\"A compound widget with 8 switches.\"\"\"\nDEFAULT_CSS = \"\"\"\n    ByteInput {\n        width: auto;\n        height: auto;\n        border: blank;\n        layout: horizontal;\n    }\n    ByteInput:focus-within {\n        border: heavy $secondary;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nfor bit in reversed(range(8)):\nyield BitSwitch(bit)\nclass ByteEditor(Widget):\nDEFAULT_CSS = \"\"\"\n    ByteEditor > Container {\n        height: 1fr;\n        align: center middle;\n    }\n    ByteEditor > Container.top {\n        background: $boost;\n    }\n    ByteEditor Input {\n        width: 16;\n    }\n    \"\"\"\nvalue = reactive(0)\ndef validate_value(self, value: int) -> int:  # (2)!\n\"\"\"Ensure value is between 0 and 255.\"\"\"\nreturn clamp(value, 0, 255)\ndef compose(self) -> ComposeResult:\nwith Container(classes=\"top\"):\nyield Input(placeholder=\"byte\")\nwith Container():\nyield ByteInput()\ndef on_bit_switch_bit_changed(self, event: BitSwitch.BitChanged) -> None:\n\"\"\"When a switch changes, update the value.\"\"\"\nvalue = 0\nfor switch in self.query(BitSwitch):\nvalue |= switch.value << switch.bit\nself.query_one(Input).value = str(value)\ndef on_input_changed(self, event: Input.Changed) -> None:  # (3)!\n\"\"\"When the text changes, set the value of the byte.\"\"\"\ntry:\nself.value = int(event.value or \"0\")\nexcept ValueError:\npass\ndef watch_value(self, value: int) -> None:  # (4)!\n\"\"\"When self.value changes, update switches.\"\"\"\nfor switch in self.query(BitSwitch):\nwith switch.prevent(BitSwitch.BitChanged):  # (5)!\nswitch.value = bool(value & (1 << switch.bit))  # (6)!\nclass ByteInputApp(App):\ndef compose(self) -> ComposeResult:\nyield ByteEditor()\nif __name__ == \"__main__\":\napp = ByteInputApp()\napp.run()\n
  1. When the BitSwitch's value changed, we want to update the builtin Switch to match.
  2. Ensure the value is in a the range of a byte.
  3. Handle the Input.Changed event when the user modified the value in the input.
  4. When the ByteEditor value changes, update all the switches to match.
  5. Prevent the BitChanged message from being sent.
  6. Because switch is a child, we can set its attributes directly.

ByteInputApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a100\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u00a0\u00a0\u00a0\u00a07\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a06\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a05\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a04\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a03\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a02\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a00\u00a0\u00a0\u00a0\u00a0\u00a0 \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

  • When the user edits the input, the Input widget sends a Changed event, which we handle with on_input_changed by setting self.value, which is a reactive value we added to ByteEditor.
  • If the value has changed, Textual will call watch_value which sets the value of each of the eight switches. Because we are working with children of the ByteEditor, we can set attributes directly without going via a message.
"},{"location":"guide/workers/","title":"Workers","text":"

In this chapter we will explore the topic of concurrency and how to use Textual's Worker API to make it easier.

The Worker API was added in version 0.18.0

"},{"location":"guide/workers/#concurrency","title":"Concurrency","text":"

There are many interesting uses for Textual which require reading data from an internet service. When an app requests data from the network it is important that it doesn't prevent the user interface from updating. In other words, the requests should be concurrent (happen at the same time) as the UI updates.

Managing this concurrency is a tricky topic, in any language or framework. Even for experienced developers, there are gotchas which could make your app lock up or behave oddly. Textual's Worker API makes concurrency far less error prone and easier to reason about.

"},{"location":"guide/workers/#workers_1","title":"Workers","text":"

Before we go into detail, let's see an example that demonstrates a common pitfall for apps that make network requests.

The following app uses httpx to get the current weather for any given city, by making a request to wttr.in.

weather01.pyweather.tcssOutput weather01.py
import httpx\nfrom rich.text import Text\nfrom textual.app import App, ComposeResult\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Input, Static\nclass WeatherApp(App):\n\"\"\"App to display the current weather.\"\"\"\nCSS_PATH = \"weather.tcss\"\ndef compose(self) -> ComposeResult:\nyield Input(placeholder=\"Enter a City\")\nwith VerticalScroll(id=\"weather-container\"):\nyield Static(id=\"weather\")\nasync def on_input_changed(self, message: Input.Changed) -> None:\n\"\"\"Called when the input changes\"\"\"\nawait self.update_weather(message.value)\nasync def update_weather(self, city: str) -> None:\n\"\"\"Update the weather for the given city.\"\"\"\nweather_widget = self.query_one(\"#weather\", Static)\nif city:\n# Query the network API\nurl = f\"https://wttr.in/{city}\"\nasync with httpx.AsyncClient() as client:\nresponse = await client.get(url)\nweather = Text.from_ansi(response.text)\nweather_widget.update(weather)\nelse:\n# No city, so just blank out the weather\nweather_widget.update(\"\")\nif __name__ == \"__main__\":\napp = WeatherApp()\napp.run()\n
weather.tcss
Input {\ndock: top;\nwidth: 100%;\n}\n#weather-container {\nwidth: 100%;\nheight: 1fr;\nalign: center middle;\noverflow: auto;\n}\n#weather {\nwidth: auto;\nheight: auto;\n}\n

WeatherApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aEnter\u00a0a\u00a0City\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

If you were to run this app, you should see weather information update as you type. But you may find that the input is not as responsive as usual, with a noticeable delay between pressing a key and seeing it echoed in screen. This is because we are making a request to the weather API within a message handler, and the app will not be able to process other messages until the request has completed (which may be anything from a few hundred milliseconds to several seconds later).

To resolve this we can use the run_worker method which runs the update_weather coroutine (async def function) in the background. Here's the code:

weather02.py
import httpx\nfrom rich.text import Text\nfrom textual.app import App, ComposeResult\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Input, Static\nclass WeatherApp(App):\n\"\"\"App to display the current weather.\"\"\"\nCSS_PATH = \"weather.tcss\"\ndef compose(self) -> ComposeResult:\nyield Input(placeholder=\"Enter a City\")\nwith VerticalScroll(id=\"weather-container\"):\nyield Static(id=\"weather\")\nasync def on_input_changed(self, message: Input.Changed) -> None:\n\"\"\"Called when the input changes\"\"\"\nself.run_worker(self.update_weather(message.value), exclusive=True)\nasync def update_weather(self, city: str) -> None:\n\"\"\"Update the weather for the given city.\"\"\"\nweather_widget = self.query_one(\"#weather\", Static)\nif city:\n# Query the network API\nurl = f\"https://wttr.in/{city}\"\nasync with httpx.AsyncClient() as client:\nresponse = await client.get(url)\nweather = Text.from_ansi(response.text)\nweather_widget.update(weather)\nelse:\n# No city, so just blank out the weather\nweather_widget.update(\"\")\nif __name__ == \"__main__\":\napp = WeatherApp()\napp.run()\n

This one line change will make typing as responsive as you would expect from any app.

The run_worker method schedules a new worker to run update_weather, and returns a Worker object. This happens almost immediately, so it won't prevent other messages from being processed. The update_weather function is now running concurrently, and will finish a second or two later.

Tip

The Worker object has a few useful methods on it, but you can often ignore it as we did in weather02.py.

The call to run_worker also sets exclusive=True which solves an additional problem with concurrent network requests: when pulling data from the network, there is no guarantee that you will receive the responses in the same order as the requests. For instance, if you start typing \"Paris\", you may get the response for \"Pari\" after the response for \"Paris\", which could show the wrong weather information. The exclusive flag tells Textual to cancel all previous workers before starting the new one.

"},{"location":"guide/workers/#work-decorator","title":"Work decorator","text":"

An alternative to calling run_worker manually is the work decorator, which automatically generates a worker from the decorated method.

Let's use this decorator in our weather app:

weather03.py
import httpx\nfrom rich.text import Text\nfrom textual import work\nfrom textual.app import App, ComposeResult\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Input, Static\nclass WeatherApp(App):\n\"\"\"App to display the current weather.\"\"\"\nCSS_PATH = \"weather.tcss\"\ndef compose(self) -> ComposeResult:\nyield Input(placeholder=\"Enter a City\")\nwith VerticalScroll(id=\"weather-container\"):\nyield Static(id=\"weather\")\nasync def on_input_changed(self, message: Input.Changed) -> None:\n\"\"\"Called when the input changes\"\"\"\nself.update_weather(message.value)\n@work(exclusive=True)\nasync def update_weather(self, city: str) -> None:\n\"\"\"Update the weather for the given city.\"\"\"\nweather_widget = self.query_one(\"#weather\", Static)\nif city:\n# Query the network API\nurl = f\"https://wttr.in/{city}\"\nasync with httpx.AsyncClient() as client:\nresponse = await client.get(url)\nweather = Text.from_ansi(response.text)\nweather_widget.update(weather)\nelse:\n# No city, so just blank out the weather\nweather_widget.update(\"\")\nif __name__ == \"__main__\":\napp = WeatherApp()\napp.run()\n

The addition of @work(exclusive=True) converts the update_weather coroutine into a regular function which when called will create and start a worker. Note that even though update_weather is an async def function, the decorator means that we don't need to use the await keyword when calling it.

Tip

The decorator takes the same arguments as run_worker.

"},{"location":"guide/workers/#worker-return-values","title":"Worker return values","text":"

When you run a worker, the return value of the function won't be available until the work has completed. You can check the return value of a worker with the worker.result attribute which will initially be None, but will be replaced with the return value of the function when it completes.

If you need the return value you can call worker.wait which is a coroutine that will wait for the work to complete. But note that if you do this in a message handler it will also prevent the widget from updating until the worker returns. Often a better approach is to handle worker events which will notify your app when a worker completes, and the return value is available without waiting.

"},{"location":"guide/workers/#cancelling-workers","title":"Cancelling workers","text":"

You can cancel a worker at any time before it is finished by calling Worker.cancel. This will raise a CancelledError within the coroutine, and should cause it to exit prematurely.

"},{"location":"guide/workers/#worker-errors","title":"Worker errors","text":"

The default behavior when a worker encounters an exception is to exit the app and display the traceback in the terminal. You can also create workers which will not immediately exit on exception, by setting exit_on_error=False on the call to run_worker or the @work decorator.

"},{"location":"guide/workers/#worker-lifetime","title":"Worker lifetime","text":"

Workers are managed by a single WorkerManager instance, which you can access via app.workers. This is a container-like object which you iterate over to see your active workers.

Workers are tied to the DOM node (widget, screen, or app) where they are created. This means that if you remove the widget or pop the screen where they are created, then the tasks will be cleaned up automatically. Similarly if you exit the app, any running tasks will be cancelled.

Worker objects have a state attribute which will contain a WorkerState enumeration that indicates what the worker is doing at any given time. The state attribute will contain one of the following values:

Value Description PENDING The worker was created, but not yet started. RUNNING The worker is currently running. CANCELLED The worker was cancelled and is no longer running. ERROR The worker raised an exception, and worker.error will contain the exception. SUCCESS The worker completed successful, and worker.result will contain the return value.

Workers start with a PENDING state, then go to RUNNING. From there, they will go to CANCELLED, ERROR or SUCCESS.

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nOVbaVPbSlx1MDAxNv2eX8EwX2aqQqf3JVVTU2BcdTAwMWN2YzDLkFevUsKWjWLZciSZ7VX++7uSwdq8gnFMxkmxqFvS7dv33HNud/PXh7W19fChZ69/Xlu37+uW6zR86279Y3T91vZcdTAwMDPH60JcdTAwMTONf1x1MDAwZry+X4973oRhL/j86VPH8tt22HOtuo1unaBvuUHYbzhcdTAwMWWqe51PTmh3gv9GXytWx/5Pz+s0Qlx1MDAxZiUv2bBcdTAwMWJO6PmDd9mu3bG7YVx1MDAwME//XHUwMDAzfl9b+yv+mrLOt+uh1W25dnxD3JRcdTAwMTjIhM5frXjd2FiKXHUwMDE10YRiTIY9nGBcdTAwMWLeXHUwMDE32lxyaG6CzXbSXHUwMDEyXVq3znrXZLtd3dpUXHUwMDBm21xyfSrtRilIXtt0XFy3XHUwMDE2PrhcdTAwMDNXWPWbvp8yKlxifa9tXzqN8Fx1MDAwNtpJ7vrwvoZcdTAwMTXcgFx1MDAwMcNm3+u3brp2XHUwMDEwZG7yelbdXHRcdTAwMWbgmsTDi1x1MDAwMy98Xkuu3MNvgjDEXHUwMDE1JpozKolcImbYXHUwMDFh3U6MRsxAo1CYXHUwMDExKnJmlTxcdTAwMTdmXHUwMDAyzPonjj+JXddWvd1cdTAwMDLjuo2kXHUwMDBmXHUwMDExlnXdVCrpdfc8XFylkZGaYENcdTAwMTjWwrBkWm5sp3VcdTAwMTNCXHUwMDFmRZDWXFxJIdNm2PFsXHUwMDEww6lcdTAwMTBaXHUwMDEwOWyJXt7ba8SR8WfaXd3Gk7ueQyVcdFx1MDAxNvZ05WcyjKh/OVx1MDAxZmTpQMtcdTAwMDRbaN+Hw9GlXCKDXHUwMDFlYm653vbRySU+7SmveVx1MDAxZPh368N+Pz+Ofuzg5tbX3bPbg4rg2K0+bjaCutNcdTAwMGV19i3P77d830s/9+mnZPz9XsNcdTAwMWFcdTAwMDQwkVx1MDAxYVx1MDAwYmaEMJypYbvrdNvQ2O27bnLNq7eTmP+QMrhcdTAwMDC2zPjTiYCbcTgjlElCXHKZXHUwMDFkZqOduViYXHUwMDA1XHUwMDFlJJtFokxwxFx1MDAwNadMa0MkTsVpdDulXHUwMDE0acYphVxizob3/ChcdTAwMGJ9q1x1MDAxYvQsXHUwMDFmXHUwMDAytogzo4q4oiyPJsNcZotiY1x1MDAxZTBlXCKmgJpFXHUwMDA2YDLRXjesOY+DZJ25+sXqOO5DZq7iyFx1MDAwNPdUy5XtvcpO2oWBXHIvjUNRZbpvuk4rXG7e9TpcZsP2M3FcdTAwMWQ6QEfDXHUwMDBlXHUwMDFkp9FIXHUwMDEzTFx1MDAxZGyw4Jn+3iy84PlOy+la7lnOxIlIm0hrVLPxcMOGXG6qUi6fhrcq/966XGKCbvussnlMSuLhuH9cdTAwMTj+WryZ6aymXHUwMDExQFx04lx1MDAwYuBmXGalXHUwMDE5vHFsXHUwMDEwIJFTpVx1MDAxOFx1MDAwNN/rWFxyUHttyzdhNcKYoVLNXHUwMDA1xCWy2rfqZVmwb3T/ZrfU8qonrTPaO1pcdTAwMDKrTXxuXHUwMDE4XHUwMDFjXHUwMDE4uSVlu3K811xid75x9ztcclx1MDAxN/Dcsz0lws7Z8Vx1MDAxN1BBzdOr+uFV6bv1vliYXHUwMDE4OjYtQHxqRjgzM6eF0bO/4mlBYkQ4XHUwMDA2b3NNOc2JXc5cdTAwMThcdTAwMTJcdTAwMWFcdTAwMDNgXHUwMDA1l/p1aWEyXHKLYlwiKNKwXHUwMDAyNc5cck9N2u9Dw6fnlcqyaXhcbo/lafjZxJfTME/JrVx1MDAxY95cdTAwMDRcdTAwMTPGXGLK2Mxw278qNbulSvXm6urkXFxaTJ5WW1x1MDAwZr9cdTAwMTZuXHUwMDA0T8NcdTAwMWI1XG5cdTAwMTjQ4NE0XGakh0BcZnNJoHpTUGy/ioeb1jXGYvEsrOHNUFx1MDAxZq8qXHTLi3ur6e5cXDx6m1++063+5Vx1MDAxMS6fLoEsV4XUOFx1MDAxZLuEXHUwMDAzYWUwwXNw2mhnrjjIXHUwMDE4VVBbMjya1GAukMRU86j4pK9cdTAwMDTZRFYjqfQ/gdaI0Vx1MDAwNv5cdTAwMTP2O/JaabNSKlx1MDAxZlx1MDAxZZa3l8psU7ghz2yJkS/nNkHEONhRoYWSxMzObf2gVz06bzfZ0ebBl1p5V5VZzVl12Fx1MDAwMXsjQoFcdTAwMWSE0YRcdTAwMDNcdTAwMDBz3CZcdTAwMDCUUlGJXHUwMDE1YFLr18BcdTAwMGW4TURF+1vUmIxSRoRKXHJ3pehtyznY3WnKq9ZuhXnNvc6J7LR+zEpvj03F5f79t83H8I5cXO79aFx1MDAwNY1cdTAwMGLzzuiN87FcdTAwMWFSSC4wobOv5Ix25qrjTClEgFtkviqLYaZcZpLcQPmqtVSvhNlEdlOkXGKsUeRmKMSHTs3a70Nu5dPT49OlXHUwMDEy21x1MDAxNGLIXHUwMDEz28DAl5Oa5HJcdTAwMWPYiIZMbrjWc+xT6KteKdxix51wrytkTe5vXf7iXHUwMDA1kuloU1x1MDAwMiNKXHUwMDE0ZYYpo3VKXFxcdTAwMGZYTSHMXHUwMDE0V9Fmm3ntyinHdSzeZjfQRHeLXHUwMDE1Ldno0f1t7+7r8WG5XSv/j11cXJrKJf8/WodcdTAwMTSpdbpcdTAwMDLMXGKIJVx1MDAwZVbMXHUwMDBls5HeXFx1mFx1MDAxOYZcdTAwMTRwt2FcdTAwMDaUY3pjfVCzXHUwMDExRKmJlmSNfu1cdTAwMDbF5JVIWoTWXGJWXHUwMDAzuGtcIomeRyS+XHUwMDE3Vqudl0rlWm2pvDaFXHUwMDFh8rz2bOJEtFxy0D5cdTAwMDJuTI9dIYlyK+irOUq1yds2v1x1MDAwNG16XHUwMDFh2CRmQFx1MDAxNlx1MDAxMMRcbpiBM5JcdTAwMDVcdTAwMWKUcIjA1Vx1MDAxMXSy2EpcckF0S0O0UlxmftIjkFx1MDAwNypcdTAwMTeIj0FCgFxmSDiWuoBEbahkOj2lL6E2+lbU5tbUj6ZcdTAwMWSow2qtVf8udv3b6ld/PlxugrQok3icXHUwMDAz/UFo+eGW02043VZ21E+nxGbZjY/zRb1cdTAwMWa5YFx1MDAwMyNcZlLEKFx1MDAxZS1RR3pcIknEkd+tXpRAkZagilx1MDAxNOFxoi741e42pps0eWdcImdcdTAwMTKEXHUwMDEwN8JoZVx1MDAwNFCE5Fx1MDAwNZM0opoorJRkUUlEdMEo11xuwpLX6TghuL7qOd0w7+LYl5tRRrmxrUa+XHUwMDE1XHUwMDA2lW7Lp55e9MRsXHUwMDEw/ZFcblx1MDAxNZyOXHUwMDFiPPz5z48je2+MxU30KVwiJnneh/T3eTWKXHUwMDFjL1EkOJ5cdTAwMDPIZ19WXHUwMDFljYrVTprgUmSUVlB6Q+LkJLt1w6hBhEJcdTAwMWNySplk9M2Wt/hsXHUwMDAyRVx1MDAxMYh3upyiW2GskvG+tTy59Py27aNcdTAwMTiQ//r3UlXKXHUwMDE0rs+rlJylL1x1MDAxMytqwnaOwVQxSMazXHUwMDAzb/L+1orWXHUwMDA2QktcdTAwMDS9KDGKZlx1MDAwNUl8SEFcdTAwMWLEhVEkOlx1MDAxN/TKQ1xuXHUwMDEzgEdcdEZSYoVcdTAwMTnoJUzFiNUvXCI5YoRcdTAwMDEzsvyxxSdUQtbGMFx1MDAwMjLPTs9cdTAwMTLFyr7TOvH6cqu822w/XFw2N1x1MDAxZZ3z8423rZfnXHUwMDE1K/MoXHUwMDAzwVxyg6mimkPZyLRK9XpcdTAwMTZcdTAwMDZES0HZy4XK5I2mrDk0OlJcdTAwMDRpXHUwMDE5XmhcdTAwMDCzpGBcdTAwMGVFYKRkw1N471xcplx1MDAwMC5ZdFTAsKfD8un7pVx1MDAwMFFmXHUwMDE0Y4Nl6uFgxz9vLFx1MDAwMKNPXHUwMDAxelx1MDAwYlI9avwxbcJcdTAwMTVhkHJm321cdTAwMThccq9cdTAwMTVPvoyKaFx1MDAxYUFKgtjUXHUwMDAw70zyXHUwMDE1nCFQPVxcgNBkmYXJRcueROJM3G3gWlExVzH4XpZl7lx1MDAwNmqibkH8uUtcdTAwMTY+U2RDXvjkTX2Z8oFcdTAwMDJzbM0ho9NcdTAwMWRcdTAwMDC/2cE3eZl4NU9nSmZcdTAwMDBdRFx1MDAxMKi2WeaoSix8XGaBXHUwMDFjXG7131Nd+2bSh1x1MDAxOIOUXHUwMDE22DBcdTAwMDI1XHUwMDA1o4pcdTAwMTexSCTUnURjXG4l0lx1MDAxOPHDuVx1MDAwMv7jYkXFz87Rdui2+4dcdTAwMWLbvavHg1x1MDAwNt1cdTAwMGZvN3bfqfjBXGLIScnon+QwJSbVZ6A1SLSWQyeJjZn0z+R124xFQDWQnomKTlVcdTAwMGLNaVH/cKRcdTAwMTmTRFx1MDAwM5njhMLfp/whSiCjwfWEXHUwMDE4XHUwMDEyXHUwMDBmJ6N+XGZcIuB6XCJcdTAwMDfbedPVz3hcYsatXHUwMDA18C1I/tDxxadcdTAwMDAsQ1Jis2//jlx1MDAwNthqZ2ClXHUwMDE1UpSDqmC4eNhCcI1cZsWYXHUwMDE3Nq1cdTAwMTabgMVMx+OBXHUwMDA0XGbEXHUwMDEz/lx1MDAxZE9abHtd+1x1MDAxZkuVPFP0Ql7yXGZcZnyZ0GF6rM5cdTAwMTGR8Dacz46yyUe8Vlx1MDAxM2VcdTAwMTLYSGGsoWRcdTAwMDRIXHSVXHUwMDA1XHUwMDE5jir55JBcdTAwMDV/I5BcdTAwMTFgKEJcdTAwMTWWUF1CYoM0O0LlXGKo0bnCQGD5fehcdTAwMDFcdTAwMDbh2VBcdTAwMDeJV56NfzONU71t7Vx1MDAxY7Q23E1v1z5o3oqT+0M9527UojROnpmna43JZ5/WsptCoJaJplx1MDAxNGzlXHUwMDAyXHUwMDE0NC/uXG5RJGT0Z708PqKm3/1yy9jwjT5CI1x1MDAwNlU8V0+nb2dYb1x1MDAwMUhcbqKMUVx1MDAxOL5CVDOVfmBcdTAwMDFcdFx1MDAwYlJcdTAwMWNi/DZcdTAwMTOL8iCAa/ZcXDg63Fc7XHUwMDE3gnpD0Z9CcfB45NnseTPBXHUwMDE1YuD1p02mV/+F0LhkaEaUeEXFQZXBkkv6Oy63lO/rdi+MgnKZqmNcbntcdTAwMTfOd1x1MDAwZY1cdTAwMWOA7cNcdTAwMTOc161er1x1MDAxNoL/hulcdTAwMTRmxmk8OSHx2fqtY99tjYqM+Fx1MDAxMz01XHUwMDA2cFx1MDAwNFx1MDAxNTvmqp9cdTAwMWZ+/lxycIktXGYifQ== PENDINGRUNNINGCANCELLEDERRORSUCCESSWorker.start()worker.cancel()Done!Exception"},{"location":"guide/workers/#worker-events","title":"Worker events","text":"

When a worker changes state, it sends a Worker.StateChanged event to the widget where the worker was created. You can handle this message by defining an on_worker_state_changed event handler. For instance, here is how we might log the state of the worker that updates the weather:

weather04.py
import httpx\nfrom rich.text import Text\nfrom textual import work\nfrom textual.app import App, ComposeResult\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Input, Static\nfrom textual.worker import Worker\nclass WeatherApp(App):\n\"\"\"App to display the current weather.\"\"\"\nCSS_PATH = \"weather.tcss\"\ndef compose(self) -> ComposeResult:\nyield Input(placeholder=\"Enter a City\")\nwith VerticalScroll(id=\"weather-container\"):\nyield Static(id=\"weather\")\nasync def on_input_changed(self, message: Input.Changed) -> None:\n\"\"\"Called when the input changes\"\"\"\nself.update_weather(message.value)\n@work(exclusive=True)\nasync def update_weather(self, city: str) -> None:\n\"\"\"Update the weather for the given city.\"\"\"\nweather_widget = self.query_one(\"#weather\", Static)\nif city:\n# Query the network API\nurl = f\"https://wttr.in/{city}\"\nasync with httpx.AsyncClient() as client:\nresponse = await client.get(url)\nweather = Text.from_ansi(response.text)\nweather_widget.update(weather)\nelse:\n# No city, so just blank out the weather\nweather_widget.update(\"\")\ndef on_worker_state_changed(self, event: Worker.StateChanged) -> None:\n\"\"\"Called when the worker state changes.\"\"\"\nself.log(event)\nif __name__ == \"__main__\":\napp = WeatherApp()\napp.run()\n

If you run the above code with textual you should see the worker lifetime events logged in the Textual console.

textual run weather04.py --dev\n
"},{"location":"guide/workers/#thread-workers","title":"Thread workers","text":"

In previous examples we used run_worker or the work decorator in conjunction with coroutines. This works well if you are using an async API like httpx, but if your API doesn't support async you may need to use threads.

What are threads?

Threads are a form of concurrency supplied by your Operating System. Threads allow your code to run more than a single function simultaneously.

You can create threads by setting thread=True on the run_worker method or the work decorator. The API for thread workers is identical to async workers, but there are a few differences you need to be aware of when writing code for thread workers.

The first difference is that you should avoid calling methods on your UI directly, or setting reactive variables. You can work around this with the App.call_from_thread method which schedules a call in the main thread.

The second difference is that you can't cancel threads in the same way as coroutines, but you can manually check if the worker was cancelled.

Let's demonstrate thread workers by replacing httpx with urllib.request (in the standard library). The urllib module is not async aware, so we will need to use threads:

weather05.py
from urllib.request import Request, urlopen\nfrom rich.text import Text\nfrom textual import work\nfrom textual.app import App, ComposeResult\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Input, Static\nfrom textual.worker import Worker, get_current_worker\nclass WeatherApp(App):\n\"\"\"App to display the current weather.\"\"\"\nCSS_PATH = \"weather.tcss\"\ndef compose(self) -> ComposeResult:\nyield Input(placeholder=\"Enter a City\")\nwith VerticalScroll(id=\"weather-container\"):\nyield Static(id=\"weather\")\nasync def on_input_changed(self, message: Input.Changed) -> None:\n\"\"\"Called when the input changes\"\"\"\nself.update_weather(message.value)\n@work(exclusive=True, thread=True)\ndef update_weather(self, city: str) -> None:\n\"\"\"Update the weather for the given city.\"\"\"\nweather_widget = self.query_one(\"#weather\", Static)\nworker = get_current_worker()\nif city:\n# Query the network API\nurl = f\"https://wttr.in/{city}\"\nrequest = Request(url)\nrequest.add_header(\"User-agent\", \"CURL\")\nresponse_text = urlopen(request).read().decode(\"utf-8\")\nweather = Text.from_ansi(response_text)\nif not worker.is_cancelled:\nself.call_from_thread(weather_widget.update, weather)\nelse:\n# No city, so just blank out the weather\nif not worker.is_cancelled:\nself.call_from_thread(weather_widget.update, \"\")\ndef on_worker_state_changed(self, event: Worker.StateChanged) -> None:\n\"\"\"Called when the worker state changes.\"\"\"\nself.log(event)\nif __name__ == \"__main__\":\napp = WeatherApp()\napp.run()\n

In this example, the update_weather is not asynchronous (i.e. a regular function). The @work decorator has thread=True which makes it a thread worker. Note the use of get_current_worker which the function uses to check if it has been cancelled or not.

Important

Textual will raise an exception if you add the work decorator to a regular function without thread=True.

"},{"location":"guide/workers/#posting-messages","title":"Posting messages","text":"

Most Textual functions are not thread-safe which means you will need to use call_from_thread to run them from a thread worker. An exception would be post_message which is thread-safe. If your worker needs to make multiple updates to the UI, it is a good idea to send custom messages and let the message handler update the state of the UI.

"},{"location":"how-to/","title":"How To","text":"

Welcome to the How To section.

Here you will find How To articles which cover various topics at a higher level than the Guide or Reference. We will be adding more articles in the future. If there is anything you would like to see covered, open an issue in the Textual repository!

"},{"location":"how-to/center-things/","title":"Center things","text":"

If you have ever needed to center something in a web page, you will be glad to know it is much easier in Textual.

This article discusses a few different ways in which things can be centered, and the differences between them.

"},{"location":"how-to/center-things/#aligning-widgets","title":"Aligning widgets","text":"

The align rule will center a widget relative to one or both edges. This rule is applied to a container, and will impact how the container's children are arranged. Let's see this in practice with a trivial app containing a Static widget:

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass CenterApp(App):\n\"\"\"How to center things.\"\"\"\nCSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Static(\"Hello, World!\")\nif __name__ == \"__main__\":\napp = CenterApp()\napp.run()\n

Here's the output:

CenterApp Hello,\u00a0World!

The container of the widget is the screen, which has the align: center middle; rule applied. The center part tells Textual to align in the horizontal direction, and middle tells Textual to align in the vertical direction.

The output may surprise you. The text appears to be aligned in the middle (i.e. vertical edge), but left aligned on the horizontal. This isn't a bug \u2014 I promise. Let's make a small change to reveal what is happening here. In the next example, we will add a background and a border to our text:

Tip

Adding a border is a very good way of visualizing layout issues, if something isn't behaving as you would expect.

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass CenterApp(App):\n\"\"\"How to center things.\"\"\"\nCSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    #hello {\n        background: blue 50%;\n        border: wide white;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Static(\"Hello, World!\", id=\"hello\")\nif __name__ == \"__main__\":\napp = CenterApp()\napp.run()\n

The static widget will now have a blue background and white border:

CenterApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eHello,\u00a0World!\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

Note the static widget is as wide as the screen. Since the widget is as wide as its container, there is no room for it to move in the horizontal direction.

Info

The align rule applies to widgets, not the text.

In order to see the center alignment, we will have to make the widget smaller than the width of the screen. Let's set the width of the Static widget to auto, which will make the widget just wide enough to fit the content:

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass CenterApp(App):\n\"\"\"How to center things.\"\"\"\nCSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    #hello {\n        background: blue 50%;\n        border: wide white;\n        width: auto;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Static(\"Hello, World!\", id=\"hello\")\nif __name__ == \"__main__\":\napp = CenterApp()\napp.run()\n

If you run this now, you should see the widget is aligned on both axis:

CenterApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eHello,\u00a0World!\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

"},{"location":"how-to/center-things/#aligning-text","title":"Aligning text","text":"

In addition to aligning widgets, you may also want to align text. In order to demonstrate the difference, lets update the example with some longer text. We will also set the width of the widget to something smaller, to force the text to wrap.

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nQUOTE = \"Could not find you in Seattle and no terminal is in operation at your classified address.\"\nclass CenterApp(App):\n\"\"\"How to center things.\"\"\"\nCSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    #hello {\n        background: blue 50%;\n        border: wide white;\n        width: 40;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Static(QUOTE, id=\"hello\")\nif __name__ == \"__main__\":\napp = CenterApp()\napp.run()\n

Here's what it looks like with longer text:

CenterApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eCould\u00a0not\u00a0find\u00a0you\u00a0in\u00a0Seattle\u00a0and\u00a0no\u00a0\u258a \u258eterminal\u00a0is\u00a0in\u00a0operation\u00a0at\u00a0your\u00a0\u258a \u258eclassified\u00a0address.\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

Note how the widget is centered, but the text within it is flushed to the left edge. Left aligned text is the default, but you can also center the text with the text-align rule. Let's center align the longer text by setting this rule:

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nQUOTE = \"Could not find you in Seattle and no terminal is in operation at your classified address.\"\nclass CenterApp(App):\n\"\"\"How to center things.\"\"\"\nCSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    #hello {\n        background: blue 50%;\n        border: wide white;\n        width: 40;\n        text-align: center;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Static(QUOTE, id=\"hello\")\nif __name__ == \"__main__\":\napp = CenterApp()\napp.run()\n

If you run this, you will see that each line of text is individually centered:

CenterApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u00a0Could\u00a0not\u00a0find\u00a0you\u00a0in\u00a0Seattle\u00a0and\u00a0no\u00a0\u258a \u258e\u00a0\u00a0\u00a0terminal\u00a0is\u00a0in\u00a0operation\u00a0at\u00a0your\u00a0\u00a0\u00a0\u258a \u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0classified\u00a0address.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

You can also use text-align to right align text or justify the text (align to both edges).

"},{"location":"how-to/center-things/#aligning-content","title":"Aligning content","text":"

There is one last rule that can help us center things. The content-align rule aligns content within a widget. It treats the text as a rectangular region and positions it relative to the space inside a widget's border.

In order to see why we might need this rule, we need to make the Static widget larger than required to fit the text. Let's set the height of the Static widget to 9 to give the content room to move:

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nQUOTE = \"Could not find you in Seattle and no terminal is in operation at your classified address.\"\nclass CenterApp(App):\n\"\"\"How to center things.\"\"\"\nCSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    #hello {\n        background: blue 50%;\n        border: wide white;\n        width: 40;\n        height: 9;\n        text-align: center;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Static(QUOTE, id=\"hello\")\nif __name__ == \"__main__\":\napp = CenterApp()\napp.run()\n

Here's what it looks like with the larger widget:

CenterApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u00a0Could\u00a0not\u00a0find\u00a0you\u00a0in\u00a0Seattle\u00a0and\u00a0no\u00a0\u258a \u258e\u00a0\u00a0\u00a0terminal\u00a0is\u00a0in\u00a0operation\u00a0at\u00a0your\u00a0\u00a0\u00a0\u258a \u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0classified\u00a0address.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258e\u258a \u258e\u258a \u258e\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

Textual aligns a widget's content to the top border by default, which is why the space is below the text. We can tell Textual to align the content to the center by setting content-align: center middle;

Note

Strictly speaking, we only need to align the content vertically here (there is no room to move the content left or right) So we could have done content-align-vertical: middle;

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nQUOTE = \"Could not find you in Seattle and no terminal is in operation at your classified address.\"\nclass CenterApp(App):\n\"\"\"How to center things.\"\"\"\nCSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    #hello {\n        background: blue 50%;\n        border: wide white;\n        width: 40;\n        height: 9;\n        text-align: center;\n        content-align: center middle;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Static(QUOTE, id=\"hello\")\nif __name__ == \"__main__\":\napp = CenterApp()\napp.run()\n

If you run this now, the content will be centered within the widget:

CenterApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258e\u258a \u258e\u00a0Could\u00a0not\u00a0find\u00a0you\u00a0in\u00a0Seattle\u00a0and\u00a0no\u00a0\u258a \u258e\u00a0\u00a0\u00a0terminal\u00a0is\u00a0in\u00a0operation\u00a0at\u00a0your\u00a0\u00a0\u00a0\u258a \u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0classified\u00a0address.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258e\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

"},{"location":"how-to/center-things/#aligning-multiple-widgets","title":"Aligning multiple widgets","text":"

It's just as easy to align multiple widgets as it is a single widget. Applying align: center middle; to the parent widget (screen or other container) will align all its children.

Let's create an example with two widgets. The following code adds two widgets with auto dimensions:

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass CenterApp(App):\n\"\"\"How to center things.\"\"\"\nCSS = \"\"\"\n    .words {\n        background: blue 50%;\n        border: wide white;\n        width: auto;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Static(\"How about a nice game\", classes=\"words\")\nyield Static(\"of chess?\", classes=\"words\")\nif __name__ == \"__main__\":\napp = CenterApp()\napp.run()\n

This produces the following output:

CenterApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eHow\u00a0about\u00a0a\u00a0nice\u00a0game\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eof\u00a0chess?\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

We can center both those widgets by applying the align rule as before:

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass CenterApp(App):\n\"\"\"How to center things.\"\"\"\nCSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    .words {\n        background: blue 50%;\n        border: wide white;\n        width: auto;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Static(\"How about a nice game\", classes=\"words\")\nyield Static(\"of chess?\", classes=\"words\")\nif __name__ == \"__main__\":\napp = CenterApp()\napp.run()\n

Here's the output:

CenterApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eHow\u00a0about\u00a0a\u00a0nice\u00a0game\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eof\u00a0chess?\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

Note how the widgets are aligned as if they are a single group. In other words, their position relative to each other didn't change, just their position relative to the screen.

If you do want to center each widget independently, you can place each widget inside its own container, and set align for those containers. Textual has a builtin Center container for just this purpose.

Let's wrap our two widgets in a Center container:

from textual.app import App, ComposeResult\nfrom textual.containers import Center\nfrom textual.widgets import Static\nclass CenterApp(App):\n\"\"\"How to center things.\"\"\"\nCSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    .words {\n        background: blue 50%;\n        border: wide white;\n        width: auto;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nwith Center():\nyield Static(\"How about a nice game\", classes=\"words\")\nwith Center():\nyield Static(\"of chess?\", classes=\"words\")\nif __name__ == \"__main__\":\napp = CenterApp()\napp.run()\n

If you run this, you will see that the widgets are centered relative to each other, not just the screen:

CenterApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eHow\u00a0about\u00a0a\u00a0nice\u00a0game\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eof\u00a0chess?\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

"},{"location":"how-to/center-things/#summary","title":"Summary","text":"

Keep the following in mind when you want to center content in Textual:

  • In order to center a widget, it needs to be smaller than its container.
  • The align rule is applied to the parent of the widget you want to center (i.e. the widget's container).
  • The text-align rule aligns text on a line by line basis.
  • The content-align rule aligns content within a widget.
  • Use the Center container if you want to align multiple widgets relative to each other.
  • Add a border if the alignment isn't working as you would expect.

If you need further help, we are here to help.

"},{"location":"how-to/design-a-layout/","title":"Design a Layout","text":"

This article discusses an approach you can take when designing the layout for your applications.

Textual's layout system is flexible enough to accommodate just about any application design you could conceive of, but it may be hard to know where to start. We will go through a few tips which will help you get over the initial hurdle of designing an application layout.

"},{"location":"how-to/design-a-layout/#tip-1-make-a-sketch","title":"Tip 1. Make a sketch","text":"

The initial design of your application is best done with a sketch. You could use a drawing package such as Excalidraw for your sketch, but pen and paper is equally as good.

Start by drawing a rectangle to represent a blank terminal, then draw a rectangle for each element in your application. Annotate each of the rectangles with the content they will contain, and note wether they will scroll (and in what direction).

For the purposes of this article we are going to design a layout for a Twitter or Mastodon client, which will have a header / footer and a number of columns.

Note

The approach we are discussing here is applicable even if the app you want to build looks nothing like our sketch!

Here's our sketch:

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1daW9cdTAwMWK7kv1+f0WQ92VcdTAwMDa46ktcdTAwMTbJXCJ5gcHAi1x1MDAxY1mOl8iKt8GDoaUtyda+eHvIf5+ivKjVUsstW7JbuTaQxGktzWafOudcdTAwMTRZTf7njy9fvvbv2v7Xv7989W9LhXqt3C3cfP3TXHUwMDFkv/a7vVqrSS/B8P+91qBbXHUwMDFhvrPa77d7f//1V6PQvfL77Xqh5HvXtd6gUO/1XHUwMDA35VrLK7VcdTAwMWF/1fp+o/e/7u+9QsP/n3arUe53vdFJUn651m91XHUwMDFmzuXX/Ybf7Pfo2/+P/v/ly3+Gf1x1MDAwN1rX9Uv9QrNS94dcdTAwMWZcdTAwMTi+NGogZ0aFXHUwMDBm77Waw9ZyLoxGxVxyPL+j1tukXHUwMDEz9v0yvXxBjfZHr7hDX1x1MDAwZnL9dT+3q/ay56WbXCK2djHdXHUwMDFkjM57UavXXHUwMDBm+3f1YbtK3Vavl6pcdTAwMTb6peroXHUwMDFkvX63deVcdTAwMWbXyv3qU/dcdTAwMDWOP3+216KuXHUwMDE4farbXHUwMDFhVKpNv+d6gT9cdTAwMWZttVx1MDAwYqVa/254lez56ENX/P1ldOSW/pdC4Vx0LY1VqLW1KtAr7lx1MDAwYkAoXHUwMDBmhFx1MDAxNKhcdTAwMDVarahfQi3baNXpjlDL/sWGP6O2XHUwMDE1XHUwMDBipatcbjWwWX5+T79baPbahS7dt9H7blx1MDAxZa9ZSemhcCdjinM+akfVr1WqfXqHQOZcdTAwMTnOrJQohLs9OGqMP7w1XHUwMDFjjNCopVx1MDAxNs+vuCa0t8tDnPw72HHN8mPHNVx1MDAwN/X6qNXuhXRcdTAwMDBbo89cZtrlwlx1MDAwM1x1MDAwMjhcdTAwMWFcdIpJ1Fxio1x1MDAwZanXmlfhr6u3Slcj0Fxmj/7681x1MDAxNXDVKlx1MDAxYa1WcVx1MDAwNIlcdTAwMThcdTAwMWKsuVLn5367sL63s3+Lza2fXHUwMDA3nZ3tTlx1MDAwNFhDgPtImFx1MDAxYcGVtIpxJjhBIIRT6Vx1MDAxOaUlgcJcdTAwMDJKi8vEqfBcdTAwMThHXHUwMDA1dD6urTaTQFx1MDAwNeMhMiFccjJAQaCeXHUwMDAwKt0zJYGuZvVw6tfrtXZvKkpxXHUwMDA2p1x1MDAxYZCcI1cmNkzbP86OS+lstssvc9f5+3TqrtG8fVxyTPn7wVRxz1xuhdTxQljOQIbolDjOMCMl11x1MDAwZcqgw1x1MDAxMTRcdTAwMTdM/3VRUKBgXHUwMDEyolxceJJcdTAwMDEoi0D/SGPkJEY5eFxuKVKISFx1MDAxOVxiXHUwMDAxUocxyql9ilNcdTAwMWL5b1x1MDAwNVItbFx1MDAxNEi1QXfNXHUwMDEwX/dP1jrpu8ZB7qC4tnXT2upvna3jTcIxXG7KY9TdyLnhlsBcdTAwMDEhiFxuT1x1MDAxObRkXG4kUZhV8o1cdTAwMTAtMqaWXHUwMDA1Uc1cdTAwMTXpgeC/XHUwMDE5Qq2IQijnjJSFS81iQ7TeSbdTV1x1MDAxNexs1upXWLrrb53UXHUwMDBlk1xyUVx1MDAxMljgXHUwMDAyQWjBmWRWh1x1MDAxMFxujmQl3Vx1MDAxNVxyxLVv41BcdTAwMGVFY3BZXHUwMDAwpcsgqkcuV1DoZ+dPKlx1MDAxMqPu1iihXHUwMDAzb3hcdKLr37ZcbrsldbLV2V1vnt3+OL6pXHUwMDFjNJJcctFcdTAwMTTlXCKk4lZcdTAwMTFFcnBJyThGXHUwMDExXHQ+hFxmTTTKJcOlYFRJ5oym4Vx1MDAxNCnaQiBOXHUwMDAyOZMnNaekytphO0CFIUquTJGLNYrNXHUwMDAx0SewjOBcIlx1MDAxZY/8ikbu82dGn1x1MDAwZcCt79+O3HZcdTAwMDBcdTAwMWPd9mG1cbB9tj/YK+VzlbXWgVx1MDAxOdx9fX7fr8ffosJCXGImjTLWLiosxtpcdTAwMTmMXGJcdTAwMTNcdTAwMTVcdTAwMTCczs5dvqBiR8T0i55cdTAwMWVcdTAwMTHVQqk66PpcdTAwMWZcdTAwMWZcdTAwMTNcXCO5W+JKQIK+YTLE21x1MDAxYTxcdTAwMDdB4nNcdTAwMTlcbpnXXHUwMDA0XHUwMDA1xVx1MDAxZbuYYn4p8oBcdTAwMTPaXHUwMDE5p1x1MDAwZSeRmJafTVx1MDAwNoFcdTAwMDWuyZ3PXHUwMDE1XHUwMDA0r+Pp11x1MDAwMHJ041vN/mHt3vU9sLGjW4VGrX43du+GSKWuyviFst/9OnZ8rV6rONR+LVF7gy9cdTAwMTJ2+7VSof78hkatXFxcdTAwMGVqQIlOVqg1/e52XHUwMDFj7m51a5Vas1DPR7SFrt3PPOupXHUwMDE3uDPFQs93r1x1MDAwZVx1MDAwM+j1Qlx1MDAwNVx1MDAxOJmTkns0QltcdTAwMTN/5GRtc6O+M2Bddn/XuK/YfL5/lc68LizfcexEXHUwMDE5j5NRkoxsfUhcdTAwMDPcN1xihp7ipGQ8XHUwMDE4XHUwMDE3XHUwMDBiXHUwMDFlNeGKuEFScvEkiKPrXHUwMDFmhSX5KbCGXHUwMDFisnXKqCA/PLkp0IZy6sDgz1x1MDAxMt2UJt5cdTAwMGVkP0t1U1x1MDAwMapcZouHM/zW5fmxUdqpbbT2sr3NXGZ2N1x1MDAwZTK57av9evpV43vvOXBcIjy6t+hcdTAwMTJwMis6hFBuKCdcdTAwMTX0gmIhYLzGTalCoXgh5TTPzz1GSIUpg89Selx1MDAxMjSzj01cYjbxXHUwMDExnUJcdTAwMWKlmVx0XGZcYr5cdTAwMTWcrzNM2Xbnvt45Ov/x48f5Sf3i6vgwfXxcdTAwMWMwTH9O/9qHXHUwMDBm51t7lUIu5Z9095pH9lx1MDAxMCv1y+1cdTAwMWbjZ3k6f6Hbbd3MYcQ4XHUwMDAwhZRQi4qoSCNcdTAwMTY5vkMmjGhcdTAwMGWtiZ+aTO/MhFx1MDAxYrFcdTAwMTRcdTAwMTGpIOtDySBlXHUwMDA1aMLhJJin6bBloFxirnYh2cm0eJLW46QsmnJBy6ZGVcDEPI2MazJvxr5cdTAwMDPFXHUwMDBiSpjIi1x1MDAwNYj1dUaM49jRXHUwMDE5Rix/4/v9JfmwXHUwMDE3SD/sw0JNiWfD5Fx1MDAxYmyYgPDR5/FcdTAwMDLBXGYjb1x1MDAxMt+GVXnaNrYvdzqp6lx1MDAwNuxfyrPTn0onX+BcZnJcdTAwMDGGh+1cdTAwMTdcdTAwMWGPRIVcdTAwMDJEaGCaiVCbPsUteOu3mtlccr9/36hWz5pq/7qU7drcSXxcdTAwMTFcdTAwMDLFKVx1MDAwMdNq+aNcdTAwMDFcdTAwMDFWXGKrkLbcXHUwMDE4w+dcdTAwMTggm37VK6BCJD+KSWHleHYxxL1hXHUwMDFlJSXyzXnHylx1MDAwYpCYXHUwMDAzjMlcdTAwMTWgXHUwMDE3SPnDXHUwMDA1yLLw0ZEtVJJroUT8uen1k5udY5VLnzXRXFzdf9va6ejNXtJcdTAwMTWISN5cdTAwMDBcdC03XHUwMDFjJ4dcdTAwMDEkMOp24iVOIYtGvNFcdTAwMTT+5jqUXHUwMDFmXFyv5fa+X/LU/u3deW692Pl29X1OXHUwMDFkUjJQXHUwMDE4sixcdTAwMWTSkb5cdTAwMGKYkFxcQrBW5iXYT7/qxOuQ8JRigCBcdTAwMTXnlIKG0iHiYU9qXHUwMDEwlFx1MDAwYjFBXHUwMDE27Y1cdTAwMDbsU4xcdTAwMTIgRi/w80eLkTA8fPQpKlxySM1cdTAwMDH1aIzrpaBs9b5cdTAwMWSet9bWe2epn+1jW7lnJ7yQdC1SXHUwMDFlkTjprlx1MDAxNVO1SLteXHUwMDE3Slx1MDAxYm6QXHTFlydGU8ah5cTAc1JU5yB9WG9uXHUwMDFmVlx1MDAwN/5ajde+dUSnXdqZU3U0LF91Zk26kOxcdTAwMDCoOZKf6Vx1MDAxN71cdTAwMDKig0hcdTAwMTIvNJsqOkYsXHUwMDE04Z+qk1x1MDAwMNV5gYk/WnU4RqpcdTAwMGVcdTAwMDHEWUGr45codPZcdTAwMGWvXHUwMDFi6Hc6WXPon9rDwkb7eDPZsmOYp13dNVk9YVx0fCM6f5hlolBcdTAwMTGCXCKTQmk8Yl9cdTAwMTOSXHUwMDAwpuhPKywzXHUwMDFlc1Vj04ORXGLa01x1MDAxOJ6MfZz+ZHTcXG7BR/q0xFwiXHUwMDA1VFxc2PcpJlx1MDAwM1x1MDAxYpmkcCGlq+Ww8f3Qjl83379l6rtcdTAwMTfqqr2pdLfWbq4nXHUwMDFimK6aXGaBOdplXHUwMDE2UYcqXHUwMDFlldJcdTAwMWWlxXRYg5VcdTAwMTbeVvFooGS5P1x0zH9ENVmnWMzk8T6T7V3ft3WuslNax/R8XHUwMDBlytBcdTAwMWTgi4qLyLxcdTAwMWRlVEhoXHUwMDAzwDmzc1D11ItOuIPiWntcdTAwMTKtq8IgymMqVE2mUHiMoXtcdTAwMDe5TXzjXHUwMDEwcmQxmSTAWyvoNCBcZlx1MDAwNNK2hFx1MDAxNJMtyD/FLybbarX6Sysme4G6w1x1MDAwNirclqVcdTAwMTeTXHUwMDA1XHUwMDA3JENRKSkkiacxvk5cdTAwMWSfyduLTnFXZHbSm5mN3Lp/1j9KejFcdTAwMTnnnPyJtGjMw2zl6GtcdTAwMWVqySxFpXJPOHF8c43nSlaUTdelx6Gajla63vJ57fREXFyKrl/cuD1fWHVccmk2W5gwzTZsMnp2k9hcdTAwMWHcJEL86fxcdTAwMDNd6N521i94tre/0Thkx1x1MDAwN+e902RcdTAwMWI2QrdngFx1MDAxMftCuJZ5XHUwMDE4XHUwMDA2wDyrjEW7iDD47SdT1vKnu9vbP1x1MDAwZnY2+1x1MDAxOdk46lx1MDAxZmyd5W3cirXs9o+LU6wp1chu9ZrXR5XCzdXu4irW3NTg0s1cdTAwMWXMKP90s0RcdTAwMWGZjVx1MDAxZE7TezPpbo9SXHUwMDFjrYiOXHUwMDE5Ob6JXHUwMDA0SFxi4ZGlUlxcPZYwvy2cPlx1MDAwN8timr1lXHUwMDBllr3A+lx1MDAxZj1YJmT0xKlcIoiilvGHsEV3/fJ252fZ7MHZcVVV8VStdZOvcKRvJOVcdTAwMTiWsGFEalI4So0oXHUwMDAzs1x1MDAwYnjA7TdcdTAwMTe4/PptrWvWXHUwMDBlb1tcdTAwMWIoikff7zavXHUwMDBi+3M9wyZcdTAwMTRcYlxc+qhDcGooLEQgmUBleXzUT7/qxFx1MDAwYlx1MDAxMXpcdTAwMTZdMkdJ/6RcdTAwMTBcdTAwMTlcdTAwMTJcIvc4r3vY9+3FmqsqRK/AY3KF6Fx1MDAwNXL+cCGy0c8zUL5H2TaK+KtcdTAwMDGcsfNO6qJvMsfVtZ3Uxam6zlx1MDAwNzLQhCqR8lx1MDAxNFx1MDAwNzR0qUJPKJFcdTAwMDTwXGZl92RcdTAwMWQ1uHj5VKJcdTAwMTlKxLLp0rbMs61Byp5cdTAwMWOcXHUwMDE2JWtcdTAwMTVcdTAwMDbzjX9LYiZcdTAwMTbE/1KUKLpCXHUwMDA2NWHBTVx1MDAwYsVcdTAwMDb99IteXHUwMDAxIZJuXHUwMDEwmaDjVlx1MDAxYVx1MDAxOFx1MDAxN1wiKZTHkFx1MDAwMGdcZqi3V2uuqlx1MDAxMP1WXHUwMDE50Vx1MDAwYtz80UIkZ1RQk1WSXHUwMDA20cZcdTAwMTeiXvNO+er7fjs/yFxmWje8t7X5bTfpQiQ98r9aXHUwMDAyQ+VcbmdHXGb0ULQmSIiY0kYwMz5h9Y8uWjtcdTAwMTPrezc3crN4spMvpG4vq7uVg0pcdTAwMDIlR+jIKVeyIHS/jOXxNWf6Va+A5lxi4chcdTAwMWXc8LWRIatl1EJcdTAwMTG+4qozz9pcdTAwMDDJVZ1cdTAwMTeI+KNVh7xeVFSCMlxiwOJPNG01N3c7W6ksbMP5oLnm64vNb1FcdTAwMDNcdTAwMTJcdNFcdTAwMWNQzFOMXHUwMDFiTVx1MDAxZW+yXGJCXHUwMDAwp9RILWpJnZUuWVx1MDAxYq7Ywd6rZE1gdMlcdTAwMWFcdTAwMThB3oDJ+KVcdTAwMDCbOyeH7eyp3fyW/nFweqK+i3w/asWOxJRcdTAwMDLQZXqKYFx1MDAwMUrgtFx1MDAxMWLOPFSWhJPDm59r/u0qXHUwMDAxdvs3p8Wj01x1MDAxY5rMnbzoraW3blpnXHUwMDBim7WUYFx1MDAwMlx1MDAxNfhLrVx1MDAwNFx1MDAxMJFcdTAwMTUxnJO2U0Ns/JVrjptnxVRd18o1xnJZOEinipuphFx1MDAxM7QxRNBaI2pwSz2HaopcdTAwMDE8gr5WXGa5q/JXn/Mks1KFa57ezd3traV+yPtTuGfFjctKMW4hwPFdeW3ztsVcdTAwMGYzuWo5n8FBrnmbW1xcIYCVbOkpyMxVXHUwMDAztNKcXHUwMDFiXHUwMDFi/2nN6b2Z8Fx1MDAxNITMtSeFVdZcdTAwMTlszoJcdTAwMTHzUFx04HJw7Vx1MDAxYziGXHUwMDE20/hMQVY1XHUwMDA1eYH2PzpcdTAwMDVcdTAwMTFcInLgXHUwMDBiKE92i1xixlx1MDAxZvdK93aE/H7q31xcXHUwMDFmd1x1MDAwYmv3ftpcdTAwMWVVo8aiXHUwMDEzI3HaXHUwMDE1u1lheFjEXHUwMDFlSlx1MDAwMcAjkUelXHUwMDFmpkTDXHL7lLjg/b+6YoNGZuOuVsZu75JlZfv84GLO0TD3Z9lSXHUwMDA0PPpcdTAwMDFcdTAwMDRAVFx1MDAwNIb4XHUwMDBmIEy/6MQrkXElaVx1MDAxYbmRjPAvxlFvSIhIXHUwMDE4mISQ7/vUoVXVoVx1MDAxN7j5w3XIRJaJUrRcbuJnmGNcdJutVvU8ZVtHfnX/JNPM1lxuulHNJl+IXGJtQqCSROdhIZIgPWRMuVx1MDAxZFjQ2OC04KdcdTAwMTJNKtH29fqV8Sv5Sr3Fi6Zlv12y9LzzMu+iRFx1MDAxOF1cdTAwMDAjlVuxiFx1MDAwNYpcdTAwMDVegv30q15cdTAwMDEpcvVWyEBcdTAwMWJcdTAwMTGWXCIp0DPcjXxcdG5ccshFbIjxKUVcdTAwMWYrRS+w80dLkdSRUoSKvJLSc9Sk2SNbNlBLr/XyV6XizV1e/cgmfCVcdTAwMDEw6GlGcWCUskqHXHUwMDAzUitPUpxQsqRcdTAwMDAkLi8jWqlCgFx1MDAxMzi9zJTklrxs71c3dnZz+6U7lUDBXHUwMDEx0etkSPdwXHUwMDFiSX98vZl+0SugN1x1MDAxY6zbeskyJoIrRD/UXHUwMDAx6EXi+1NwXHUwMDEyIDgvkPBHXHUwMDBiXHUwMDBlRG+AqrVGXHUwMDAwruOnPt30UZrt7O83hTpJy8Za5uw+9zPZguOqnIGSXG5BXHUwMDA0NNysYywgXHUwMDA1uMdcdTAwMTJcdTAwMDJjcG9cbsjVL1x1MDAwM1x1MDAxMDqQXGIvtVxmQEfv+oRCgWVyjpVr9lmrv5nL+oY31/X6xe4mXjRcdTAwMTO/qix6buEowp52i5mFXHUwMDE28qNcdTAwMWXwrFvv+nFcdTAwMTW0t42SRVx1MDAwMVO6labdhlNuq1x1MDAwM/c1OFx0T5Bu116SXHUwMDExPVxcx1x1MDAwMyZcdTAwMGJcdTAwMDDeXHUwMDE3o1x1MDAxMoRcXPoq5DNYU7n1XHUwMDAyOJujiv460ylmXHUwMDFh9udm+eLozFx1MDAxN1x1MDAwNzlTgGbSnVxmXHUwMDExJlx1MDAwMVx1MDAxMNFwI1x1MDAxMVhgfYSHrSS14yxOyTORhjViedtGk5eR5ExcdTAwMTRcYlx1MDAwYqFAmbGUXGY1yY1wwLKt+9KLUlx1MDAxNuSV5linpnbrlyO8Ut2/6M9wSv1WO8omjTV3Yjma8VMuYjWa6MdjXCLHxITbbFCjiV8nUDXr+3fn1fIgla1s5Vx1MDAxYq3LuzxL/OxcZoWFR1xcL4XVhlEqMl58hpx0h6SXQKeMU55/dFxc58hcdTAwMTZcdTAwMTbyV2dcXF9cdTAwMWNuX36HUjrTb3/G9Vx1MDAwN8b1QzdPM5Qztlx1MDAxMVVuu2vkMv7I2mxCT2pkg/bc9ijKKKVItsdcdTAwMWQloPBcdTAwMDSS41x1MDAxNNxcIuCbxrpnXHUwMDA2tmWeNW5FVMtcdTAwMTRcdTAwMTnHXHUwMDEx31x1MDAwNi0loqA2MOOe0MNcdEvJiXtcdTAwMTSToOdcdTAwMTmMmFxcXGZcdTAwMTFcdTAwMWWPzFhcZnFG5Zog9tOv2my01y90++u1ZrnWrIw3zH849XZcZl84jOjSwLWSeaApYXLzXHUwMDE0bjhSju6s65hC2/G2Ry6c2lxmlj9MsE1cXLrfLL/cpNn11IEmpZhnXHUwMDE0pztkhit6XHUwMDE4hWKiTZR5o9vcW5I9dONQdqJN9UKvv9FqNGp96vmDVq3ZXHUwMDBm9/CwK9dcXNBX/cJcdTAwMDTh0DVcdTAwMDVfXHUwMDBis0PbfeM4yY9++zKKnuF/nn//959T352KwrX7mUD06Nv+XGL+Ozet2ehHq1xmUr9Solx1MDAxY3/4ZracJZXVXHUwMDE0o1x1MDAwNJQpUIbwL9n4g1x1MDAxY1xi3ONusFx1MDAwMJlcdTAwMTaISyyWt8JDbYZLW4xtXGY4mj/gXHUwMDFlXHUwMDEzlFxmW0roXHRcdTAwMWWT01x0XHUwMDFjKWWiTJnNY15cdTAwMTbOasxcYnzVY78xWW22KVx1MDAxZadcdTAwMTBiNNdXXGJARKLZiO6fOcR40m1cdTAwMDIurGTuwVFjZvPa+HWsXHUwMDE0u0yH1/C1XHRkzckukfM1NnIyUlx0NG5p8/iL1lx1MDAxY1X29zZQ9vzaXHUwMDFl+Nnvt53Dw42bpJNLSqJLd4hBrEIlJIyzXHUwMDBiV+5JXHUwMDFkrVxyMolumONNw3Czn8Ux2uNCXHUwMDE5lNHZ0MScXHK3lFxyKVx1MDAwN4x3SYfesMftn7O+91xyK1x1MDAxMc783iU+2OD2one9/8b0Lf5cdTAwMTRcdTAwMTahaNBo9r7811N29qVX6rbq9f9+35QuRjNcdTAwMTYxpVx1MDAxNemHzIxcdTAwMDc9lFtseq5nzWdcdTAwMDM6oZwljEdGXHUwMDEzrHFcdTAwMWJccihcdTAwMDXjXHUwMDAzOI5HXHUwMDE0XHUwMDEzRltcdTAwMTKS5Y7MkunVJM1cbshccmshg9tCjCa3QHiGh2r+nthcdTAwMGKcnVx1MDAxYptSeG8/5MLY7dm3RD80W1x1MDAxN8ezPNBkcyVKpa1Ukk2meZy6XFxcInPVkbONUFRrZu92PO7OXHUwMDE4Uk5jQIOWXG5IlfREczQhkfwsvW4lXHUwMDEycuVEo1bKg0VcdTAwMDPa/Vx1MDAwNKE8p1x1MDAwMYukM2TRdCY510zMs13BbFx1MDAxZE0onVnjaW5cdGSEXCJcdCZUmsxccvfc7CRcYiWXTGfaeJRmSLe/MFx1MDAxMavk09hMoEfo0I55hzvdXHUwMDA0nkp95LRhnVx1MDAwMVx1MDAwNFx1MDAxN194f05T1Ly5dtpYXHUwMDFhp1x1MDAxMYtI4G6tU1wiXG6plbTTSMRyx2jSXHUwMDBlf8zkMFEsZpu9LO5cdTAwMTjPckKVXHUwMDE0YIxcdTAwMDX6VUzSLHiUhJEyuM50T2/halx1MDAxM1skst3PJKZcdTAwMTdGbjxyXHUwMDEynVTOclxu9vjcNtvLJ5TbXGI4XHUwMDFlKkBKLCWA0eOT6NxQz1x1MDAxM945d0umkl1b3pg8R+5m891CXHUwMDBlklx1MDAwMlLBlEJcdTAwMGbKOSlYJcWhXHUwMDFlrvRcdTAwMTBMdlx1MDAxZshNKtRqrlxc8/d1a47ZyPRcZvdop0ZJXHUwMDE2XHUwMDFjXHUwMDEwfnJrZFx1MDAxOEjFXHTsZlg680rPNvsp33FmQ+ZG+MDtduVOOTko71x1MDAxNlx1MDAxNzFuSS73yFx1MDAwMzVcdHGiUatEbdHAXHUwMDFlvjpcdTAwMDHpObkter3nSNvGnfZqPUel88Fpo8GuQF9tnaX3i93SbvbnbtRcIueJoTaNnuSkXHUwMDE3XHUwMDA0fjfXKMeX70CiNiNcZqM/Llx1MDAwZmVLXHUwMDFjN1NIpt1cdTAwMWHlRuhcZuppS9hMKatcdTAwMTSMzLVcclxc5YruXvNYRvDWcaj4ZVx1MDAwNNVWt3bvxoueRn7ed/xpxumXWl5cdTAwMTCoXHUwMDFiXHUwMDBll1xyKcvA7VpcdTAwMTY73mfDIaHxzqX2iF6F21x1MDAwN5TcitLjhdSopVx1MDAwN4xcdTAwMWOkojcuN+Ct52qDrZ3Ykz7gZKwnnVx1MDAwMNqIkSdtXHUwMDE5XHSHlfOE/6In4ubedWpeKzNbVr6EJuKoP9zjXCJ0f8mowlx1MDAxNN/gJvNcdTAwMWZ2d32li5m5a9uYi2Hu4Vx1MDAxOMVcdTAwMDRnROtcdTAwMDKnjIOBoFx1MDAxYqxcdTAwMWTna5fIMTZ5L1bKxURh2v2kJuBcdTAwMWNlYv54PMHXQrt92Ce4Pd9cdTAwMGXCd638yPWjq/x6XfNv1qfUi19cZn+cXGZccvvTUZI/jIFff/z6f3dfuVAifQ== HeaderTweetTweetTweetTweetFooterTweetTweetTweetTweetTweetTweetTweetTweetFixedFixedColumns (vertical scroll)horizontal scroll

It's rough, but it's all we need.

"},{"location":"how-to/design-a-layout/#tip-2-work-outside-in","title":"Tip 2. Work outside in","text":"

Like a sculpture with a block of marble, it is best to work from the outside towards the center. If your design has fixed elements (like a header, footer, or sidebar), start with those first.

In our sketch we have a header and footer. Since these are the outermost widgets, we will begin by adding them.

Tip

Textual has builtin Header and Footer widgets which you could use in a real application.

The following example defines an app, a screen, and our header and footer widgets. Since we're starting from scratch and don't have any functionality for our widgets, we are going to use the Placeholder widget to help us visualize our design.

In a real app, we would replace these placeholders with more useful content.

layout01.pyOutput
from textual.app import App, ComposeResult\nfrom textual.screen import Screen\nfrom textual.widgets import Placeholder\nclass Header(Placeholder):  # (1)!\npass\nclass Footer(Placeholder):  # (2)!\npass\nclass TweetScreen(Screen):\ndef compose(self) -> ComposeResult:\nyield Header(id=\"Header\")  # (3)!\nyield Footer(id=\"Footer\")  # (4)!\nclass LayoutApp(App):\ndef on_mount(self) -> None:\nself.push_screen(TweetScreen())\nif __name__ == \"__main__\":\napp = LayoutApp()\napp.run()\n
  1. The Header widget extends Placeholder.
  2. The footer widget extends Placeholder.
  3. Creates the header widget (the id will be displayed within the placeholder widget).
  4. Creates the footer widget.

LayoutApp #Header

"},{"location":"how-to/design-a-layout/#tip-3-apply-docks","title":"Tip 3. Apply docks","text":"

This app works, but the header and footer don't behave as expected. We want both of these widgets to be fixed to an edge of the screen and limited in height. In Textual this is known as docking which you can apply with the dock rule.

We will dock the header and footer to the top and bottom edges of the screen respectively, by adding a little CSS to the widget classes:

layout02.pyOutput
from textual.app import App, ComposeResult\nfrom textual.screen import Screen\nfrom textual.widgets import Placeholder\nclass Header(Placeholder):\nDEFAULT_CSS = \"\"\"\n    Header {\n        height: 3;\n        dock: top;\n    }\n    \"\"\"\nclass Footer(Placeholder):\nDEFAULT_CSS = \"\"\"\n    Footer {\n        height: 3;\n        dock: bottom;\n    }\n    \"\"\"\nclass TweetScreen(Screen):\ndef compose(self) -> ComposeResult:\nyield Header(id=\"Header\")\nyield Footer(id=\"Footer\")\nclass LayoutApp(App):\ndef on_ready(self) -> None:\nself.push_screen(TweetScreen())\nif __name__ == \"__main__\":\napp = LayoutApp()\napp.run()\n

LayoutApp #Header #Footer

The DEFAULT_CSS class variable is used to set CSS directly in Python code. We could define these in an external CSS file, but writing the CSS inline like this can be convenient if it isn't too complex.

When you dock a widget, it reduces the available area for other widgets. This means that Textual will automatically compensate for the 6 additional lines reserved for the header and footer.

"},{"location":"how-to/design-a-layout/#tip-4-use-fr-units-for-flexible-things","title":"Tip 4. Use FR Units for flexible things","text":"

After we've added the header and footer, we want the remaining space to be used for the main interface, which will contain the columns in the sketch. This area is flexible (will change according to the size of the terminal), so how do we ensure that it takes up precisely the space needed?

The simplest way is to use fr units. By setting both the width and height to 1fr, we are telling Textual to divide the space equally amongst the remaining widgets. There is only a single widget, so that widget will fill all of the remaining space.

Let's make that change.

layout03.pyOutput
from textual.app import App, ComposeResult\nfrom textual.screen import Screen\nfrom textual.widgets import Placeholder\nclass Header(Placeholder):\nDEFAULT_CSS = \"\"\"\n    Header {\n        height: 3;\n        dock: top;\n    }\n    \"\"\"\nclass Footer(Placeholder):\nDEFAULT_CSS = \"\"\"\n    Footer {\n        height: 3;\n        dock: bottom;\n    }\n    \"\"\"\nclass ColumnsContainer(Placeholder):\nDEFAULT_CSS = \"\"\"\n    ColumnsContainer {\n        width: 1fr;\n        height: 1fr;\n        border: solid white;\n    }\n    \"\"\"  # (1)!\nclass TweetScreen(Screen):\ndef compose(self) -> ComposeResult:\nyield Header(id=\"Header\")\nyield Footer(id=\"Footer\")\nyield ColumnsContainer(id=\"Columns\")\nclass LayoutApp(App):\ndef on_ready(self) -> None:\nself.push_screen(TweetScreen())\nif __name__ == \"__main__\":\napp = LayoutApp()\napp.run()\n
  1. Here's where we set the width and height to 1fr. We also add a border just to illustrate the dimensions better.

LayoutApp #Header \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502#Columns\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 #Footer

As you can see, the central Columns area will resize with the terminal window.

"},{"location":"how-to/design-a-layout/#tip-5-use-containers","title":"Tip 5. Use containers","text":"

Before we add content to the Columns area, we have an opportunity to simplify. Rather than extend Placeholder for our ColumnsContainer widget, we can use one of the builtin containers. A container is simply a widget designed to contain other widgets. Containers are styled with fr units to fill the remaining space so we won't need to add any more CSS.

Let's replace the ColumnsContainer class in the previous example with a HorizontalScroll container, which also adds an automatic horizontal scrollbar.

layout04.pyOutput
from textual.app import App, ComposeResult\nfrom textual.containers import HorizontalScroll\nfrom textual.screen import Screen\nfrom textual.widgets import Placeholder\nclass Header(Placeholder):\nDEFAULT_CSS = \"\"\"\n    Header {\n        height: 3;\n        dock: top;\n    }\n    \"\"\"\nclass Footer(Placeholder):\nDEFAULT_CSS = \"\"\"\n    Footer {\n        height: 3;\n        dock: bottom;\n    }\n    \"\"\"\nclass TweetScreen(Screen):\ndef compose(self) -> ComposeResult:\nyield Header(id=\"Header\")\nyield Footer(id=\"Footer\")\nyield HorizontalScroll()  # (1)!\nclass LayoutApp(App):\ndef on_ready(self) -> None:\nself.push_screen(TweetScreen())\nif __name__ == \"__main__\":\napp = LayoutApp()\napp.run()\n
  1. The builtin container widget.

LayoutApp #Header #Footer

The container will appear as blank space until we add some widgets to it.

Let's add the columns to the HorizontalScroll. A column is itself a container which will have a vertical scrollbar, so we will define our Column by subclassing VerticalScroll. In a real app, these columns will likely be added dynamically from some kind of configuration, but let's add 4 to visualize the layout.

We will also define a Tweet placeholder and add a few to each column.

layout05.pyOutput
from textual.app import App, ComposeResult\nfrom textual.containers import HorizontalScroll, VerticalScroll\nfrom textual.screen import Screen\nfrom textual.widgets import Placeholder\nclass Header(Placeholder):\nDEFAULT_CSS = \"\"\"\n    Header {\n        height: 3;\n        dock: top;\n    }\n    \"\"\"\nclass Footer(Placeholder):\nDEFAULT_CSS = \"\"\"\n    Footer {\n        height: 3;\n        dock: bottom;\n    }\n    \"\"\"\nclass Tweet(Placeholder):\npass\nclass Column(VerticalScroll):\ndef compose(self) -> ComposeResult:\nfor tweet_no in range(1, 20):\nyield Tweet(id=f\"Tweet{tweet_no}\")\nclass TweetScreen(Screen):\ndef compose(self) -> ComposeResult:\nyield Header(id=\"Header\")\nyield Footer(id=\"Footer\")\nwith HorizontalScroll():\nyield Column()\nyield Column()\nyield Column()\nyield Column()\nclass LayoutApp(App):\ndef on_ready(self) -> None:\nself.push_screen(TweetScreen())\nif __name__ == \"__main__\":\napp = LayoutApp()\napp.run()\n

LayoutApp #Header #Tweet1#Tweet1#Tweet1#Tweet1 #Footer

Note from the output that each Column takes a quarter of the screen width. This happens because Column extends a container which has a width of 1fr.

It makes more sense for a column in a Twitter / Mastodon client to use a fixed width. Let's set the width of the columns to 32.

We also want to reduce the height of each \"tweet\". In the real app, you might set the height to \"auto\" so it fits the content, but lets set it to 5 lines for now.

Here's the final example and a reminder of the sketch.

layout06.pyOutputSketch
from textual.app import App, ComposeResult\nfrom textual.containers import HorizontalScroll, VerticalScroll\nfrom textual.screen import Screen\nfrom textual.widgets import Placeholder\nclass Header(Placeholder):\nDEFAULT_CSS = \"\"\"\n    Header {\n        height: 3;\n        dock: top;\n    }\n    \"\"\"\nclass Footer(Placeholder):\nDEFAULT_CSS = \"\"\"\n    Footer {\n        height: 3;\n        dock: bottom;\n    }\n    \"\"\"\nclass Tweet(Placeholder):\nDEFAULT_CSS = \"\"\"\n    Tweet {\n        height: 5;\n        width: 1fr;\n        border: tall $background;\n    }\n    \"\"\"\nclass Column(VerticalScroll):\nDEFAULT_CSS = \"\"\"\n    Column {\n        height: 1fr;\n        width: 32;\n        margin: 0 2;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nfor tweet_no in range(1, 20):\nyield Tweet(id=f\"Tweet{tweet_no}\")\nclass TweetScreen(Screen):\ndef compose(self) -> ComposeResult:\nyield Header(id=\"Header\")\nyield Footer(id=\"Footer\")\nwith HorizontalScroll():\nyield Column()\nyield Column()\nyield Column()\nyield Column()\nclass LayoutApp(App):\ndef on_ready(self) -> None:\nself.push_screen(TweetScreen())\nif __name__ == \"__main__\":\napp = LayoutApp()\napp.run()\n

LayoutApp #Header \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e\u258a\u258e\u258a\u258e \u258a#Tweet1\u258e\u258a#Tweet1\u258e\u258a#Tweet1\u258e \u258a\u258e\u258a\u258e\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e\u2583\u2583\u258a\u258e\u2583\u2583\u258a\u258e \u258a#Tweet2\u258e\u258a#Tweet2\u258e\u258a#Tweet2\u258e \u258a\u258e\u258a\u258e\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e\u258a\u258e\u258a\u258e \u258a#Tweet3\u258e\u258a#Tweet3\u258e\u258a#Tweet3\u258e \u258a\u258e\u258a\u258e\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e\u258a\u258e\u258a\u258e \u258a#Tweet4\u258e\u258a#Tweet4\u258e\u258a#Tweet4\u258e \u258a\u258e\u258a\u258e\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e\u258a\u258e\u258a\u258e \u258a#Tweet5\u258e\u258a#Tweet5\u258e\u258a#Tweet5\u258e \u258a\u258e\u258a\u258e\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258c #Footer

eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1daW9cdTAwMWK7kv1+f0WQ92VcdTAwMDa46ktcdTAwMTbJXCJ5gcHAi1x1MDAxY1mOl8iKt8GDoaUtyda+eHvIf5+ivKjVUsstW7JbuTaQxGktzWafOudcdTAwMTRZTf7njy9fvvbv2v7Xv7989W9LhXqt3C3cfP3TXHUwMDFkv/a7vVqrSS/B8P+91qBbXHUwMDFhvrPa77d7f//1V6PQvfL77Xqh5HvXtd6gUO/1XHUwMDA35VrLK7VcdTAwMWF/1fp+o/e/7u+9QsP/n3arUe53vdFJUn651m91XHUwMDFmzuXX/Ybf7Pfo2/+P/v/ly3+Gf1x1MDAwN1rX9Uv9QrNS94dcdTAwMWZcdTAwMTi+NGogZ0aFXHUwMDBm77Waw9ZyLoxGxVxyPL+j1tukXHUwMDEz9v0yvXxBjfZHr7hDX1x1MDAwZnL9dT+3q/ay56WbXCK2djHdXHUwMDFkjM57UavXXHUwMDBm+3f1YbtK3Vavl6pcdTAwMTb6peroXHUwMDFkvX63deVcdTAwMWbXyv3qU/dcdTAwMDWOP3+216KuXHUwMDE4farbXHUwMDFhVKpNv+d6gT9cdTAwMWZttVx1MDAwYqVa/254lez56ENX/P1ldOSW/pdC4Vx0LY1VqLW1KtAr7lx1MDAwYkAoXHUwMDBmhFx1MDAxNKhcdTAwMDVarahfQi3baNXpjlDL/sWGP6O2XHUwMDE1XHUwMDBipatcbjWwWX5+T79baPbahS7dt9H7blx1MDAxZa9ZSemhcCdjinM+akfVr1WqfXqHQOZcdTAwMTnOrJQohLs9OGqMP7w1XHUwMDFjjNCopVx1MDAxNs+vuCa0t8tDnPw72HHN8mPHNVx1MDAwN/X6qNXuhXRcdTAwMDBbo89cZtrlwlx1MDAwM1x1MDAwMjhcdTAwMWFcdIpJ1Fxio1x1MDAwZanXmlfhr6u3Slcj0Fxmj/7681x1MDAxNXDVKlx1MDAxYa1WcVx1MDAwNIlcdTAwMThcdTAwMWKsuVLn5367sL63s3+Lza2fXHUwMDA3nZ3tTlx1MDAwNFhDgPtImFx1MDAxYcGVtIpxJjhBIIRT6Vx1MDAxOaUlgcJcdTAwMDJKi8vEqfBcdTAwMThHXHUwMDA1dD6urTaTQFx1MDAwNeMhMiFccjJAQaCeXHUwMDAwKt0zJYGuZvVw6tfrtXZvKkpxXHUwMDA2p1x1MDAxYZCcI1cmNkzbP86OS+lstssvc9f5+3TqrtG8fVxyTPn7wVRxz1xuhdTxQljOQIbolDjOMCMl11x1MDAwZcqgw1x1MDAxMTRcdTAwMTdM/3VRUKBgXHUwMDEyolxceJJcdTAwMDEoi0D/SGPkJEY5eFxuKVKISFx1MDAxOVxiXHUwMDAxUocxyql9ilNcdTAwMWL5b1x1MDAwNVItbFx1MDAxNEi1QXfNXHUwMDEwX/dP1jrpu8ZB7qC4tnXT2upvna3jTcIxXG7KY9TdyLnhlsBcdTAwMDEhiFxuT1x1MDAxObRkXG4kUZhV8o1cdTAwMTAtMqaWXHUwMDA1Uc1cdTAwMTXpgeC/XHUwMDE5Qq2IQijnjJSFS81iQ7TeSbdTV1x1MDAxNexs1upXWLrrb53UXHUwMDBlk1xyUVx1MDAxMljgXHUwMDAyQWjBmWRWh1x1MDAxMFxujmQl3Vx1MDAxNVxyxLVv41BcdTAwMGVFY3BZXHUwMDAwpcsgqkcuV1DoZ+dPKlx1MDAxMqPu1iihXHUwMDAzb3hcdKLr37ZcbrsldbLV2V1vnt3+OL6pXHUwMDFjNJJcctFcdTAwMTTlXCKk4lZcdTAwMTFFcnBJyThGXHUwMDExXHQ+hFxmTTTKJcOlYFRJ5oym4Vx1MDAxNCnaQiBOXHUwMDAyOZMnNaekytphO0CFIUquTJGLNYrNXHUwMDAx0SewjOBcIlx1MDAxZY/8ikbu82dGn1x1MDAwZcCt79+O3HZcdTAwMDBcdTAwMWPd9mG1cbB9tj/YK+VzlbXWgVx1MDAxOdx9fX7fr8ffosJCXGImjTLWLiosxtpcdTAwMTmMXGJcdTAwMTNcdTAwMTVcdTAwMTCczs5dvqBiR8T0i55cdTAwMWVcdTAwMTHVQqk66PpcdTAwMWZcdTAwMWZcdTAwMTNcXCO5W+JKQIK+YTLE21x1MDAxYTxcdTAwMDdB4nNcdTAwMTlcbpnXXHUwMDA0XHUwMDA1xVx1MDAxZbuYYn4p8oBcdTAwMTPaXHUwMDE5p1x1MDAwZSeRmJafTVx1MDAwNoFcdTAwMDWuyZ3PXHUwMDE1XHUwMDA0r+Pp11x1MDAwMHJ041vN/mHt3vU9sLGjW4VGrX43du+GSKWuyviFst/9OnZ8rV6rONR+LVF7gy9cdTAwMTJ2+7VSof78hkatXFxcdTAwMGVqQIlOVqg1/e52XHUwMDFj7m51a5Vas1DPR7SFrt3PPOupXHUwMDE3uDPFQs93r1x1MDAwZVx1MDAwM+j1Qlx1MDAwNVx1MDAxOJmTkns0QltcdTAwMTN/5GRtc6O+M2Bddn/XuK/YfL5/lc68LizfcexEXHUwMDE5j5NRkoxsfUhcdTAwMDPcN1xihp7ipGQ8XHUwMDE4XHUwMDE3XHUwMDBiXHUwMDFlNeGKuEFScvEkiKPrXHUwMDFmhSX5KbCGXHUwMDFisnXKqCA/PLkp0IZy6sDgz1x1MDAxMt2UJt5cdTAwMGVkP0t1U1x1MDAwMapcZouHM/zW5fmxUdqpbbT2sr3NXGZ2N1x1MDAwZTK57av9evpV43vvOXBcIjy6t+hcdTAwMTJwMis6hFBuKCdcdTAwMTX0gmIhYLzGTalCoXgh5TTPzz1GSIUpg89Selx1MDAxMjSzj01cYjbxXHUwMDExnUJcdTAwMWKlmVx0XGZcYr5cdTAwMTWcrzNM2Xbnvt45Ov/x48f5Sf3i6vgwfXxcdTAwMWMwTH9O/9qHXHUwMDBm51t7lUIu5Z9095pH9lx1MDAxMCv1y+1cdTAwMWbjZ3k6f6Hbbd3MYcQ4XHUwMDAwhZRQi4qoSCNcdTAwMTY5vkMmjGhcdTAwMGWtiZ+aTO/MhFx1MDAxYrFcdTAwMTRcdTAwMTGpIOtDySBlXHUwMDA1aMLhJJin6bBloFxirnYh2cm0eJLW46QsmnJBy6ZGVcDEPI2MazJvxr5cdTAwMDPFXHUwMDBiSpjIi1x1MDAwNYj1dUaM49jRXHUwMDE5Rix/4/v9JfmwXHUwMDE3SD/sw0JNiWfD5Fx1MDAxYmyYgPDR5/FcdTAwMDLBXGYjb1x1MDAxMt+GVXnaNrYvdzqp6lx1MDAwNuxfyrPTn0onX+BcZnJcdTAwMDGGh+1cdTAwMTdcdTAwMWGPRIVcdTAwMDJEaGCaiVCbPsUteOu3mtlccr9/36hWz5pq/7qU7drcSXxcdTAwMTFcdTAwMDLFKVx1MDAwMdNq+aNcdTAwMDFcdTAwMDFWXGKrkLbcXHUwMDE4w+dcdTAwMTggm37VK6BCJD+KSWHleHYxxL1hXHUwMDFlJSXyzXnHylx1MDAwYpCYXHUwMDAzjMlcdTAwMTWgXHUwMDE3SPnDXHUwMDA1yLLw0ZEtVJJroUT8uen1k5udY5VLnzXRXFzdf9va6ejNXtJcdTAwMTWISN5cdTAwMDBcdC03XHUwMDFjJ4dcdTAwMDEkMOp24iVOIYtGvNFcdTAwMTT+5jqUXHUwMDFmXFyv5fa+X/LU/u3deW692Pl29X1OXHUwMDFkUjJQXHUwMDE4sixcdTAwMWTSkb5cdTAwMGKYkFxcQrBW5iXYT7/qxOuQ8JRigCBcdTAwMTXnlIKG0iHiYU9qXHUwMDEwlFx1MDAwYjFBXHUwMDE27Y1cdTAwMDbsU4xcdTAwMTIgRi/w80eLkTA8fPQpKlxySM1cdTAwMDH1aIzrpaBs9b5cdTAwMWSet9bWe2epn+1jW7lnJ7yQdC1SXHUwMDFlkTjprlx1MDAxNVO1SLteXHUwMDE3Slx1MDAxYm6QXHTFlydGU8ah5cTAc1JU5yB9WG9uXHUwMDFmVlx1MDAwN/5ajde+dUSnXdqZU3U0LF91Zk26kOxcdTAwMDCoOZKf6Vx1MDAxN71cdTAwMDKig0hcdTAwMTIvNJsqOkYsXHUwMDE04Z+qk1x1MDAwMNV5gYk/WnU4RqpcdTAwMGVcdTAwMDHEWUGr45codPZcdTAwMGWvXHUwMDFi6Hc6WXPon9rDwkb7eDPZsmOYp13dNVk9YVx0fCM6f5hlolBcdTAwMTGCXCKTQmk8Yl9cdTAwMTOSXHUwMDAwpuhPKywzXHUwMDFlc1Vj04ORXGLa01x1MDAxOJ6MfZz+ZHTcXG7BR/q0xFwiXHUwMDA1VFxc2PcpJlx1MDAwM1x1MDAxYpmkcCGlq+Ww8f3Qjl83379l6rtcdTAwMTfqqr2pdLfWbq4nXHUwMDFimK6aXGaBOdplXHUwMDE2UYcqXHUwMDFlldJcdTAwMWWlxXRYg5VcdTAwMTbeVvFooGS5P1x0zH9ENVmnWMzk8T6T7V3ft3WuslNax/R8XHUwMDBlytBcdTAwMWTgi4qLyLxcdTAwMWRlVEhoXHUwMDAzwDmzc1D11ItOuIPiWntcdTAwMTKtq8IgymMqVE2mUHiMoXtcdTAwMDe5TXzjXHUwMDEwcmQxmSTAWyvoNCBcZlx1MDAwNNK2hFx1MDAxNJMtyD/FLybbarX6Sysme4G6w1x1MDAwNirclqVcdTAwMTeTXHUwMDA1XHUwMDA3JENRKSkkiacxvk5cdTAwMWSfyduLTnFXZHbSm5mN3Lp/1j9KejFcdTAwMTnnnPyJtGjMw2zl6GtcdTAwMWVqySxFpXJPOHF8c43nSlaUTdelx6Gajla63vJ57fREXFyKrl/cuD1fWHVccmk2W5gwzTZsMnp2k9hcdTAwMWHcJEL86fxcdTAwMDNd6N521i94tre/0Thkx1x1MDAwN+e902RcdTAwMWI2QrdngFx1MDAxMftCuJZ5XHUwMDE4XHUwMDA2wDyrjEW7iDD47SdT1vKnu9vbP1x1MDAwZnY2+1x1MDAxOdk46lx1MDAxZmyd5W3cirXs9o+LU6wp1chu9ZrXR5XCzdXu4irW3NTg0s1cdTAwMWXMKP90s0RcdTAwMWGZjVx1MDAxZE7TezPpbo9SXHUwMDFjrYiOXHUwMDE5Ob6JXHUwMDA0SFxi4ZGlUlxcPZYwvy2cPlx1MDAwN8timr1lXHUwMDBllr3A+lx1MDAxZj1YJmT0xKlcIoiilvGHsEV3/fJ252fZ7MHZcVVV8VStdZOvcKRvJOVcdTAwMTiWsGFEalI4So0oXHUwMDAzs1x1MDAwYnjA7TdcdTAwMTe4/PptrWvWXHUwMDBlb1tcdTAwMWIoikff7zavXHUwMDBi+3M9wyZcdTAwMTRcYlxc+qhDcGooLEQgmUBleXzUT7/qxFx1MDAwYlx1MDAxMXpcdTAwMTZdMkdJ/6RcdTAwMTBcdTAwMTlcdTAwMTJcIvc4r3vY9+3FmqsqRK/AY3KF6Fx1MDAwNXL+cCGy0c8zUL5H2TaK+KtcdTAwMDGcsfNO6qJvMsfVtZ3Uxam6zlx1MDAwNzLQhCqR8lx1MDAxNFx1MDAwNzR0qUJPKJFcdTAwMDTwXGZl92RcdTAwMWQ1uHj5VKJcdTAwMTlKxLLp0rbMs61Byp5cdTAwMWOcXHUwMDE2JWtcdTAwMTVcdTAwMDbzjX9LYiZcdTAwMTbE/1KUKLpCXHUwMDA2NWHBTVx1MDAwYsVcdTAwMDb99IteXHUwMDAxIZJuXHUwMDEwmaDjVlx1MDAxYVx1MDAxOFx1MDAxN1wiKZTHkFx1MDAwMGdcZqi3V2uuqlx1MDAxMP1WXHUwMDE50Vx1MDAwYtz80UIkZ1RQk1WSXHUwMDA20cZcdTAwMTeiXvNO+er7fjs/yFxmWje8t7X5bTfpQiQ98r9aXHUwMDAyQ+VcbmdHXGb0ULQmSIiY0kYwMz5h9Y8uWjtcdTAwMTPrezc3crN4spMvpG4vq7uVg0pcdTAwMDIlR+jIKVeyIHS/jOXxNWf6Va+A5lxi4chcdTAwMWXc8LWRIatl1EJcdTAwMTG+4qozz9pcdTAwMDDJVZ1cdTAwMTeI+KNVh7xeVFSCMlxiwOJPNG01N3c7W6ksbMP5oLnm64vNb1FcdTAwMDNcdTAwMTJcdNFcdTAwMWNQzFOMXHUwMDFiTVx1MDAxZW+yXGJCXHUwMDAwp9RILWpJnZUuWVx1MDAxYq7Ywd6rZE1gdMlcdTAwMWFcdTAwMThB3oDJ+KVcdTAwMDCbOyeH7eyp3fyW/nFweqK+i3w/asWOxJRcdTAwMDLQZXqKYFx1MDAwMUrgtFx1MDAxMWLOPFSWhJPDm59r/u0qXHUwMDAxdvs3p8Wj01x1MDAxY5rMnbzoraW3blpnXHUwMDBim7WUYFx1MDAwMlx1MDAxNfhLrVx1MDAwNFx1MDAxMJFcdTAwMTUxnJO2U0Ns/JVrjptnxVRd18o1xnJZOEinipuphFx1MDAxM7QxRNBaI2pwSz2HaopcdTAwMDE8gr5WXGa5q/JXn/Mks1KFa57ezd3traV+yPtTuGfFjctKMW4hwPFdeW3ztsVcdTAwMGYzuWo5n8FBrnmbW1xcIYCVbOkpyMxVXHUwMDAztNKcXHUwMDFiXHUwMDFi/2nN6b2Z8Fx1MDAxNITMtSeFVdZcdTAwMTlszoJcdTAwMTHzUFx04HJw7Vx1MDAxYziGXHUwMDE20/hMQVY1XHUwMDA1eYH2PzpcdTAwMDVcdTAwMTFcInLgXHUwMDBiKE92i1xixlx1MDAxZvdK93aE/H7q31xcXHUwMDFmd1x1MDAwYmv3ftpcdTAwMWVVo8aiXHUwMDEzI3HaXHUwMDE1u1lheFjEXHUwMDFlSlx1MDAwMcAjkUelXHUwMDFmpkTDXHL7lLjg/b+6YoNGZuOuVsZu75JlZfv84GLO0TD3Z9lSXHUwMDA0PPpcdTAwMDFcdTAwMDRAVFx1MDAwNIb4XHUwMDBmIEy/6MQrkXElaVx1MDAxYbmRjPAvxlFvSIhIXHUwMDE4mISQ7/vUoVXVoVx1MDAxN7j5w3XIRJaJUrRcbuJnmGNcdJutVvU8ZVtHfnX/JNPM1lxuulHNJl+IXGJtQqCSROdhIZIgPWRMuVx1MDAxZFjQ2OC04KdcdTAwMTJNKtH29fqV8Sv5Sr3Fi6Zlv12y9LzzMu+iRFx1MDAxOF1cdTAwMDAjlVuxiFx1MDAwNYpcdTAwMDVegv30q15cdTAwMDEpcvVWyEBcdTAwMWJcdTAwMTGWXCIp0DPcjXxcdG5ccshFbIjxKUVcdTAwMWYrRS+w80dLkdSRUoSKvJLSc9Sk2SNbNlBLr/XyV6XizV1e/cgmfCVcdTAwMDEw6GlGcWCUskqHXHUwMDAzUitPUpxQsqRcdTAwMDAkLi8jWqlCgFx1MDAxMzi9zJTklrxs71c3dnZz+6U7lUDBXHUwMDEx0etkSPdwXHUwMDFiSX98vZl+0SugN1x1MDAxY6zbeskyJoIrRD/UXHUwMDAx6EXi+1NwXHUwMDEyIDgvkPBHXHUwMDBiXHUwMDBlRG+AqrVGXHUwMDAwruOnPt30UZrt7O83hTpJy8Za5uw+9zPZguOqnIGSXG5BXHUwMDA0NNysYywgXHUwMDA1uMdcdTAwMTJcdTAwMDJjcG9cbsjVL1x1MDAwM1x1MDAxMDqQXGIvtVxmQEfv+oRCgWVyjpVr9lmrv5nL+oY31/X6xe4mXjRcdTAwMTO/qix6buEowp52i5mFXHUwMDE28qNcdTAwMWXwrFvv+nFcdTAwMTW0t42SRVx1MDAwMVO6labdhlNuq1x1MDAwM/c1OFx0T5Bu116SXHUwMDExPVxcx1x1MDAwMyZcdTAwMGJcdTAwMDDeXHUwMDE3o1x1MDAxMoRcXPoq5DNYU7n1XHUwMDAyOJujiv460ylmXHUwMDFh9udm+eLozFx1MDAxN1x1MDAwNzlTgGbSnVxmXHUwMDExJlx1MDAwMVx1MDAxMNFwI1x1MDAxMVhgfYSHrSS14yxOyTORhjViedtGk5eR5ExcdTAwMTRcYlx1MDAwYqFAmbGUXGY1yY1wwLKt+9KLUlx1MDAxNuSV5linpnbrlyO8Ut2/6M9wSv1WO8omjTV3Yjma8VMuYjWa6MdjXCLHxITbbFCjiV8nUDXr+3fn1fIgla1s5Vx1MDAxYq3LuzxL/OxcZoWFR1xcL4XVhlEqMl58hpx0h6SXQKeMU55/dFxc58hcdTAwMTZcdTAwMTbyV2dcXF9cdTAwMWNuX36HUjrTb3/G9Vx1MDAwN8b1QzdPM5Qztlx1MDAxMVVuu2vkMv7I2mxCT2pkg/bc9ijKKKVItsdcdTAwMWQloPBcdTAwMDSS41x1MDAxNNxcIuCbxrpnXHUwMDA2tmWeNW5FVMtcdTAwMTRcdTAwMTnHXHUwMDEx31x1MDAwNi0loqA2MOOe0MNcdEvJiXtcdTAwMTSToOdcdTAwMTmMmFxcXGZcdTAwMTFcdTAwMWWPzFhcZnFG5Zog9tOv2my01y90++u1ZrnWrIw3zH849XZcZl84jOjSwLWSeaApYXLzXHUwMDE0bjhSju6s65hC2/G2Ry6c2lxmlj9MsE1cXLrfLL/cpNn11IEmpZhnXHUwMDE0pztkhit6XHUwMDE4hWKiTZR5o9vcW5I9dONQdqJN9UKvv9FqNGp96vmDVq3ZXHUwMDBm9/CwK9dcXNBX/cJcdTAwMDTh0DVcdTAwMDVfXHUwMDBis0PbfeM4yY9++zKKnuF/nn//959T352KwrX7mUD06Nv+XGL+Ozet2ehHq1xmUr9Solx1MDAxY3/4ZracJZXVXHUwMDE0o1x1MDAwNJQpUIbwL9n4g1x1MDAxY1xi3ONusFx1MDAwMJlcdTAwMTaISyyWt8JDbYZLW4xtXGY4mj/gXHUwMDFlXHUwMDEzlFxmW0roXHRcdTAwMWWT01x0XHUwMDFjKWWiTJnNY15cdTAwMTbOasxcYnzVY78xWW22KVx1MDAxZadcdTAwMTBiNNdXXGJARKLZiO6fOcR40m1cdTAwMDIurGTuwVFjZvPa+HWsXHUwMDE0u0yH1/C1XHRkzckukfM1NnIyUlx0NG5p8/iL1lx1MDAxY1X29zZQ9vzaXHUwMDFl+Nnvt53Dw42bpJNLSqJLd4hBrEIlJIyzXHUwMDBiV+5JXHUwMDFkrVxyMolumONNw3Czn8Ux2uNCXHUwMDE5lNHZ0MScXHK3lFxyKVx1MDAwN4x3SYfesMftn7O+91xyK1x1MDAxMc783iU+2OD2one9/8b0Lf5cdTAwMTRcdTAwMTahaNBo9r7811N29qVX6rbq9f9+35QuRjNcdTAwMTYxpVx1MDAxNemHzIxcdTAwMDc9lFtseq5nzWdcdTAwMDM6oZwljEdGXHUwMDEzrHFcdTAwMWJccihcdTAwMDXjXHUwMDAzOI5HXHUwMDE0XHUwMDEzRltcdTAwMTKS5Y7MkunVJM1cbshccmshg9tCjCa3QHiGh2r+nthcdTAwMGKcnVx1MDAxYptSeG8/5MLY7dm3RD80W1x1MDAxN8ezPNBkcyVKpa1Ukk2meZy6XFxcInPVkbONUFRrZu92PO7OXHUwMDE4Uk5jQIOWXG5IlfREczQhkfwsvW4lXHUwMDEycuVEo1bKg0VcdTAwMDPa/Vx1MDAwNKE8p1x1MDAwMYukM2TRdCY510zMs13BbFx1MDAxZE0onVnjaW5cdGSEXCJcdCZUmsxccvfc7CRcYiWXTGfaeJRmSLe/MFx1MDAxMavk09hMoEfo0I55hzvdXHUwMDA0nkp95LRhnVx1MDAwMVx1MDAwNFx1MDAxN194f05T1Ly5dtpYXHUwMDFhp1x1MDAxMYtI4G6tU1wiXG6plbTTSMRyx2jSXHUwMDBlf8zkMFEsZpu9LO5cdTAwMTjPckKVXHUwMDE0YIxcdTAwMDX6VUzSLHiUhJEyuM50T2/halx1MDAxM1skst3PJKZcdTAwMTdGbjxyXHUwMDEynVTOclxu9vjcNtvLJ5TbXGI4XHUwMDFlKkBKLCWA0eOT6NxQz1x1MDAxM945d0umkl1b3pg8R+5m891CXHUwMDBlklx1MDAwMlLBlEJcdTAwMGbKOSlYJcWhXHUwMDFlrvRcdTAwMTBMdlx1MDAxZshNKtRqrlxc8/d1a47ZyPRcZvdop0ZJXHUwMDE2XHUwMDFjXHUwMDEwfnJrZFx1MDAxOEjFXHTsZlg680rPNvsp33FmQ+ZG+MDtduVOOTko71x1MDAxNlx1MDAxNzFuSS73yFx1MDAwMzVcdHGiUatEbdHAXHUwMDFlvjpcdTAwMDHpObkter3nSNvGnfZqPUel88Fpo8GuQF9tnaX3i93SbvbnbtRcIueJoTaNnuSkXHUwMDE3XHUwMDA0fjfXKMeX70CiNiNcZqM/Llx1MDAwZmVLXHUwMDFjN1NIpt1cdTAwMWHlRuhcZuppS9hMKatcdTAwMTSMzLVcclxc5YruXvNYRvDWcaj4ZVx1MDAwNNVWt3bvxoueRn7ed/xpxumXWl5cdTAwMTCoXHUwMDFiXHUwMDBll1xyKcvA7VpcdTAwMTY73mfDIaHxzqX2iF6F21x1MDAwN5TcitLjhdSopVx1MDAwN4xcdTAwMWOkojcuN+Ct52qDrZ3Ykz7gZKwnnVx1MDAwMNqIkSdtXHUwMDE5XHSHlfOE/6In4ubedWpeKzNbVr6EJuKoP9zjXCJ0f8mowlx1MDAxNN/gJvNcdTAwMWZ2d32li5m5a9uYi2Hu4Vx1MDAxOMVcdTAwMDRnROtcdTAwMDKnjIOBoFx1MDAxYqxcdTAwMWTna5fIMTZ5L1bKxURh2v2kJuBcdTAwMWNlYv54PMHXQrt92Ce4Pd9cdTAwMGXCd638yPWjq/x6XfNv1qfUi19cZn+cXGZccvvTUZI/jIFff/z6f3dfuVAifQ== HeaderTweetTweetTweetTweetFooterTweetTweetTweetTweetTweetTweetTweetTweetFixedFixedColumns (vertical scroll)horizontal scroll

A layout like this is a great starting point. In a real app, you would start replacing each of the placeholders with builtin or custom widgets.

"},{"location":"how-to/design-a-layout/#summary","title":"Summary","text":"

Layout is the first thing you will tackle when building a Textual app. The following tips will help you get started.

  1. Make a sketch (pen and paper is fine).
  2. Work outside in. Start with the entire space of the terminal, add the outermost content first.
  3. Dock fixed widgets. If the content doesn't move or scroll, you probably want to dock it.
  4. Make use of fr for flexible space within layouts.
  5. Use containers to contain other widgets, particularly if they scroll!

If you need further help, we are here to help.

"},{"location":"reference/","title":"Reference","text":"

Welcome to the Textual Reference.

  • CSS Types

    CSS Types are the data types that CSS styles accept in their rules.

    CSS Types Reference

  • Events

    Events are how Textual communicates with your application.

    Events Reference

  • Styles

    All the styles you can use to take your Textual app to the next level.

    Styles Reference

  • Widgets

    How to use the many widgets builtin to Textual.

    Widgets Reference

"},{"location":"styles/","title":"Styles","text":"

A reference to Widget styles.

See the links to the left of the page, or in the hamburger menu (three horizontal bars, top left).

"},{"location":"styles/align/","title":"Align","text":"

The align style aligns children within a container.

"},{"location":"styles/align/#syntax","title":"Syntax","text":"
\nalign: <horizontal> <vertical>;\n\nalign-horizontal: <horizontal>;\nalign-vertical: <vertical>;\n

The align style takes a <horizontal> followed by a <vertical>.

You can also set the alignment for each axis individually with align-horizontal and align-vertical.

"},{"location":"styles/align/#examples","title":"Examples","text":""},{"location":"styles/align/#basic-usage","title":"Basic usage","text":"

This example contains a simple app with two labels centered on the screen with align: center middle;:

Outputalign.pyalign.tcss

AlignApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503Vertical\u00a0alignment\u00a0with\u00a0Textual\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503Take\u00a0note,\u00a0browsers.\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

from textual.app import App\nfrom textual.widgets import Label\nclass AlignApp(App):\ndef compose(self):\nyield Label(\"Vertical alignment with [b]Textual[/]\", classes=\"box\")\nyield Label(\"Take note, browsers.\", classes=\"box\")\napp = AlignApp(css_path=\"align.tcss\")\n
Screen {\nalign: center middle;\n}\n.box {\nwidth: 40;\nheight: 5;\nmargin: 1;\npadding: 1;\nbackground: green;\ncolor: white 90%;\nborder: heavy white;\n}\n
"},{"location":"styles/align/#all-alignments","title":"All alignments","text":"

The next example shows a 3 by 3 grid of containers with text labels. Each label has been aligned differently inside its container, and its text shows its align: ... value.

Outputalign_all.pyalign_all.tcss

AlignAllApp \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502left\u00a0top\u2502\u2502center\u00a0top\u2502\u2502right\u00a0top\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502left\u00a0middle\u2502\u2502center\u00a0middle\u2502\u2502right\u00a0middle\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502 \u2502left\u00a0bottom\u2502\u2502center\u00a0bottom\u2502\u2502right\u00a0bottom\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

from textual.app import App, ComposeResult\nfrom textual.containers import Container\nfrom textual.widgets import Label\nclass AlignAllApp(App):\n\"\"\"App that illustrates all alignments.\"\"\"\nCSS_PATH = \"align_all.tcss\"\ndef compose(self) -> ComposeResult:\nyield Container(Label(\"left top\"), id=\"left-top\")\nyield Container(Label(\"center top\"), id=\"center-top\")\nyield Container(Label(\"right top\"), id=\"right-top\")\nyield Container(Label(\"left middle\"), id=\"left-middle\")\nyield Container(Label(\"center middle\"), id=\"center-middle\")\nyield Container(Label(\"right middle\"), id=\"right-middle\")\nyield Container(Label(\"left bottom\"), id=\"left-bottom\")\nyield Container(Label(\"center bottom\"), id=\"center-bottom\")\nyield Container(Label(\"right bottom\"), id=\"right-bottom\")\n
#left-top {\n/* align: left top; this is the default value and is implied. */\n}\n#center-top {\nalign: center top;\n}\n#right-top {\nalign: right top;\n}\n#left-middle {\nalign: left middle;\n}\n#center-middle {\nalign: center middle;\n}\n#right-middle {\nalign: right middle;\n}\n#left-bottom {\nalign: left bottom;\n}\n#center-bottom {\nalign: center bottom;\n}\n#right-bottom {\nalign: right bottom;\n}\nScreen {\nlayout: grid;\ngrid-size: 3 3;\ngrid-gutter: 1;\n}\nContainer {\nbackground: $boost;\nborder: solid gray;\nheight: 100%;\n}\nLabel {\nwidth: auto;\nheight: 1;\nbackground: $accent;\n}\n
"},{"location":"styles/align/#css","title":"CSS","text":"
/* Align child widgets to the center. */\nalign: center middle;\n/* Align child widget to the top right */\nalign: right top;\n/* Change the horizontal alignment of the children of a widget */\nalign-horizontal: right;\n/* Change the vertical alignment of the children of a widget */\nalign-vertical: middle;\n
"},{"location":"styles/align/#python","title":"Python","text":"
# Align child widgets to the center\nwidget.styles.align = (\"center\", \"middle\")\n# Align child widgets to the top right\nwidget.styles.align = (\"right\", \"top\")\n# Change the horizontal alignment of the children of a widget\nwidget.styles.align_horizontal = \"right\"\n# Change the vertical alignment of the children of a widget\nwidget.styles.align_vertical = \"middle\"\n
"},{"location":"styles/align/#see-also","title":"See also","text":"
  • content-align to set the alignment of content inside a widget.
  • text-align to set the alignment of text in a widget.
"},{"location":"styles/background/","title":"Background","text":"

The background style sets the background color of a widget.

"},{"location":"styles/background/#syntax","title":"Syntax","text":"
\nbackground: <color> [<percentage>];\n

The background style requires a <color> optionally followed by <percentage> to specify the color's opacity (clamped between 0% and 100%).

"},{"location":"styles/background/#examples","title":"Examples","text":""},{"location":"styles/background/#basic-usage","title":"Basic usage","text":"

This example creates three widgets and applies a different background to each.

Outputbackground.pybackground.tcss

BackgroundApp Widget\u00a01 Widget\u00a02 Widget\u00a03

from textual.app import App\nfrom textual.widgets import Label\nclass BackgroundApp(App):\ndef compose(self):\nyield Label(\"Widget 1\", id=\"static1\")\nyield Label(\"Widget 2\", id=\"static2\")\nyield Label(\"Widget 3\", id=\"static3\")\napp = BackgroundApp(css_path=\"background.tcss\")\n
Label {\nwidth: 100%;\nheight: 1fr;\ncontent-align: center middle;\ncolor: white;\n}\n#static1 {\nbackground: red;\n}\n#static2 {\nbackground: rgb(0, 255, 0);\n}\n#static3 {\nbackground: hsl(240, 100%, 50%);\n}\n
"},{"location":"styles/background/#different-opacity-settings","title":"Different opacity settings","text":"

The next example creates ten widgets laid out side by side to show the effect of setting different percentages for the background color's opacity.

Outputbackground_transparency.pybackground_transparency.tcss

BackgroundTransparencyApp 10%20%30%40%50%60%70%80%90%100%

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass BackgroundTransparencyApp(App):\n\"\"\"Simple app to exemplify different transparency settings.\"\"\"\ndef compose(self) -> ComposeResult:\nyield Static(\"10%\", id=\"t10\")\nyield Static(\"20%\", id=\"t20\")\nyield Static(\"30%\", id=\"t30\")\nyield Static(\"40%\", id=\"t40\")\nyield Static(\"50%\", id=\"t50\")\nyield Static(\"60%\", id=\"t60\")\nyield Static(\"70%\", id=\"t70\")\nyield Static(\"80%\", id=\"t80\")\nyield Static(\"90%\", id=\"t90\")\nyield Static(\"100%\", id=\"t100\")\napp = BackgroundTransparencyApp(css_path=\"background_transparency.tcss\")\n
#t10 {\nbackground: red 10%;\n}\n#t20 {\nbackground: red 20%;\n}\n#t30 {\nbackground: red 30%;\n}\n#t40 {\nbackground: red 40%;\n}\n#t50 {\nbackground: red 50%;\n}\n#t60 {\nbackground: red 60%;\n}\n#t70 {\nbackground: red 70%;\n}\n#t80 {\nbackground: red 80%;\n}\n#t90 {\nbackground: red 90%;\n}\n#t100 {\nbackground: red 100%;\n}\nScreen {\nlayout: horizontal;\n}\nStatic {\nheight: 100%;\nwidth: 1fr;\ncontent-align: center middle;\n}\n
"},{"location":"styles/background/#css","title":"CSS","text":"
/* Blue background */\nbackground: blue;\n/* 20% red background */\nbackground: red 20%;\n/* RGB color */\nbackground: rgb(100, 120, 200);\n/* HSL color */\nbackground: hsl(290, 70%, 80%);\n
"},{"location":"styles/background/#python","title":"Python","text":"

You can use the same syntax as CSS, or explicitly set a Color object for finer-grained control.

# Set blue background\nwidget.styles.background = \"blue\"\n# Set through HSL model\nwidget.styles.background = \"hsl(351,32%,89%)\"\nfrom textual.color import Color\n# Set with a color object by parsing a string\nwidget.styles.background = Color.parse(\"pink\")\nwidget.styles.background = Color.parse(\"#FF00FF\")\n# Set with a color object instantiated directly\nwidget.styles.background = Color(120, 60, 100)\n
"},{"location":"styles/background/#see-also","title":"See also","text":"
  • color to set the color of text in a widget.
"},{"location":"styles/border/","title":"Border","text":"

The border style enables the drawing of a box around a widget.

A border style may also be applied to individual edges with border-top, border-right, border-bottom, and border-left.

Note

border and outline cannot coexist in the same edge of a widget.

"},{"location":"styles/border/#syntax","title":"Syntax","text":"
\nborder: [<border>] [<color>] [<percentage>];\n\nborder-top: [<border>] [<color>] [<percentage>];\nborder-right: [<border>] [<color> [<percentage>]];\nborder-bottom: [<border>] [<color> [<percentage>]];\nborder-left: [<border>] [<color> [<percentage>]];\n

In CSS, the border is set with a border style and a color. Both are optional. An optional percentage may be added to blend the border with the background color.

In Python, the border is set with a tuple of border style and a color.

"},{"location":"styles/border/#border-command","title":"Border command","text":"

The textual CLI has a subcommand which will let you explore the various border types interactively:

textual borders\n

Alternatively, you can see the examples below.

"},{"location":"styles/border/#examples","title":"Examples","text":""},{"location":"styles/border/#basic-usage","title":"Basic usage","text":"

This examples shows three widgets with different border styles.

Outputborder.pyborder.tcss

BorderApp \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502 \u2502My\u00a0border\u00a0is\u00a0solid\u00a0red\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250f\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u2513 \u254f\u254f \u254fMy\u00a0border\u00a0is\u00a0dashed\u00a0green\u254f \u254f\u254f \u2517\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u251b \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258aMy\u00a0border\u00a0is\u00a0tall\u00a0blue\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

from textual.app import App\nfrom textual.widgets import Label\nclass BorderApp(App):\ndef compose(self):\nyield Label(\"My border is solid red\", id=\"label1\")\nyield Label(\"My border is dashed green\", id=\"label2\")\nyield Label(\"My border is tall blue\", id=\"label3\")\napp = BorderApp(css_path=\"border.tcss\")\n
#label1 {\nbackground: red 20%;\ncolor: red;\nborder: solid red;\n}\n#label2 {\nbackground: green 20%;\ncolor: green;\nborder: dashed green;\n}\n#label3 {\nbackground: blue 20%;\ncolor: blue;\nborder: tall blue;\n}\nScreen {\nbackground: white;\n}\nScreen > Label {\nwidth: 100%;\nheight: 5;\ncontent-align: center middle;\ncolor: white;\nmargin: 1;\nbox-sizing: border-box;\n}\n
"},{"location":"styles/border/#all-border-types","title":"All border types","text":"

The next example shows a grid with all the available border types.

Outputborder_all.pyborder_all.tcss

AllBordersApp +------------------+\u250f\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u2513 |ascii|blank\u254fdashed\u254f +------------------+\u2517\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u251b \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2551double\u2551\u2503heavy\u2503hidden/none \u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d\u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2597\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2596\u259b\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u259c hkey\u2590inner\u258c\u258couter\u2590 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u259d\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2598\u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u2502round\u2502\u2502solid\u2502\u258atall\u258e \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u2588\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2588\u258f\u2595\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2588thick\u2588\u258fvkey\u2595\u258ewide\u258a \u2588\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2588\u258f\u2595\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Label\nclass AllBordersApp(App):\ndef compose(self):\nyield Grid(\nLabel(\"ascii\", id=\"ascii\"),\nLabel(\"blank\", id=\"blank\"),\nLabel(\"dashed\", id=\"dashed\"),\nLabel(\"double\", id=\"double\"),\nLabel(\"heavy\", id=\"heavy\"),\nLabel(\"hidden/none\", id=\"hidden\"),\nLabel(\"hkey\", id=\"hkey\"),\nLabel(\"inner\", id=\"inner\"),\nLabel(\"outer\", id=\"outer\"),\nLabel(\"round\", id=\"round\"),\nLabel(\"solid\", id=\"solid\"),\nLabel(\"tall\", id=\"tall\"),\nLabel(\"thick\", id=\"thick\"),\nLabel(\"vkey\", id=\"vkey\"),\nLabel(\"wide\", id=\"wide\"),\n)\napp = AllBordersApp(css_path=\"border_all.tcss\")\n
#ascii {\nborder: ascii $accent;\n}\n#blank {\nborder: blank $accent;\n}\n#dashed {\nborder: dashed $accent;\n}\n#double {\nborder: double $accent;\n}\n#heavy {\nborder: heavy $accent;\n}\n#hidden {\nborder: hidden $accent;\n}\n#hkey {\nborder: hkey $accent;\n}\n#inner {\nborder: inner $accent;\n}\n#outer {\nborder: outer $accent;\n}\n#round {\nborder: round $accent;\n}\n#solid {\nborder: solid $accent;\n}\n#tall {\nborder: tall $accent;\n}\n#thick {\nborder: thick $accent;\n}\n#vkey {\nborder: vkey $accent;\n}\n#wide {\nborder: wide $accent;\n}\nGrid {\ngrid-size: 3 5;\nalign: center middle;\ngrid-gutter: 1 2;\n}\nLabel {\nwidth: 20;\nheight: 3;\ncontent-align: center middle;\n}\n
"},{"location":"styles/border/#borders-and-outlines","title":"Borders and outlines","text":"

The next example makes the difference between border and outline clearer by having three labels side-by-side. They contain the same text, have the same width and height, and are styled exactly the same up to their border and outline styles.

This example also shows that a widget cannot contain both a border and an outline:

Outputoutline_vs_border.pyoutline_vs_border.tcss

OutlineBorderApp \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502ear\u00a0is\u00a0the\u00a0mind-killer.\u2502 \u2502ear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2502 \u2502\u00a0will\u00a0face\u00a0my\u00a0fear.\u2502 \u2502\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2502 \u2502nd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path\u2502 \u2502here\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503I\u00a0must\u00a0not\u00a0fear.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0mind-killer.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2503 \u2503I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2503 \u2503I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2503 \u2503And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502I\u00a0must\u00a0not\u00a0fear.\u2502 \u2502Fear\u00a0is\u00a0the\u00a0mind-killer.\u2502 \u2502Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2502 \u2502I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2502 \u2502I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2502 \u2502And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

from textual.app import App\nfrom textual.widgets import Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass OutlineBorderApp(App):\ndef compose(self):\nyield Label(TEXT, classes=\"outline\")\nyield Label(TEXT, classes=\"border\")\nyield Label(TEXT, classes=\"outline border\")\napp = OutlineBorderApp(css_path=\"outline_vs_border.tcss\")\n
Label {\nheight: 8;\n}\n.outline {\noutline: $error round;\n}\n.border {\nborder: $success heavy;\n}\n
"},{"location":"styles/border/#css","title":"CSS","text":"
/* Set a heavy white border */\nborder: heavy white;\n/* Set a red border on the left */\nborder-left: outer red;\n/* Set a rounded orange border, 50% opacity. */\nborder: round orange 50%;\n
"},{"location":"styles/border/#python","title":"Python","text":"
# Set a heavy white border\nwidget.styles.border = (\"heavy\", \"white\")\n# Set a red border on the left\nwidget.styles.border_left = (\"outer\", \"red\")\n
"},{"location":"styles/border/#see-also","title":"See also","text":"
  • box-sizing to specify how to account for the border in a widget's dimensions.
  • outline to add an outline around the content of a widget.
  • border-title-align to set the title's alignment.
  • border-title-color to set the title's color.
  • border-title-background to set the title's background color.
  • border-title-style to set the title's text style.

  • border-subtitle-align to set the sub-title's alignment.

  • border-subtitle-color to set the sub-title's color.
  • border-subtitle-background to set the sub-title's background color.
  • border-subtitle-style to set the sub-title's text style.
"},{"location":"styles/border_subtitle_align/","title":"Border-subtitle-align","text":"

The border-subtitle-align style sets the horizontal alignment for the border subtitle.

"},{"location":"styles/border_subtitle_align/#syntax","title":"Syntax","text":"
\nborder-subtitle-align: <horizontal>;\n

The border-subtitle-align style takes a <horizontal> that determines where the border subtitle is aligned along the top edge of the border. This means that the border corners are always visible.

"},{"location":"styles/border_subtitle_align/#default","title":"Default","text":"

The default alignment is right.

"},{"location":"styles/border_subtitle_align/#examples","title":"Examples","text":""},{"location":"styles/border_subtitle_align/#basic-usage","title":"Basic usage","text":"

This example shows three labels, each with a different border subtitle alignment:

Outputborder_subtitle_align.pyborder_subtitle_align.tcss

BorderSubtitleAlignApp \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502 \u2502My\u00a0subtitle\u00a0is\u00a0on\u00a0the\u00a0left.\u2502 \u2502\u2502 \u2514\u2500\u00a0<\u00a0Left\u00a0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250f\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u2513 \u254f\u254f \u254fMy\u00a0subtitle\u00a0is\u00a0centered\u254f \u254f\u254f \u2517\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u00a0Centered!\u00a0\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u251b \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258aMy\u00a0subtitle\u00a0is\u00a0on\u00a0the\u00a0right\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u00a0Right\u00a0>\u00a0\u2581\u258e

from textual.app import App\nfrom textual.widgets import Label\nclass BorderSubtitleAlignApp(App):\ndef compose(self):\nlbl = Label(\"My subtitle is on the left.\", id=\"label1\")\nlbl.border_subtitle = \"< Left\"\nyield lbl\nlbl = Label(\"My subtitle is centered\", id=\"label2\")\nlbl.border_subtitle = \"Centered!\"\nyield lbl\nlbl = Label(\"My subtitle is on the right\", id=\"label3\")\nlbl.border_subtitle = \"Right >\"\nyield lbl\napp = BorderSubtitleAlignApp(css_path=\"border_subtitle_align.tcss\")\n
#label1 {\nborder: solid $secondary;\nborder-subtitle-align: left;\n}\n#label2 {\nborder: dashed $secondary;\nborder-subtitle-align: center;\n}\n#label3 {\nborder: tall $secondary;\nborder-subtitle-align: right;\n}\nScreen > Label {\nwidth: 100%;\nheight: 5;\ncontent-align: center middle;\ncolor: white;\nmargin: 1;\nbox-sizing: border-box;\n}\n
"},{"location":"styles/border_subtitle_align/#complete-usage-reference","title":"Complete usage reference","text":"

This example shows all border title and subtitle alignments, together with some examples of how (sub)titles can have custom markup. Open the code tabs to see the details of the code examples.

Outputborder_sub_title_align_all.pyborder_sub_title_align_all.tcss

BorderSubTitleAlignAll \u258fBorder\u00a0title\u2595\u256d\u2500Lef\u2026\u2500\u256e\u2581\u2581\u2581\u2581\u2581Left\u2581\u2581\u2581\u2581\u2581 \u258fThis\u00a0is\u00a0the\u00a0story\u00a0of\u2595\u2502a\u00a0Python\u2502\u258edeveloper\u00a0that\u258a \u258fBorder\u00a0subtitle\u2595\u2570\u2500Cen\u2026\u2500\u256f\u2594\u2594\u2594\u2594\u2594@@@\u2594\u2594\u2594\u2594\u2594\u2594 +--------------+\u2500Title\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 |had\u00a0to\u00a0fill\u00a0up|nine\u00a0labelsand\u00a0ended\u00a0up\u00a0redoing\u00a0it +-Left-------+\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500Subtitle\u2500 \u2500Title,\u00a0but\u00a0really\u00a0looo\u2026\u2500 \u2500Title,\u00a0but\u00a0r\u2026\u2500\u2500Title,\u00a0but\u00a0reall\u2026\u2500 because\u00a0the\u00a0first\u00a0tryhad\u00a0some\u00a0labelsthat\u00a0were\u00a0too\u00a0long. \u2500Subtitle,\u00a0bu\u2026\u2500\u2500Subtitle,\u00a0but\u00a0re\u2026\u2500 \u2500Subtitle,\u00a0but\u00a0really\u00a0l\u2026\u2500

from textual.app import App\nfrom textual.containers import Container, Grid\nfrom textual.widgets import Label\ndef make_label_container(  # (11)!\ntext: str, id: str, border_title: str, border_subtitle: str\n) -> Container:\nlbl = Label(text, id=id)\nlbl.border_title = border_title\nlbl.border_subtitle = border_subtitle\nreturn Container(lbl)\nclass BorderSubTitleAlignAll(App[None]):\ndef compose(self):\nwith Grid():\nyield make_label_container(  # (1)!\n\"This is the story of\",\n\"lbl1\",\n\"[b]Border [i]title[/i][/]\",\n\"[u][r]Border[/r] subtitle[/]\",\n)\nyield make_label_container(  # (2)!\n\"a Python\",\n\"lbl2\",\n\"[b red]Left, but it's loooooooooooong\",\n\"[reverse]Center, but it's loooooooooooong\",\n)\nyield make_label_container(  # (3)!\n\"developer that\",\n\"lbl3\",\n\"[b i on purple]Left[/]\",\n\"[r u white on black]@@@[/]\",\n)\nyield make_label_container(\n\"had to fill up\",\n\"lbl4\",\n\"\",  # (4)!\n\"[link=https://textual.textualize.io]Left[/]\",  # (5)!\n)\nyield make_label_container(  # (6)!\n\"nine labels\", \"lbl5\", \"Title\", \"Subtitle\"\n)\nyield make_label_container(  # (7)!\n\"and ended up redoing it\",\n\"lbl6\",\n\"Title\",\n\"Subtitle\",\n)\nyield make_label_container(  # (8)!\n\"because the first try\",\n\"lbl7\",\n\"Title, but really loooooooooong!\",\n\"Subtitle, but really loooooooooong!\",\n)\nyield make_label_container(  # (9)!\n\"had some labels\",\n\"lbl8\",\n\"Title, but really loooooooooong!\",\n\"Subtitle, but really loooooooooong!\",\n)\nyield make_label_container(  # (10)!\n\"that were too long.\",\n\"lbl9\",\n\"Title, but really loooooooooong!\",\n\"Subtitle, but really loooooooooong!\",\n)\napp = BorderSubTitleAlignAll(css_path=\"border_sub_title_align_all.tcss\")\nif __name__ == \"__main__\":\napp.run()\n
  1. Border (sub)titles can contain nested markup.
  2. Long (sub)titles get truncated and occupy as much space as possible.
  3. (Sub)titles can be stylised with Rich markup.
  4. An empty (sub)title isn't displayed.
  5. The markup can even contain Rich links.
  6. If the widget does not have a border, the title and subtitle are not shown.
  7. When the side borders are not set, the (sub)title will align with the edge of the widget.
  8. The title and subtitle are aligned on the left and very long, so they get truncated and we can still see the rightmost character of the border edge.
  9. The title and subtitle are centered and very long, so they get truncated and are centered with one character of padding on each side.
  10. The title and subtitle are aligned on the right and very long, so they get truncated and we can still see the leftmost character of the border edge.
  11. An auxiliary function to create labels with border title and subtitle.
Grid {\ngrid-size: 3 3;\nalign: center middle;\n}\nContainer {\nwidth: 100%;\nheight: 100%;\nalign: center middle;\n}\n#lbl1 {  /* (1)! */\nborder: vkey $secondary;\n}\n#lbl2 {  /* (2)! */\nborder: round $secondary;\nborder-title-align: right;\nborder-subtitle-align: right;\n}\n#lbl3 {\nborder: wide $secondary;\nborder-title-align: center;\nborder-subtitle-align: center;\n}\n#lbl4 {\nborder: ascii $success;\nborder-title-align: center;  /* (3)! */\nborder-subtitle-align: left;\n}\n#lbl5 {  /* (4)! */\n/* No border = no (sub)title. */\nborder: none $success;\nborder-title-align: center;\nborder-subtitle-align: center;\n}\n#lbl6 {  /* (5)! */\nborder-top: solid $success;\nborder-bottom: solid $success;\n}\n#lbl7 {  /* (6)! */\nborder-top: solid $error;\nborder-bottom: solid $error;\npadding: 1 2;\nborder-subtitle-align: left;\n}\n#lbl8 {\nborder-top: solid $error;\nborder-bottom: solid $error;\nborder-title-align: center;\nborder-subtitle-align: center;\n}\n#lbl9 {\nborder-top: solid $error;\nborder-bottom: solid $error;\nborder-title-align: right;\n}\n
  1. The default alignment for the title is left and the default alignment for the subtitle is right.
  2. Specifying an alignment when the (sub)title is too long has no effect. (Although, it will have an effect if the (sub)title is shortened or if the widget is widened.)
  3. Setting the alignment does not affect empty (sub)titles.
  4. If the border is not set, or set to none/hidden, the (sub)title is not shown.
  5. If the (sub)title alignment is on a side which does not have a border edge, the (sub)title will be flush to that side.
  6. Naturally, (sub)title positioning is affected by padding.
"},{"location":"styles/border_subtitle_align/#css","title":"CSS","text":"
border-subtitle-align: left;\nborder-subtitle-align: center;\nborder-subtitle-align: right;\n
"},{"location":"styles/border_subtitle_align/#python","title":"Python","text":"
widget.styles.border_subtitle_align = \"left\"\nwidget.styles.border_subtitle_align = \"center\"\nwidget.styles.border_subtitle_align = \"right\"\n
"},{"location":"styles/border_subtitle_align/#see-also","title":"See also","text":"
  • border-title-align to set the title's alignment.
  • border-title-color to set the title's color.
  • border-title-background to set the title's background color.
  • border-title-style to set the title's text style.

  • border-subtitle-align to set the sub-title's alignment.

  • border-subtitle-color to set the sub-title's color.
  • border-subtitle-background to set the sub-title's background color.
  • border-subtitle-style to set the sub-title's text style.
"},{"location":"styles/border_subtitle_background/","title":"Border-subtitle-background","text":"

The border-subtitle-background style sets the background color of the border_subtitle.

"},{"location":"styles/border_subtitle_background/#syntax","title":"Syntax","text":"
\nborder-subtitle-background: (<color> | auto) [<percentage>];\n
"},{"location":"styles/border_subtitle_background/#example","title":"Example","text":"

The following examples demonstrates customization of the border color and text style rules.

Outputborder_title_colors.pyborder_title_colors.tcss

BorderTitleApp \u250f\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503Hello,\u00a0World!\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u251b

from textual.app import App, ComposeResult\nfrom textual.widgets import Label\nclass BorderTitleApp(App):\nCSS_PATH = \"border_title_colors.tcss\"\ndef compose(self) -> ComposeResult:\nyield Label(\"Hello, World!\")\ndef on_mount(self) -> None:\nlabel = self.query_one(Label)\nlabel.border_title = \"Textual Rocks\"\nlabel.border_subtitle = \"Textual Rocks\"\nif __name__ == \"__main__\":\napp = BorderTitleApp()\napp.run()\n
Screen {\nalign: center middle;\n}\nLabel {\npadding: 4 8;\nborder: heavy red;\nborder-title-color: green;\nborder-title-background: white;\nborder-title-style: bold;\nborder-subtitle-color: magenta;\nborder-subtitle-background: yellow;\nborder-subtitle-style: italic;\n}\n
"},{"location":"styles/border_subtitle_background/#css","title":"CSS","text":"
border-subtitle-background: blue;\n
"},{"location":"styles/border_subtitle_background/#python","title":"Python","text":"
widget.styles.border_subtitle_background = \"blue\"\n
"},{"location":"styles/border_subtitle_background/#see-also","title":"See also","text":"
  • border-title-align to set the title's alignment.
  • border-title-color to set the title's color.
  • border-title-background to set the title's background color.
  • border-title-style to set the title's text style.

  • border-subtitle-align to set the sub-title's alignment.

  • border-subtitle-color to set the sub-title's color.
  • border-subtitle-background to set the sub-title's background color.
  • border-subtitle-style to set the sub-title's text style.
"},{"location":"styles/border_subtitle_color/","title":"Border-subtitle-color","text":"

The border-subtitle-color style sets the color of the border_subtitle.

"},{"location":"styles/border_subtitle_color/#syntax","title":"Syntax","text":"
\nborder-subtitle-color: (<color> | auto) [<percentage>];\n
"},{"location":"styles/border_subtitle_color/#example","title":"Example","text":"

The following examples demonstrates customization of the border color and text style rules.

Outputborder_title_colors.pyborder_title_colors.tcss

BorderTitleApp \u250f\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503Hello,\u00a0World!\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u251b

from textual.app import App, ComposeResult\nfrom textual.widgets import Label\nclass BorderTitleApp(App):\nCSS_PATH = \"border_title_colors.tcss\"\ndef compose(self) -> ComposeResult:\nyield Label(\"Hello, World!\")\ndef on_mount(self) -> None:\nlabel = self.query_one(Label)\nlabel.border_title = \"Textual Rocks\"\nlabel.border_subtitle = \"Textual Rocks\"\nif __name__ == \"__main__\":\napp = BorderTitleApp()\napp.run()\n
Screen {\nalign: center middle;\n}\nLabel {\npadding: 4 8;\nborder: heavy red;\nborder-title-color: green;\nborder-title-background: white;\nborder-title-style: bold;\nborder-subtitle-color: magenta;\nborder-subtitle-background: yellow;\nborder-subtitle-style: italic;\n}\n
"},{"location":"styles/border_subtitle_color/#css","title":"CSS","text":"
border-subtitle-color: red;\n
"},{"location":"styles/border_subtitle_color/#python","title":"Python","text":"
widget.styles.border_subtitle_color = \"red\"\n
"},{"location":"styles/border_subtitle_color/#see-also","title":"See also","text":"
  • border-title-align to set the title's alignment.
  • border-title-color to set the title's color.
  • border-title-background to set the title's background color.
  • border-title-style to set the title's text style.

  • border-subtitle-align to set the sub-title's alignment.

  • border-subtitle-color to set the sub-title's color.
  • border-subtitle-background to set the sub-title's background color.
  • border-subtitle-style to set the sub-title's text style.
"},{"location":"styles/border_subtitle_style/","title":"Border-subtitle-style","text":"

The border-subtitle-style style sets the text style of the border_subtitle.

"},{"location":"styles/border_subtitle_style/#syntax","title":"Syntax","text":"
\nborder-subtitle-style: <text-style>;\n
"},{"location":"styles/border_subtitle_style/#example","title":"Example","text":"

The following examples demonstrates customization of the border color and text style rules.

Outputborder_title_colors.pyborder_title_colors.tcss

BorderTitleApp \u250f\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503Hello,\u00a0World!\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u251b

from textual.app import App, ComposeResult\nfrom textual.widgets import Label\nclass BorderTitleApp(App):\nCSS_PATH = \"border_title_colors.tcss\"\ndef compose(self) -> ComposeResult:\nyield Label(\"Hello, World!\")\ndef on_mount(self) -> None:\nlabel = self.query_one(Label)\nlabel.border_title = \"Textual Rocks\"\nlabel.border_subtitle = \"Textual Rocks\"\nif __name__ == \"__main__\":\napp = BorderTitleApp()\napp.run()\n
Screen {\nalign: center middle;\n}\nLabel {\npadding: 4 8;\nborder: heavy red;\nborder-title-color: green;\nborder-title-background: white;\nborder-title-style: bold;\nborder-subtitle-color: magenta;\nborder-subtitle-background: yellow;\nborder-subtitle-style: italic;\n}\n
"},{"location":"styles/border_subtitle_style/#css","title":"CSS","text":"
border-subtitle-style: bold underline;\n
"},{"location":"styles/border_subtitle_style/#python","title":"Python","text":"
widget.styles.border_subtitle_style = \"bold underline\"\n
"},{"location":"styles/border_subtitle_style/#see-also","title":"See also","text":"
  • border-title-align to set the title's alignment.
  • border-title-color to set the title's color.
  • border-title-background to set the title's background color.
  • border-title-style to set the title's text style.

  • border-subtitle-align to set the sub-title's alignment.

  • border-subtitle-color to set the sub-title's color.
  • border-subtitle-background to set the sub-title's background color.
  • border-subtitle-style to set the sub-title's text style.
"},{"location":"styles/border_title_align/","title":"Border-title-align","text":"

The border-title-align style sets the horizontal alignment for the border title.

"},{"location":"styles/border_title_align/#syntax","title":"Syntax","text":"
\nborder-title-align: <horizontal>;\n

The border-title-align style takes a <horizontal> that determines where the border title is aligned along the top edge of the border. This means that the border corners are always visible.

"},{"location":"styles/border_title_align/#default","title":"Default","text":"

The default alignment is left.

"},{"location":"styles/border_title_align/#examples","title":"Examples","text":""},{"location":"styles/border_title_align/#basic-usage","title":"Basic usage","text":"

This example shows three labels, each with a different border title alignment:

Outputborder_title_align.pyborder_title_align.tcss

BorderTitleAlignApp \u250c\u2500\u00a0<\u00a0Left\u00a0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502 \u2502My\u00a0title\u00a0is\u00a0on\u00a0the\u00a0left.\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u250f\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u00a0Centered!\u00a0\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u2513 \u254f\u254f \u254fMy\u00a0title\u00a0is\u00a0centered\u254f \u254f\u254f \u2517\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u251b \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u00a0Right\u00a0>\u00a0\u2594\u258e \u258a\u258e \u258aMy\u00a0title\u00a0is\u00a0on\u00a0the\u00a0right\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

from textual.app import App\nfrom textual.widgets import Label\nclass BorderTitleAlignApp(App):\ndef compose(self):\nlbl = Label(\"My title is on the left.\", id=\"label1\")\nlbl.border_title = \"< Left\"\nyield lbl\nlbl = Label(\"My title is centered\", id=\"label2\")\nlbl.border_title = \"Centered!\"\nyield lbl\nlbl = Label(\"My title is on the right\", id=\"label3\")\nlbl.border_title = \"Right >\"\nyield lbl\napp = BorderTitleAlignApp(css_path=\"border_title_align.tcss\")\n
#label1 {\nborder: solid $secondary;\nborder-title-align: left;\n}\n#label2 {\nborder: dashed $secondary;\nborder-title-align: center;\n}\n#label3 {\nborder: tall $secondary;\nborder-title-align: right;\n}\nScreen > Label {\nwidth: 100%;\nheight: 5;\ncontent-align: center middle;\ncolor: white;\nmargin: 1;\nbox-sizing: border-box;\n}\n
"},{"location":"styles/border_title_align/#complete-usage-reference","title":"Complete usage reference","text":"

This example shows all border title and subtitle alignments, together with some examples of how (sub)titles can have custom markup. Open the code tabs to see the details of the code examples.

Outputborder_sub_title_align_all.pyborder_sub_title_align_all.tcss

BorderSubTitleAlignAll \u258fBorder\u00a0title\u2595\u256d\u2500Lef\u2026\u2500\u256e\u2581\u2581\u2581\u2581\u2581Left\u2581\u2581\u2581\u2581\u2581 \u258fThis\u00a0is\u00a0the\u00a0story\u00a0of\u2595\u2502a\u00a0Python\u2502\u258edeveloper\u00a0that\u258a \u258fBorder\u00a0subtitle\u2595\u2570\u2500Cen\u2026\u2500\u256f\u2594\u2594\u2594\u2594\u2594@@@\u2594\u2594\u2594\u2594\u2594\u2594 +--------------+\u2500Title\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 |had\u00a0to\u00a0fill\u00a0up|nine\u00a0labelsand\u00a0ended\u00a0up\u00a0redoing\u00a0it +-Left-------+\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500Subtitle\u2500 \u2500Title,\u00a0but\u00a0really\u00a0looo\u2026\u2500 \u2500Title,\u00a0but\u00a0r\u2026\u2500\u2500Title,\u00a0but\u00a0reall\u2026\u2500 because\u00a0the\u00a0first\u00a0tryhad\u00a0some\u00a0labelsthat\u00a0were\u00a0too\u00a0long. \u2500Subtitle,\u00a0bu\u2026\u2500\u2500Subtitle,\u00a0but\u00a0re\u2026\u2500 \u2500Subtitle,\u00a0but\u00a0really\u00a0l\u2026\u2500

from textual.app import App\nfrom textual.containers import Container, Grid\nfrom textual.widgets import Label\ndef make_label_container(  # (11)!\ntext: str, id: str, border_title: str, border_subtitle: str\n) -> Container:\nlbl = Label(text, id=id)\nlbl.border_title = border_title\nlbl.border_subtitle = border_subtitle\nreturn Container(lbl)\nclass BorderSubTitleAlignAll(App[None]):\ndef compose(self):\nwith Grid():\nyield make_label_container(  # (1)!\n\"This is the story of\",\n\"lbl1\",\n\"[b]Border [i]title[/i][/]\",\n\"[u][r]Border[/r] subtitle[/]\",\n)\nyield make_label_container(  # (2)!\n\"a Python\",\n\"lbl2\",\n\"[b red]Left, but it's loooooooooooong\",\n\"[reverse]Center, but it's loooooooooooong\",\n)\nyield make_label_container(  # (3)!\n\"developer that\",\n\"lbl3\",\n\"[b i on purple]Left[/]\",\n\"[r u white on black]@@@[/]\",\n)\nyield make_label_container(\n\"had to fill up\",\n\"lbl4\",\n\"\",  # (4)!\n\"[link=https://textual.textualize.io]Left[/]\",  # (5)!\n)\nyield make_label_container(  # (6)!\n\"nine labels\", \"lbl5\", \"Title\", \"Subtitle\"\n)\nyield make_label_container(  # (7)!\n\"and ended up redoing it\",\n\"lbl6\",\n\"Title\",\n\"Subtitle\",\n)\nyield make_label_container(  # (8)!\n\"because the first try\",\n\"lbl7\",\n\"Title, but really loooooooooong!\",\n\"Subtitle, but really loooooooooong!\",\n)\nyield make_label_container(  # (9)!\n\"had some labels\",\n\"lbl8\",\n\"Title, but really loooooooooong!\",\n\"Subtitle, but really loooooooooong!\",\n)\nyield make_label_container(  # (10)!\n\"that were too long.\",\n\"lbl9\",\n\"Title, but really loooooooooong!\",\n\"Subtitle, but really loooooooooong!\",\n)\napp = BorderSubTitleAlignAll(css_path=\"border_sub_title_align_all.tcss\")\nif __name__ == \"__main__\":\napp.run()\n
  1. Border (sub)titles can contain nested markup.
  2. Long (sub)titles get truncated and occupy as much space as possible.
  3. (Sub)titles can be stylised with Rich markup.
  4. An empty (sub)title isn't displayed.
  5. The markup can even contain Rich links.
  6. If the widget does not have a border, the title and subtitle are not shown.
  7. When the side borders are not set, the (sub)title will align with the edge of the widget.
  8. The title and subtitle are aligned on the left and very long, so they get truncated and we can still see the rightmost character of the border edge.
  9. The title and subtitle are centered and very long, so they get truncated and are centered with one character of padding on each side.
  10. The title and subtitle are aligned on the right and very long, so they get truncated and we can still see the leftmost character of the border edge.
  11. An auxiliary function to create labels with border title and subtitle.
Grid {\ngrid-size: 3 3;\nalign: center middle;\n}\nContainer {\nwidth: 100%;\nheight: 100%;\nalign: center middle;\n}\n#lbl1 {  /* (1)! */\nborder: vkey $secondary;\n}\n#lbl2 {  /* (2)! */\nborder: round $secondary;\nborder-title-align: right;\nborder-subtitle-align: right;\n}\n#lbl3 {\nborder: wide $secondary;\nborder-title-align: center;\nborder-subtitle-align: center;\n}\n#lbl4 {\nborder: ascii $success;\nborder-title-align: center;  /* (3)! */\nborder-subtitle-align: left;\n}\n#lbl5 {  /* (4)! */\n/* No border = no (sub)title. */\nborder: none $success;\nborder-title-align: center;\nborder-subtitle-align: center;\n}\n#lbl6 {  /* (5)! */\nborder-top: solid $success;\nborder-bottom: solid $success;\n}\n#lbl7 {  /* (6)! */\nborder-top: solid $error;\nborder-bottom: solid $error;\npadding: 1 2;\nborder-subtitle-align: left;\n}\n#lbl8 {\nborder-top: solid $error;\nborder-bottom: solid $error;\nborder-title-align: center;\nborder-subtitle-align: center;\n}\n#lbl9 {\nborder-top: solid $error;\nborder-bottom: solid $error;\nborder-title-align: right;\n}\n
  1. The default alignment for the title is left and the default alignment for the subtitle is right.
  2. Specifying an alignment when the (sub)title is too long has no effect. (Although, it will have an effect if the (sub)title is shortened or if the widget is widened.)
  3. Setting the alignment does not affect empty (sub)titles.
  4. If the border is not set, or set to none/hidden, the (sub)title is not shown.
  5. If the (sub)title alignment is on a side which does not have a border edge, the (sub)title will be flush to that side.
  6. Naturally, (sub)title positioning is affected by padding.
"},{"location":"styles/border_title_align/#css","title":"CSS","text":"
border-title-align: left;\nborder-title-align: center;\nborder-title-align: right;\n
"},{"location":"styles/border_title_align/#python","title":"Python","text":"
widget.styles.border_title_align = \"left\"\nwidget.styles.border_title_align = \"center\"\nwidget.styles.border_title_align = \"right\"\n
"},{"location":"styles/border_title_align/#see-also","title":"See also","text":"
  • border-title-align to set the title's alignment.
  • border-title-color to set the title's color.
  • border-title-background to set the title's background color.
  • border-title-style to set the title's text style.

  • border-subtitle-align to set the sub-title's alignment.

  • border-subtitle-color to set the sub-title's color.
  • border-subtitle-background to set the sub-title's background color.
  • border-subtitle-style to set the sub-title's text style.
"},{"location":"styles/border_title_background/","title":"Border-title-background","text":"

The border-title-background style sets the background color of the border_title.

"},{"location":"styles/border_title_background/#syntax","title":"Syntax","text":"
\nborder-title-background: (<color> | auto) [<percentage>];\n
"},{"location":"styles/border_title_background/#example","title":"Example","text":"

The following examples demonstrates customization of the border color and text style rules.

Outputborder_title_colors.pyborder_title_colors.tcss

BorderTitleApp \u250f\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503Hello,\u00a0World!\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u251b

from textual.app import App, ComposeResult\nfrom textual.widgets import Label\nclass BorderTitleApp(App):\nCSS_PATH = \"border_title_colors.tcss\"\ndef compose(self) -> ComposeResult:\nyield Label(\"Hello, World!\")\ndef on_mount(self) -> None:\nlabel = self.query_one(Label)\nlabel.border_title = \"Textual Rocks\"\nlabel.border_subtitle = \"Textual Rocks\"\nif __name__ == \"__main__\":\napp = BorderTitleApp()\napp.run()\n
Screen {\nalign: center middle;\n}\nLabel {\npadding: 4 8;\nborder: heavy red;\nborder-title-color: green;\nborder-title-background: white;\nborder-title-style: bold;\nborder-subtitle-color: magenta;\nborder-subtitle-background: yellow;\nborder-subtitle-style: italic;\n}\n
"},{"location":"styles/border_title_background/#css","title":"CSS","text":"
border-title-background: blue;\n
"},{"location":"styles/border_title_background/#python","title":"Python","text":"
widget.styles.border_title_background = \"blue\"\n
"},{"location":"styles/border_title_background/#see-also","title":"See also","text":"
  • border-title-align to set the title's alignment.
  • border-title-color to set the title's color.
  • border-title-background to set the title's background color.
  • border-title-style to set the title's text style.

  • border-subtitle-align to set the sub-title's alignment.

  • border-subtitle-color to set the sub-title's color.
  • border-subtitle-background to set the sub-title's background color.
  • border-subtitle-style to set the sub-title's text style.
"},{"location":"styles/border_title_color/","title":"Border-title-color","text":"

The border-title-color style sets the color of the border_title.

"},{"location":"styles/border_title_color/#syntax","title":"Syntax","text":"
\nborder-title-color: (<color> | auto) [<percentage>];\n
"},{"location":"styles/border_title_color/#example","title":"Example","text":"

The following examples demonstrates customization of the border color and text style rules.

Outputborder_title_colors.pyborder_title_colors.tcss

BorderTitleApp \u250f\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503Hello,\u00a0World!\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u251b

from textual.app import App, ComposeResult\nfrom textual.widgets import Label\nclass BorderTitleApp(App):\nCSS_PATH = \"border_title_colors.tcss\"\ndef compose(self) -> ComposeResult:\nyield Label(\"Hello, World!\")\ndef on_mount(self) -> None:\nlabel = self.query_one(Label)\nlabel.border_title = \"Textual Rocks\"\nlabel.border_subtitle = \"Textual Rocks\"\nif __name__ == \"__main__\":\napp = BorderTitleApp()\napp.run()\n
Screen {\nalign: center middle;\n}\nLabel {\npadding: 4 8;\nborder: heavy red;\nborder-title-color: green;\nborder-title-background: white;\nborder-title-style: bold;\nborder-subtitle-color: magenta;\nborder-subtitle-background: yellow;\nborder-subtitle-style: italic;\n}\n
"},{"location":"styles/border_title_color/#css","title":"CSS","text":"
border-title-color: red;\n
"},{"location":"styles/border_title_color/#python","title":"Python","text":"
widget.styles.border_title_color = \"red\"\n
"},{"location":"styles/border_title_color/#see-also","title":"See also","text":"
  • border-title-align to set the title's alignment.
  • border-title-color to set the title's color.
  • border-title-background to set the title's background color.
  • border-title-style to set the title's text style.

  • border-subtitle-align to set the sub-title's alignment.

  • border-subtitle-color to set the sub-title's color.
  • border-subtitle-background to set the sub-title's background color.
  • border-subtitle-style to set the sub-title's text style.
"},{"location":"styles/border_title_style/","title":"Border-title-style","text":"

The border-title-style style sets the text style of the border_title.

"},{"location":"styles/border_title_style/#syntax","title":"Syntax","text":"
\nborder-title-style: <text-style>;\n
"},{"location":"styles/border_title_style/#example","title":"Example","text":"

The following examples demonstrates customization of the border color and text style rules.

Outputborder_title_colors.pyborder_title_colors.tcss

BorderTitleApp \u250f\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503Hello,\u00a0World!\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0Textual\u00a0Rocks\u00a0\u2501\u251b

from textual.app import App, ComposeResult\nfrom textual.widgets import Label\nclass BorderTitleApp(App):\nCSS_PATH = \"border_title_colors.tcss\"\ndef compose(self) -> ComposeResult:\nyield Label(\"Hello, World!\")\ndef on_mount(self) -> None:\nlabel = self.query_one(Label)\nlabel.border_title = \"Textual Rocks\"\nlabel.border_subtitle = \"Textual Rocks\"\nif __name__ == \"__main__\":\napp = BorderTitleApp()\napp.run()\n
Screen {\nalign: center middle;\n}\nLabel {\npadding: 4 8;\nborder: heavy red;\nborder-title-color: green;\nborder-title-background: white;\nborder-title-style: bold;\nborder-subtitle-color: magenta;\nborder-subtitle-background: yellow;\nborder-subtitle-style: italic;\n}\n
"},{"location":"styles/border_title_style/#css","title":"CSS","text":"
border-title-style: bold underline;\n
"},{"location":"styles/border_title_style/#python","title":"Python","text":"
widget.styles.border_title_style = \"bold underline\"\n
"},{"location":"styles/border_title_style/#see-also","title":"See also","text":"
  • border-title-align to set the title's alignment.
  • border-title-color to set the title's color.
  • border-title-background to set the title's background color.
  • border-title-style to set the title's text style.

  • border-subtitle-align to set the sub-title's alignment.

  • border-subtitle-color to set the sub-title's color.
  • border-subtitle-background to set the sub-title's background color.
  • border-subtitle-style to set the sub-title's text style.
"},{"location":"styles/box_sizing/","title":"Box-sizing","text":"

The box-sizing style determines how the width and height of a widget are calculated.

"},{"location":"styles/box_sizing/#syntax","title":"Syntax","text":"
box-sizing: border-box | content-box;\n
"},{"location":"styles/box_sizing/#values","title":"Values","text":"Value Description border-box (default) Padding and border are included in the width and height. If you add padding and/or border the widget will not change in size, but you will have less space for content. content-box Padding and border will increase the size of the widget, leaving the content area unaffected."},{"location":"styles/box_sizing/#example","title":"Example","text":"

Both widgets in this example have the same height (5). The top widget has box-sizing: border-box which means that padding and border reduce the space for content. The bottom widget has box-sizing: content-box which increases the size of the widget to compensate for padding and border.

Outputbox_sizing.pybox_sizing.tcss

BoxSizingApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eI'm\u00a0using\u00a0border-box!\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eI'm\u00a0using\u00a0content-box!\u258a \u258e\u258a \u258e\u258a \u258e\u258a \u258e\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

from textual.app import App\nfrom textual.widgets import Static\nclass BoxSizingApp(App):\ndef compose(self):\nyield Static(\"I'm using border-box!\", id=\"static1\")\nyield Static(\"I'm using content-box!\", id=\"static2\")\napp = BoxSizingApp(css_path=\"box_sizing.tcss\")\n
#static1 {\nbox-sizing: border-box;\n}\n#static2 {\nbox-sizing: content-box;\n}\nScreen {\nbackground: white;\ncolor: black;\n}\nApp Static {\nbackground: blue 20%;\nheight: 5;\nmargin: 2;\npadding: 1;\nborder: wide black;\n}\n
"},{"location":"styles/box_sizing/#css","title":"CSS","text":"
/* Set box sizing to border-box (default) */\nbox-sizing: border-box;\n/* Set box sizing to content-box */\nbox-sizing: content-box;\n
"},{"location":"styles/box_sizing/#python","title":"Python","text":"
# Set box sizing to border-box (default)\nwidget.box_sizing = \"border-box\"\n# Set box sizing to content-box\nwidget.box_sizing = \"content-box\"\n
"},{"location":"styles/box_sizing/#see-also","title":"See also","text":"
  • border to add a border around a widget.
  • padding to add spacing around the content of a widget.
"},{"location":"styles/color/","title":"Color","text":"

The color style sets the text color of a widget.

"},{"location":"styles/color/#syntax","title":"Syntax","text":"
\ncolor: (<color> | auto) [<percentage>];\n

The color style requires a <color> followed by an optional <percentage> to specify the color's opacity.

You can also use the special value of \"auto\" in place of a color. This tells Textual to automatically select either white or black text for best contrast against the background.

"},{"location":"styles/color/#examples","title":"Examples","text":""},{"location":"styles/color/#basic-usage","title":"Basic usage","text":"

This example sets a different text color for each of three different widgets.

Outputcolor.pycolor.tcss

ColorApp I'm\u00a0red! I'm\u00a0rgb(0,\u00a0255,\u00a00)! I'm\u00a0hsl(240,\u00a0100%,\u00a050%)!

from textual.app import App\nfrom textual.widgets import Label\nclass ColorApp(App):\ndef compose(self):\nyield Label(\"I'm red!\", id=\"label1\")\nyield Label(\"I'm rgb(0, 255, 0)!\", id=\"label2\")\nyield Label(\"I'm hsl(240, 100%, 50%)!\", id=\"label3\")\napp = ColorApp(css_path=\"color.tcss\")\n
Label {\nheight: 1fr;\ncontent-align: center middle;\nwidth: 100%;\n}\n#label1 {\ncolor: red;\n}\n#label2 {\ncolor: rgb(0, 255, 0);\n}\n#label3 {\ncolor: hsl(240, 100%, 50%);\n}\n
"},{"location":"styles/color/#auto","title":"Auto","text":"

The next example shows how auto chooses between a lighter or a darker text color to increase the contrast and improve readability.

Outputcolor_auto.pycolor_auto.tcss

ColorApp The\u00a0quick\u00a0brown\u00a0fox\u00a0jumps\u00a0over\u00a0the\u00a0lazy\u00a0dog! The\u00a0quick\u00a0brown\u00a0fox\u00a0jumps\u00a0over\u00a0the\u00a0lazy\u00a0dog! The\u00a0quick\u00a0brown\u00a0fox\u00a0jumps\u00a0over\u00a0the\u00a0lazy\u00a0dog! The\u00a0quick\u00a0brown\u00a0fox\u00a0jumps\u00a0over\u00a0the\u00a0lazy\u00a0dog! The\u00a0quick\u00a0brown\u00a0fox\u00a0jumps\u00a0over\u00a0the\u00a0lazy\u00a0dog!

from textual.app import App\nfrom textual.widgets import Label\nclass ColorApp(App):\ndef compose(self):\nyield Label(\"The quick brown fox jumps over the lazy dog!\", id=\"lbl1\")\nyield Label(\"The quick brown fox jumps over the lazy dog!\", id=\"lbl2\")\nyield Label(\"The quick brown fox jumps over the lazy dog!\", id=\"lbl3\")\nyield Label(\"The quick brown fox jumps over the lazy dog!\", id=\"lbl4\")\nyield Label(\"The quick brown fox jumps over the lazy dog!\", id=\"lbl5\")\napp = ColorApp(css_path=\"color_auto.tcss\")\n
Label {\ncolor: auto 80%;\ncontent-align: center middle;\nheight: 1fr;\nwidth: 100%;\n}\n#lbl1 {\nbackground: red 80%;\n}\n#lbl2 {\nbackground: yellow 80%;\n}\n#lbl3 {\nbackground: blue 80%;\n}\n#lbl4 {\nbackground: pink 80%;\n}\n#lbl5 {\nbackground: green 80%;\n}\n
"},{"location":"styles/color/#css","title":"CSS","text":"
/* Blue text */\ncolor: blue;\n/* 20% red text */\ncolor: red 20%;\n/* RGB color */\ncolor: rgb(100, 120, 200);\n/* Automatically choose color with suitable contrast for readability */\ncolor: auto;\n
"},{"location":"styles/color/#python","title":"Python","text":"

You can use the same syntax as CSS, or explicitly set a Color object.

# Set blue text\nwidget.styles.color = \"blue\"\nfrom textual.color import Color\n# Set with a color object\nwidget.styles.color = Color.parse(\"pink\")\n
"},{"location":"styles/color/#see-also","title":"See also","text":"
  • background to set the background color in a widget.
"},{"location":"styles/content_align/","title":"Content-align","text":"

The content-align style aligns content inside a widget.

"},{"location":"styles/content_align/#syntax","title":"Syntax","text":"
\ncontent-align: <horizontal> <vertical>;\n\ncontent-align-horizontal: <horizontal>;\ncontent-align-vertical: <vertical>;\n

The content-align style takes a <horizontal> followed by a <vertical>.

You can specify the alignment of content on both the horizontal and vertical axes at the same time, or on each of the axis separately. To specify content alignment on a single axis, use the respective style and type:

  • content-align-horizontal takes a <horizontal> and does alignment along the horizontal axis; and
  • content-align-vertical takes a <vertical> and does alignment along the vertical axis.
"},{"location":"styles/content_align/#examples","title":"Examples","text":""},{"location":"styles/content_align/#basic-usage","title":"Basic usage","text":"

This first example shows three labels stacked vertically, each with different content alignments.

Outputcontent_align.pycontent_align.tcss

ContentAlignApp With\u00a0content-align\u00a0you\u00a0can... ...Easily\u00a0align\u00a0content... ...Horizontally\u00a0and\u00a0vertically!

from textual.app import App\nfrom textual.widgets import Label\nclass ContentAlignApp(App):\ndef compose(self):\nyield Label(\"With [i]content-align[/] you can...\", id=\"box1\")\nyield Label(\"...[b]Easily align content[/]...\", id=\"box2\")\nyield Label(\"...Horizontally [i]and[/] vertically!\", id=\"box3\")\napp = ContentAlignApp(css_path=\"content_align.tcss\")\n
#box1 {\ncontent-align: left top;\nbackground: red;\n}\n#box2 {\ncontent-align-horizontal: center;\ncontent-align-vertical: middle;\nbackground: green;\n}\n#box3 {\ncontent-align: right bottom;\nbackground: blue;\n}\nLabel {\nwidth: 100%;\nheight: 1fr;\npadding: 1;\ncolor: white;\n}\n
"},{"location":"styles/content_align/#all-content-alignments","title":"All content alignments","text":"

The next example shows a 3 by 3 grid of labels. Each label has its text aligned differently.

Outputcontent_align_all.pycontent_align_all.tcss

AllContentAlignApp left\u00a0topcenter\u00a0topright\u00a0top left\u00a0middlecenter\u00a0middleright\u00a0middle left\u00a0bottomcenter\u00a0bottomright\u00a0bottom

from textual.app import App\nfrom textual.widgets import Label\nclass AllContentAlignApp(App):\ndef compose(self):\nyield Label(\"left top\", id=\"left-top\")\nyield Label(\"center top\", id=\"center-top\")\nyield Label(\"right top\", id=\"right-top\")\nyield Label(\"left middle\", id=\"left-middle\")\nyield Label(\"center middle\", id=\"center-middle\")\nyield Label(\"right middle\", id=\"right-middle\")\nyield Label(\"left bottom\", id=\"left-bottom\")\nyield Label(\"center bottom\", id=\"center-bottom\")\nyield Label(\"right bottom\", id=\"right-bottom\")\napp = AllContentAlignApp(css_path=\"content_align_all.tcss\")\n
#left-top {\n/* content-align: left top; this is the default implied value. */\n}\n#center-top {\ncontent-align: center top;\n}\n#right-top {\ncontent-align: right top;\n}\n#left-middle {\ncontent-align: left middle;\n}\n#center-middle {\ncontent-align: center middle;\n}\n#right-middle {\ncontent-align: right middle;\n}\n#left-bottom {\ncontent-align: left bottom;\n}\n#center-bottom {\ncontent-align: center bottom;\n}\n#right-bottom {\ncontent-align: right bottom;\n}\nScreen {\nlayout: grid;\ngrid-size: 3 3;\ngrid-gutter: 1;\n}\nLabel {\nwidth: 100%;\nheight: 100%;\nbackground: $primary;\n}\n
"},{"location":"styles/content_align/#css","title":"CSS","text":"
/* Align content in the very center of a widget */\ncontent-align: center middle;\n/* Align content at the top right of a widget */\ncontent-align: right top;\n/* Change the horizontal alignment of the content of a widget */\ncontent-align-horizontal: right;\n/* Change the vertical alignment of the content of a widget */\ncontent-align-vertical: middle;\n
"},{"location":"styles/content_align/#python","title":"Python","text":"
# Align content in the very center of a widget\nwidget.styles.content_align = (\"center\", \"middle\")\n# Align content at the top right of a widget\nwidget.styles.content_align = (\"right\", \"top\")\n# Change the horizontal alignment of the content of a widget\nwidget.styles.content_align_horizontal = \"right\"\n# Change the vertical alignment of the content of a widget\nwidget.styles.content_align_vertical = \"middle\"\n
"},{"location":"styles/content_align/#see-also","title":"See also","text":"
  • align to set the alignment of children widgets inside a container.
  • text-align to set the alignment of text in a widget.
"},{"location":"styles/display/","title":"Display","text":"

The display style defines whether a widget is displayed or not.

"},{"location":"styles/display/#syntax","title":"Syntax","text":"
display: block | none;\n
"},{"location":"styles/display/#values","title":"Values","text":"Value Description block (default) Display the widget as normal. none The widget is not displayed and space will no longer be reserved for it."},{"location":"styles/display/#example","title":"Example","text":"

Note that the second widget is hidden by adding the \"remove\" class which sets the display style to none.

Outputdisplay.pydisplay.tcss

DisplayApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Widget\u00a01\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Widget\u00a03\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

from textual.app import App\nfrom textual.widgets import Static\nclass DisplayApp(App):\ndef compose(self):\nyield Static(\"Widget 1\")\nyield Static(\"Widget 2\", classes=\"remove\")\nyield Static(\"Widget 3\")\napp = DisplayApp(css_path=\"display.tcss\")\n
Screen {\nbackground: green;\n}\nStatic {\nheight: 5;\nbackground: white;\ncolor: blue;\nborder: heavy blue;\n}\nStatic.remove {\ndisplay: none;\n}\n
"},{"location":"styles/display/#css","title":"CSS","text":"
/* Widget is shown */\ndisplay: block;\n/* Widget is not shown */\ndisplay: none;\n
"},{"location":"styles/display/#python","title":"Python","text":"
# Hide the widget\nself.styles.display = \"none\"\n# Show the widget again\nself.styles.display = \"block\"\n

There is also a shortcut to show / hide a widget. The display property on Widget may be set to True or False to show or hide the widget.

# Hide the widget\nwidget.display = False\n# Show the widget\nwidget.display = True\n
"},{"location":"styles/display/#see-also","title":"See also","text":"
  • visibility to specify whether a widget is visible or not.
"},{"location":"styles/dock/","title":"Dock","text":"

The dock style is used to fix a widget to the edge of a container (which may be the entire terminal window).

"},{"location":"styles/dock/#syntax","title":"Syntax","text":"
dock: bottom | left | right | top;\n

The option chosen determines the edge to which the widget is docked.

"},{"location":"styles/dock/#examples","title":"Examples","text":""},{"location":"styles/dock/#basic-usage","title":"Basic usage","text":"

The example below shows a left docked sidebar. Notice that even though the content is scrolled, the sidebar remains fixed.

Outputdock_layout1_sidebar.pydock_layout1_sidebar.tcss

DockLayoutExample SidebarDocking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0 edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0making\u00a0them\u00a0ideal\u00a0 for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0\u2587\u2587 edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0making\u00a0them\u00a0ideal\u00a0 for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0 edges\u00a0of\u00a0a\u00a0container. Docked\u00a0widgets\u00a0will\u00a0not\u00a0scroll\u00a0out\u00a0of\u00a0view,\u00a0making\u00a0them\u00a0ideal\u00a0 for\u00a0sticky\u00a0headers,\u00a0footers,\u00a0and\u00a0sidebars. Docking\u00a0a\u00a0widget\u00a0removes\u00a0it\u00a0from\u00a0the\u00a0layout\u00a0and\u00a0fixes\u00a0its\u00a0 position,\u00a0aligned\u00a0to\u00a0either\u00a0the\u00a0top,\u00a0right,\u00a0bottom,\u00a0or\u00a0left\u00a0 edges\u00a0of\u00a0a\u00a0container.

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nTEXT = \"\"\"\\\nDocking a widget removes it from the layout and fixes its position, aligned to either the top, right, bottom, or left edges of a container.\nDocked widgets will not scroll out of view, making them ideal for sticky headers, footers, and sidebars.\n\"\"\"\nclass DockLayoutExample(App):\nCSS_PATH = \"dock_layout1_sidebar.tcss\"\ndef compose(self) -> ComposeResult:\nyield Static(\"Sidebar\", id=\"sidebar\")\nyield Static(TEXT * 10, id=\"body\")\nif __name__ == \"__main__\":\napp = DockLayoutExample()\napp.run()\n
#sidebar {\ndock: left;\nwidth: 15;\nheight: 100%;\ncolor: #0f2b41;\nbackground: dodgerblue;\n}\n
"},{"location":"styles/dock/#advanced-usage","title":"Advanced usage","text":"

The second example shows how one can use full-width or full-height containers to dock labels to the edges of a larger container. The labels will remain in that position (docked) even if the container they are in scrolls horizontally and/or vertically.

Outputdock_all.pydock_all.tcss

DockAllApp \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502top\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502leftright\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502bottom\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

from textual.app import App\nfrom textual.containers import Container\nfrom textual.widgets import Label\nclass DockAllApp(App):\ndef compose(self):\nyield Container(\nContainer(Label(\"left\"), id=\"left\"),\nContainer(Label(\"top\"), id=\"top\"),\nContainer(Label(\"right\"), id=\"right\"),\nContainer(Label(\"bottom\"), id=\"bottom\"),\nid=\"big_container\",\n)\napp = DockAllApp(css_path=\"dock_all.tcss\")\n
#left {\ndock: left;\nheight: 100%;\nwidth: auto;\nalign-vertical: middle;\n}\n#top {\ndock: top;\nheight: auto;\nwidth: 100%;\nalign-horizontal: center;\n}\n#right {\ndock: right;\nheight: 100%;\nwidth: auto;\nalign-vertical: middle;\n}\n#bottom {\ndock: bottom;\nheight: auto;\nwidth: 100%;\nalign-horizontal: center;\n}\nScreen {\nalign: center middle;\n}\n#big_container {\nwidth: 75%;\nheight: 75%;\nborder: round white;\n}\n
"},{"location":"styles/dock/#css","title":"CSS","text":"
dock: bottom;  /* Docks on the bottom edge of the parent container. */\ndock: left;    /* Docks on the   left edge of the parent container. */\ndock: right;   /* Docks on the  right edge of the parent container. */\ndock: top;     /* Docks on the    top edge of the parent container. */\n
"},{"location":"styles/dock/#python","title":"Python","text":"
widget.styles.dock = \"bottom\"  # Dock bottom.\nwidget.styles.dock = \"left\"    # Dock   left.\nwidget.styles.dock = \"right\"   # Dock  right.\nwidget.styles.dock = \"top\"     # Dock    top.\n
"},{"location":"styles/dock/#see-also","title":"See also","text":"
  • The layout guide section on docking.
"},{"location":"styles/height/","title":"Height","text":"

The height style sets a widget's height.

"},{"location":"styles/height/#syntax","title":"Syntax","text":"
\nheight: <scalar>;\n

The height style needs a <scalar> to determine the vertical length of the widget. By default, it sets the height of the content area, but if box-sizing is set to border-box it sets the height of the border area.

"},{"location":"styles/height/#examples","title":"Examples","text":""},{"location":"styles/height/#basic-usage","title":"Basic usage","text":"

This examples creates a widget with a height of 50% of the screen.

Outputheight.pyheight.tcss

HeightApp Widget

from textual.app import App\nfrom textual.widget import Widget\nclass HeightApp(App):\ndef compose(self):\nyield Widget()\napp = HeightApp(css_path=\"height.tcss\")\n
Screen > Widget {\nbackground: green;\nheight: 50%;\ncolor: white;\n}\n
"},{"location":"styles/height/#all-height-formats","title":"All height formats","text":"

The next example creates a series of wide widgets with heights set with different units. Open the CSS file tab to see the comments that explain how each height is computed. (The output includes a vertical ruler on the right to make it easier to check the height of each widget.)

Outputheight_comparison.pyheight_comparison.tcss

HeightComparisonApp #cells\u00b7 \u00b7 \u00b7 #percent\u00b7 \u2022 \u00b7 #w\u00b7 \u00b7 \u00b7 \u2022 #h\u00b7 \u00b7 \u00b7 \u00b7 #vw\u2022 \u00b7 \u00b7 \u00b7 #vh\u00b7 \u2022 #auto\u00b7 #fr1\u00b7 #fr2\u00b7 \u00b7

from textual.app import App\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Label, Placeholder, Static\nclass Ruler(Static):\ndef compose(self):\nruler_text = \"\u00b7\\n\u00b7\\n\u00b7\\n\u00b7\\n\u2022\\n\" * 100\nyield Label(ruler_text)\nclass HeightComparisonApp(App):\ndef compose(self):\nyield VerticalScroll(\nPlaceholder(id=\"cells\"),  # (1)!\nPlaceholder(id=\"percent\"),\nPlaceholder(id=\"w\"),\nPlaceholder(id=\"h\"),\nPlaceholder(id=\"vw\"),\nPlaceholder(id=\"vh\"),\nPlaceholder(id=\"auto\"),\nPlaceholder(id=\"fr1\"),\nPlaceholder(id=\"fr2\"),\n)\nyield Ruler()\napp = HeightComparisonApp(css_path=\"height_comparison.tcss\")\n
  1. The id of the placeholder identifies which unit will be used to set the height of the widget.
#cells {\nheight: 2;       /* (1)! */\n}\n#percent {\nheight: 12.5%;   /* (2)! */\n}\n#w {\nheight: 5w;      /* (3)! */\n}\n#h {\nheight: 12.5h;   /* (4)! */\n}\n#vw {\nheight: 6.25vw;  /* (5)! */\n}\n#vh {\nheight: 12.5vh;  /* (6)! */\n}\n#auto {\nheight: auto;    /* (7)! */\n}\n#fr1 {\nheight: 1fr;     /* (8)! */\n}\n#fr2 {\nheight: 2fr;     /* (9)! */\n}\nScreen {\nlayers: ruler;\noverflow: hidden;\n}\nRuler {\nlayer: ruler;\ndock: right;\nwidth: 1;\nbackground: $accent;\n}\n
  1. This sets the height to 2 lines.
  2. This sets the height to 12.5% of the space made available by the container. The container is 24 lines tall, so 12.5% of 24 is 3.
  3. This sets the height to 5% of the width of the direct container, which is the VerticalScroll container. Because it expands to fit all of the terminal, the width of the VerticalScroll is 80 and 5% of 80 is 4.
  4. This sets the height to 12.5% of the height of the direct container, which is the VerticalScroll container. Because it expands to fit all of the terminal, the height of the VerticalScroll is 24 and 12.5% of 24 is 3.
  5. This sets the height to 6.25% of the viewport width, which is 80. 6.25% of 80 is 5.
  6. This sets the height to 12.5% of the viewport height, which is 24. 12.5% of 24 is 3.
  7. This sets the height of the placeholder to be the optimal size that fits the content without scrolling. Because the content only spans one line, the placeholder has its height set to 1.
  8. This sets the height to 1fr, which means this placeholder will have half the height of a placeholder with 2fr.
  9. This sets the height to 2fr, which means this placeholder will have twice the height of a placeholder with 1fr.
"},{"location":"styles/height/#css","title":"CSS","text":"
/* Explicit cell height */\nheight: 10;\n/* Percentage height */\nheight: 50%;\n/* Automatic height */\nheight: auto\n
"},{"location":"styles/height/#python","title":"Python","text":"
self.styles.height = 10  # Explicit cell height can be an int\nself.styles.height = \"50%\"\nself.styles.height = \"auto\"\n
"},{"location":"styles/height/#see-also","title":"See also","text":"
  • max-height and min-height to limit the height of a widget.
  • width to set the width of a widget.
"},{"location":"styles/layer/","title":"Layer","text":"

The layer style defines the layer a widget belongs to.

"},{"location":"styles/layer/#syntax","title":"Syntax","text":"
\nlayer: <name>;\n

The layer style accepts a <name> that defines the layer this widget belongs to. This <name> must correspond to a <name> that has been defined in a layers style by an ancestor of this widget.

More information on layers can be found in the guide.

Warning

Using a <name> that hasn't been defined in a layers declaration of an ancestor of this widget has no effect.

"},{"location":"styles/layer/#example","title":"Example","text":"

In the example below, #box1 is yielded before #box2. However, since #box1 is on the higher layer, it is drawn on top of #box2.

Outputlayers.pylayers.tcss

LayersExample box1\u00a0(layer\u00a0=\u00a0above) box2\u00a0(layer\u00a0=\u00a0below)

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass LayersExample(App):\nCSS_PATH = \"layers.tcss\"\ndef compose(self) -> ComposeResult:\nyield Static(\"box1 (layer = above)\", id=\"box1\")\nyield Static(\"box2 (layer = below)\", id=\"box2\")\nif __name__ == \"__main__\":\napp = LayersExample()\napp.run()\n
Screen {\nalign: center middle;\nlayers: below above;\n}\nStatic {\nwidth: 28;\nheight: 8;\ncolor: auto;\ncontent-align: center middle;\n}\n#box1 {\nlayer: above;\nbackground: darkcyan;\n}\n#box2 {\nlayer: below;\nbackground: orange;\noffset: 12 6;\n}\n
"},{"location":"styles/layer/#css","title":"CSS","text":"
/* Draw the widget on the layer called 'below' */\nlayer: below;\n
"},{"location":"styles/layer/#python","title":"Python","text":"
# Draw the widget on the layer called 'below'\nwidget.styles.layer = \"below\"\n
"},{"location":"styles/layer/#see-also","title":"See also","text":"
  • The layout guide section on layers.
  • layers to define an ordered set of layers.
"},{"location":"styles/layers/","title":"Layers","text":"

The layers style allows you to define an ordered set of layers.

"},{"location":"styles/layers/#syntax","title":"Syntax","text":"
\nlayers: <name>+;\n

The layers style accepts one or more <name> that define the layers that the widget is aware of, and the order in which they will be painted on the screen.

The values used here can later be referenced using the layer property. The layers defined first in the list are drawn under the layers that are defined later in the list.

More information on layers can be found in the guide.

"},{"location":"styles/layers/#example","title":"Example","text":"

In the example below, #box1 is yielded before #box2. However, since #box1 is on the higher layer, it is drawn on top of #box2.

Outputlayers.pylayers.tcss

LayersExample box1\u00a0(layer\u00a0=\u00a0above) box2\u00a0(layer\u00a0=\u00a0below)

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass LayersExample(App):\nCSS_PATH = \"layers.tcss\"\ndef compose(self) -> ComposeResult:\nyield Static(\"box1 (layer = above)\", id=\"box1\")\nyield Static(\"box2 (layer = below)\", id=\"box2\")\nif __name__ == \"__main__\":\napp = LayersExample()\napp.run()\n
Screen {\nalign: center middle;\nlayers: below above;\n}\nStatic {\nwidth: 28;\nheight: 8;\ncolor: auto;\ncontent-align: center middle;\n}\n#box1 {\nlayer: above;\nbackground: darkcyan;\n}\n#box2 {\nlayer: below;\nbackground: orange;\noffset: 12 6;\n}\n
"},{"location":"styles/layers/#css","title":"CSS","text":"
/* Bottom layer is called 'below', layer above it is called 'above' */\nlayers: below above;\n
"},{"location":"styles/layers/#python","title":"Python","text":"
# Bottom layer is called 'below', layer above it is called 'above'\nwidget.style.layers = (\"below\", \"above\")\n
"},{"location":"styles/layers/#see-also","title":"See also","text":"
  • The layout guide section on layers.
  • layer to set the layer a widget belongs to.
"},{"location":"styles/layout/","title":"Layout","text":"

The layout style defines how a widget arranges its children.

"},{"location":"styles/layout/#syntax","title":"Syntax","text":"
\nlayout: grid | horizontal | vertical;\n

The layout style takes an option that defines how child widgets will be arranged, as per the table shown below.

"},{"location":"styles/layout/#values","title":"Values","text":"Value Description grid Child widgets will be arranged in a grid. horizontal Child widgets will be arranged along the horizontal axis, from left to right. vertical (default) Child widgets will be arranged along the vertical axis, from top to bottom.

See the layout guide for more information.

"},{"location":"styles/layout/#example","title":"Example","text":"

Note how the layout style affects the arrangement of widgets in the example below. To learn more about the grid layout, you can see the layout guide or the grid reference.

Outputlayout.pylayout.tcss

LayoutApp Layout Is Vertical LayoutIsHorizontal

from textual.app import App\nfrom textual.containers import Container\nfrom textual.widgets import Label\nclass LayoutApp(App):\ndef compose(self):\nyield Container(\nLabel(\"Layout\"),\nLabel(\"Is\"),\nLabel(\"Vertical\"),\nid=\"vertical-layout\",\n)\nyield Container(\nLabel(\"Layout\"),\nLabel(\"Is\"),\nLabel(\"Horizontal\"),\nid=\"horizontal-layout\",\n)\napp = LayoutApp(css_path=\"layout.tcss\")\n
#vertical-layout {\nlayout: vertical;\nbackground: darkmagenta;\nheight: auto;\n}\n#horizontal-layout {\nlayout: horizontal;\nbackground: darkcyan;\nheight: auto;\n}\nLabel {\nmargin: 1;\nwidth: 12;\ncolor: black;\nbackground: yellowgreen;\n}\n
"},{"location":"styles/layout/#css","title":"CSS","text":"
layout: horizontal;\n
"},{"location":"styles/layout/#python","title":"Python","text":"
widget.styles.layout = \"horizontal\"\n
"},{"location":"styles/layout/#see-also","title":"See also","text":"
  • Layout guide.
  • Grid reference.
"},{"location":"styles/margin/","title":"Margin","text":"

The margin style specifies spacing around a widget.

"},{"location":"styles/margin/#syntax","title":"Syntax","text":"
\nmargin: <integer>\n      # one value for all edges\n      | <integer> <integer>\n      # top/bot   left/right\n      | <integer> <integer> <integer> <integer>;\n      # top       right     bot       left\n\nmargin-top: <integer>;\nmargin-right: <integer>;\nmargin-bottom: <integer>;\nmargin-left: <integer>;\n

The margin specifies spacing around the four edges of the widget equal to the <integer> specified. The number of values given defines what edges get what margin:

  • 1 <integer> sets the same margin for the four edges of the widget;
  • 2 <integer> set margin for top/bottom and left/right edges, respectively.
  • 4 <integer> set margin for the top, right, bottom, and left edges, respectively.

Tip

To remember the order of the edges affected by the rule margin when it has 4 values, think of a clock. Its hand starts at the top and the goes clockwise: top, right, bottom, left.

Alternatively, margin can be set for each edge individually through the styles margin-top, margin-right, margin-bottom, and margin-left, respectively.

"},{"location":"styles/margin/#examples","title":"Examples","text":""},{"location":"styles/margin/#basic-usage","title":"Basic usage","text":"

In the example below we add a large margin to a label, which makes it move away from the top-left corner of the screen.

Outputmargin.pymargin.tcss

MarginApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eI\u00a0must\u00a0not\u00a0fear.\u258a \u258eFear\u00a0is\u00a0the\u00a0mind-killer.\u258a \u258eFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u258a \u258eI\u00a0will\u00a0face\u00a0my\u00a0fear.\u258a \u258eI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u258a \u258eAnd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0\u258a \u258eits\u00a0path.\u258a \u258eWhere\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0\u258a \u258eremain.\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

from textual.app import App\nfrom textual.widgets import Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass MarginApp(App):\ndef compose(self):\nyield Label(TEXT)\napp = MarginApp(css_path=\"margin.tcss\")\n
Screen {\nbackground: white;\ncolor: black;\n}\nLabel {\nmargin: 4 8;\nbackground: blue 20%;\nborder: blue wide;\nwidth: 100%;\n}\n
"},{"location":"styles/margin/#all-margin-settings","title":"All margin settings","text":"

The next example shows a grid. In each cell, we have a placeholder that has its margins set in different ways.

Outputmargin_all.pymargin_all.tcss

MarginAllApp \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502margin\u2502\u2502margin:\u00a01\u00a0\u2502 \u2502no\u00a0margin\u2502\u2502margin:\u00a01\u2502\u2502:\u00a01\u00a05\u2502\u25021\u00a02\u00a06\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502margin-bottom:\u00a04\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502margin-right:\u00a0\u2502\u2502\u2502\u2502margin-left:\u00a03\u2502 \u2502\u2502\u25023\u2502\u2502\u2502\u2502\u2502 \u2502margin-top:\u00a04\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

from textual.app import App\nfrom textual.containers import Container, Grid\nfrom textual.widgets import Placeholder\nclass MarginAllApp(App):\ndef compose(self):\nyield Grid(\nContainer(Placeholder(\"no margin\", id=\"p1\"), classes=\"bordered\"),\nContainer(Placeholder(\"margin: 1\", id=\"p2\"), classes=\"bordered\"),\nContainer(Placeholder(\"margin: 1 5\", id=\"p3\"), classes=\"bordered\"),\nContainer(Placeholder(\"margin: 1 1 2 6\", id=\"p4\"), classes=\"bordered\"),\nContainer(Placeholder(\"margin-top: 4\", id=\"p5\"), classes=\"bordered\"),\nContainer(Placeholder(\"margin-right: 3\", id=\"p6\"), classes=\"bordered\"),\nContainer(Placeholder(\"margin-bottom: 4\", id=\"p7\"), classes=\"bordered\"),\nContainer(Placeholder(\"margin-left: 3\", id=\"p8\"), classes=\"bordered\"),\n)\napp = MarginAllApp(css_path=\"margin_all.tcss\")\n
Screen {\nbackground: $background;\n}\nGrid {\ngrid-size: 4;\ngrid-gutter: 1 2;\n}\nPlaceholder {\nwidth: 100%;\nheight: 100%;\n}\nContainer {\nwidth: 100%;\nheight: 100%;\n}\n.bordered {\nborder: white round;\n}\n#p1 {\n/* default is no margin */\n}\n#p2 {\nmargin: 1;\n}\n#p3 {\nmargin: 1 5;\n}\n#p4 {\nmargin: 1 1 2 6;\n}\n#p5 {\nmargin-top: 4;\n}\n#p6 {\nmargin-right: 3;\n}\n#p7 {\nmargin-bottom: 4;\n}\n#p8 {\nmargin-left: 3;\n}\n
"},{"location":"styles/margin/#css","title":"CSS","text":"
/* Set margin of 1 around all edges */\nmargin: 1;\n/* Set margin of 2 on the top and bottom edges, and 4 on the left and right */\nmargin: 2 4;\n/* Set margin of 1 on the top, 2 on the right,\n3 on the bottom, and 4 on the left */\nmargin: 1 2 3 4;\nmargin-top: 1;\nmargin-right: 2;\nmargin-bottom: 3;\nmargin-left: 4;\n
"},{"location":"styles/margin/#python","title":"Python","text":"

Python does not provide the properties margin-top, margin-right, margin-bottom, and margin-left. However, you can set the margin to a single integer, a tuple of 2 integers, or a tuple of 4 integers:

# Set margin of 1 around all edges\nwidget.styles.margin = 1\n# Set margin of 2 on the top and bottom edges, and 4 on the left and right\nwidget.styles.margin = (2, 4)\n# Set margin of 1 on top, 2 on the right, 3 on the bottom, and 4 on the left\nwidget.styles.margin = (1, 2, 3, 4)\n
"},{"location":"styles/margin/#see-also","title":"See also","text":"
  • padding to add spacing around the content of a widget.
"},{"location":"styles/max_height/","title":"Max-height","text":"

The max-height style sets a maximum height for a widget.

"},{"location":"styles/max_height/#syntax","title":"Syntax","text":"
\nmax-height: <scalar>;\n

The max-height style accepts a <scalar> that defines an upper bound for the height of a widget. That is, the height of a widget is never allowed to exceed max-height.

"},{"location":"styles/max_height/#example","title":"Example","text":"

The example below shows some placeholders that were defined to span vertically from the top edge of the terminal to the bottom edge. Then, we set max-height individually on each placeholder.

Outputmax_height.pymax_height.tcss

MaxHeightApp max-height:\u00a010w max-height:\u00a010 max-height:\u00a050% max-height:\u00a0999

from textual.app import App\nfrom textual.containers import Horizontal\nfrom textual.widgets import Placeholder\nclass MaxHeightApp(App):\ndef compose(self):\nyield Horizontal(\nPlaceholder(\"max-height: 10w\", id=\"p1\"),\nPlaceholder(\"max-height: 999\", id=\"p2\"),\nPlaceholder(\"max-height: 50%\", id=\"p3\"),\nPlaceholder(\"max-height: 10\", id=\"p4\"),\n)\napp = MaxHeightApp(css_path=\"max_height.tcss\")\n
Horizontal {\nheight: 100%;\nwidth: 100%;\n}\nPlaceholder {\nheight: 100%;\nwidth: 1fr;\n}\n#p1 {\nmax-height: 10w;\n}\n#p2 {\nmax-height: 999;  /* (1)! */\n}\n#p3 {\nmax-height: 50%;\n}\n#p4 {\nmax-height: 10;\n}\n
  1. This won't affect the placeholder because its height is less than the maximum height.
"},{"location":"styles/max_height/#css","title":"CSS","text":"
/* Set the maximum height to 10 rows */\nmax-height: 10;\n/* Set the maximum height to 25% of the viewport height */\nmax-height: 25vh;\n
"},{"location":"styles/max_height/#python","title":"Python","text":"
# Set the maximum height to 10 rows\nwidget.styles.max_height = 10\n# Set the maximum height to 25% of the viewport height\nwidget.styles.max_height = \"25vh\"\n
"},{"location":"styles/max_height/#see-also","title":"See also","text":"
  • min-height to set a lower bound on the height of a widget.
  • height to set the height of a widget.
"},{"location":"styles/max_width/","title":"Max-width","text":"

The max-width style sets a maximum width for a widget.

"},{"location":"styles/max_width/#syntax","title":"Syntax","text":"
\nmax-width: <scalar>;\n

The max-width style accepts a <scalar> that defines an upper bound for the width of a widget. That is, the width of a widget is never allowed to exceed max-width.

"},{"location":"styles/max_width/#example","title":"Example","text":"

The example below shows some placeholders that were defined to span horizontally from the left edge of the terminal to the right edge. Then, we set max-width individually on each placeholder.

Outputmax_width.pymax_width.tcss

MaxWidthApp max-width:\u00a0 50h max-width:\u00a0999 max-width:\u00a050% max-width:\u00a030

from textual.app import App\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Placeholder\nclass MaxWidthApp(App):\ndef compose(self):\nyield VerticalScroll(\nPlaceholder(\"max-width: 50h\", id=\"p1\"),\nPlaceholder(\"max-width: 999\", id=\"p2\"),\nPlaceholder(\"max-width: 50%\", id=\"p3\"),\nPlaceholder(\"max-width: 30\", id=\"p4\"),\n)\napp = MaxWidthApp(css_path=\"max_width.tcss\")\n
Horizontal {\nheight: 100%;\nwidth: 100%;\n}\nPlaceholder {\nwidth: 100%;\nheight: 1fr;\n}\n#p1 {\nmax-width: 50h;\n}\n#p2 {\nmax-width: 999;  /* (1)! */\n}\n#p3 {\nmax-width: 50%;\n}\n#p4 {\nmax-width: 30;\n}\n
  1. This won't affect the placeholder because its width is less than the maximum width.
"},{"location":"styles/max_width/#css","title":"CSS","text":"
/* Set the maximum width to 10 rows */\nmax-width: 10;\n/* Set the maximum width to 25% of the viewport width */\nmax-width: 25vw;\n
"},{"location":"styles/max_width/#python","title":"Python","text":"
# Set the maximum width to 10 rows\nwidget.styles.max_width = 10\n# Set the maximum width to 25% of the viewport width\nwidget.styles.max_width = \"25vw\"\n
"},{"location":"styles/max_width/#see-also","title":"See also","text":"
  • min-width to set a lower bound on the width of a widget.
  • width to set the width of a widget.
"},{"location":"styles/min_height/","title":"Min-height","text":"

The min-height style sets a minimum height for a widget.

"},{"location":"styles/min_height/#syntax","title":"Syntax","text":"
\nmin-height: <scalar>;\n

The min-height style accepts a <scalar> that defines a lower bound for the height of a widget. That is, the height of a widget is never allowed to be under min-height.

"},{"location":"styles/min_height/#example","title":"Example","text":"

The example below shows some placeholders with their height set to 50%. Then, we set min-height individually on each placeholder.

Outputmin_height.pymin_height.tcss

MinHeightApp min-height:\u00a025% min-height:\u00a075% min-height:\u00a030 min-height:\u00a040w \u2583\u2583

from textual.app import App\nfrom textual.containers import Horizontal\nfrom textual.widgets import Placeholder\nclass MinHeightApp(App):\ndef compose(self):\nyield Horizontal(\nPlaceholder(\"min-height: 25%\", id=\"p1\"),\nPlaceholder(\"min-height: 75%\", id=\"p2\"),\nPlaceholder(\"min-height: 30\", id=\"p3\"),\nPlaceholder(\"min-height: 40w\", id=\"p4\"),\n)\napp = MinHeightApp(css_path=\"min_height.tcss\")\n
Horizontal {\nheight: 100%;\nwidth: 100%;\noverflow-y: auto;\n}\nPlaceholder {\nwidth: 1fr;\nheight: 50%;\n}\n#p1 {\nmin-height: 25%;  /* (1)! */\n}\n#p2 {\nmin-height: 75%;\n}\n#p3 {\nmin-height: 30;\n}\n#p4 {\nmin-height: 40w;\n}\n
  1. This won't affect the placeholder because its height is larger than the minimum height.
"},{"location":"styles/min_height/#css","title":"CSS","text":"
/* Set the minimum height to 10 rows */\nmin-height: 10;\n/* Set the minimum height to 25% of the viewport height */\nmin-height: 25vh;\n
"},{"location":"styles/min_height/#python","title":"Python","text":"
# Set the minimum height to 10 rows\nwidget.styles.min_height = 10\n# Set the minimum height to 25% of the viewport height\nwidget.styles.min_height = \"25vh\"\n
"},{"location":"styles/min_height/#see-also","title":"See also","text":"
  • max-height to set an upper bound on the height of a widget.
  • height to set the height of a widget.
"},{"location":"styles/min_width/","title":"Min-width","text":"

The min-width style sets a minimum width for a widget.

"},{"location":"styles/min_width/#syntax","title":"Syntax","text":"
\nmin-width: <scalar>;\n

The min-width style accepts a <scalar> that defines a lower bound for the width of a widget. That is, the width of a widget is never allowed to be under min-width.

"},{"location":"styles/min_width/#example","title":"Example","text":"

The example below shows some placeholders with their width set to 50%. Then, we set min-width individually on each placeholder.

Outputmin_width.pymin_width.tcss

MinWidthApp min-width:\u00a025% min-width:\u00a075% min-width:\u00a0100 min-width:\u00a0400h

from textual.app import App\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Placeholder\nclass MinWidthApp(App):\ndef compose(self):\nyield VerticalScroll(\nPlaceholder(\"min-width: 25%\", id=\"p1\"),\nPlaceholder(\"min-width: 75%\", id=\"p2\"),\nPlaceholder(\"min-width: 100\", id=\"p3\"),\nPlaceholder(\"min-width: 400h\", id=\"p4\"),\n)\napp = MinWidthApp(css_path=\"min_width.tcss\")\n
VerticalScroll {\nheight: 100%;\nwidth: 100%;\noverflow-x: auto;\n}\nPlaceholder {\nheight: 1fr;\nwidth: 50%;\n}\n#p1 {\nmin-width: 25%;\n/* (1)! */\n}\n#p2 {\nmin-width: 75%;\n}\n#p3 {\nmin-width: 100;\n}\n#p4 {\nmin-width: 400h;\n}\n
  1. This won't affect the placeholder because its width is larger than the minimum width.
"},{"location":"styles/min_width/#css","title":"CSS","text":"
/* Set the minimum width to 10 rows */\nmin-width: 10;\n/* Set the minimum width to 25% of the viewport width */\nmin-width: 25vw;\n
"},{"location":"styles/min_width/#python","title":"Python","text":"
# Set the minimum width to 10 rows\nwidget.styles.min_width = 10\n# Set the minimum width to 25% of the viewport width\nwidget.styles.min_width = \"25vw\"\n
"},{"location":"styles/min_width/#see-also","title":"See also","text":"
  • max-width to set an upper bound on the width of a widget.
  • width to set the width of a widget.
"},{"location":"styles/offset/","title":"Offset","text":"

The offset style defines an offset for the position of the widget.

"},{"location":"styles/offset/#syntax","title":"Syntax","text":"
\noffset: <scalar> <scalar>;\n\noffset-x: <scalar>;\noffset-y: <scalar>\n

The two <scalar> in the offset define, respectively, the offsets in the horizontal and vertical axes for the widget.

To specify an offset along a single axis, you can use offset-x and offset-y.

"},{"location":"styles/offset/#example","title":"Example","text":"

In this example, we have 3 widgets with differing offsets.

Outputoffset.pyoffset.tcss

OffsetApp \u258c\u2590 \u258cChani\u00a0(offset\u00a00\u00a0\u2590 \u259b\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u259c\u258c-3)\u2590 \u258c\u2590\u258c\u2590 \u258c\u2590\u258c\u2590 \u258c\u2590\u258c\u2590 \u258cPaul\u00a0(offset\u00a08\u00a02)\u2590\u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259f \u258c\u2590 \u258c\u2590 \u258c\u2590 \u258c\u259b\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u259c \u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u258c\u2590 \u258c\u2590 \u258c\u2590 \u258cDuncan\u00a0(offset\u00a04\u00a0\u2590 \u258c10)\u2590 \u258c\u2590 \u258c\u2590 \u258c\u2590 \u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259f

from textual.app import App\nfrom textual.widgets import Label\nclass OffsetApp(App):\ndef compose(self):\nyield Label(\"Paul (offset 8 2)\", classes=\"paul\")\nyield Label(\"Duncan (offset 4 10)\", classes=\"duncan\")\nyield Label(\"Chani (offset 0 -3)\", classes=\"chani\")\napp = OffsetApp(css_path=\"offset.tcss\")\n
Screen {\nbackground: white;\ncolor: black;\nlayout: horizontal;\n}\nLabel {\nwidth: 20;\nheight: 10;\ncontent-align: center middle;\n}\n.paul {\noffset: 8 2;\nbackground: red 20%;\nborder: outer red;\ncolor: red;\n}\n.duncan {\noffset: 4 10;\nbackground: green 20%;\nborder: outer green;\ncolor: green;\n}\n.chani {\noffset: 0 -3;\nbackground: blue 20%;\nborder: outer blue;\ncolor: blue;\n}\n
"},{"location":"styles/offset/#css","title":"CSS","text":"
/* Move the widget 8 cells in the x direction and 2 in the y direction */\noffset: 8 2;\n/* Move the widget 4 cells in the x direction\noffset-x: 4;\n/* Move the widget -3 cells in the y direction\noffset-y: -3;\n
"},{"location":"styles/offset/#python","title":"Python","text":"

You cannot change programmatically the offset for a single axis. You have to set the two axes at the same time.

# Move the widget 2 cells in the x direction, and 4 in the y direction.\nwidget.styles.offset = (2, 4)\n
"},{"location":"styles/offset/#see-also","title":"See also","text":"
  • The layout guide section on offsets.
"},{"location":"styles/opacity/","title":"Opacity","text":"

The opacity style sets the opacity of a widget.

While terminals are not capable of true opacity, Textual can create an approximation by blending widgets with their background color.

"},{"location":"styles/opacity/#syntax","title":"Syntax","text":"
\nopacity: <number> | <percentage>;\n

The opacity of a widget can be set as a <number> or a <percentage>. If given as a number, then opacity should be a value between 0 and 1, where 0 is the background color and 1 is fully opaque. If given as a percentage, 0% is the background color and 100% is fully opaque.

Typically, if you set this value it would be somewhere between the two extremes. For instance, setting the opacity of a widget to 70% will make it appear dimmer than surrounding widgets, which could be used to display a disabled state.

"},{"location":"styles/opacity/#example","title":"Example","text":"

This example shows, from top to bottom, increasing opacity values for a label with a border and some text. When the opacity is zero, all we see is the (black) background.

Outputopacity.pyopacity.tcss

OpacityApp \u259b\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u259c \u258copacity:\u00a00%\u2590 \u258c\u2590 \u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259f \u259b\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u259c \u258c\u2590 \u258copacity:\u00a025%\u2590 \u258c\u2590 \u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259f \u259b\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u259c \u258c\u2590 \u258copacity:\u00a050%\u2590 \u258c\u2590 \u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259f \u259b\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u259c \u258c\u2590 \u258copacity:\u00a075%\u2590 \u258c\u2590 \u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259f \u259b\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u259c \u258c\u2590 \u258copacity:\u00a0100%\u2590 \u258c\u2590 \u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259f

from textual.app import App\nfrom textual.widgets import Label\nclass OpacityApp(App):\ndef compose(self):\nyield Label(\"opacity: 0%\", id=\"zero-opacity\")\nyield Label(\"opacity: 25%\", id=\"quarter-opacity\")\nyield Label(\"opacity: 50%\", id=\"half-opacity\")\nyield Label(\"opacity: 75%\", id=\"three-quarter-opacity\")\nyield Label(\"opacity: 100%\", id=\"full-opacity\")\napp = OpacityApp(css_path=\"opacity.tcss\")\n
#zero-opacity {\nopacity: 0%;\n}\n#quarter-opacity {\nopacity: 25%;\n}\n#half-opacity {\nopacity: 50%;\n}\n#three-quarter-opacity {\nopacity: 75%;\n}\n#full-opacity {\nopacity: 100%;\n}\nScreen {\nbackground: black;\n}\nLabel {\nwidth: 100%;\nheight: 1fr;\nborder: outer dodgerblue;\nbackground: lightseagreen;\ncontent-align: center middle;\ntext-style: bold;\n}\n
"},{"location":"styles/opacity/#css","title":"CSS","text":"
/* Fade the widget to 50% against its parent's background */\nopacity: 50%;\n
"},{"location":"styles/opacity/#python","title":"Python","text":"
# Fade the widget to 50% against its parent's background\nwidget.styles.opacity = \"50%\"\n
"},{"location":"styles/opacity/#see-also","title":"See also","text":"
  • text-opacity to blend the color of a widget's content with its background color.
"},{"location":"styles/outline/","title":"Outline","text":"

The outline style enables the drawing of a box around the content of a widget, which means the outline is drawn over the content area.

Note

border and outline cannot coexist in the same edge of a widget.

"},{"location":"styles/outline/#syntax","title":"Syntax","text":"
\noutline: [<border>] [<color>];\n\noutline-top: [<border>] [<color>];\noutline-right: [<border>] [<color>];\noutline-bottom: [<border>] [<color>];\noutline-left: [<border>] [<color>];\n

The style outline accepts an optional <border> that sets the visual style of the widget outline and an optional <color> to set the color of the outline.

Unlike the style border, the frame of the outline is drawn over the content area of the widget. This rule can be useful to add temporary emphasis on the content of a widget, if you want to draw the user's attention to it.

"},{"location":"styles/outline/#border-command","title":"Border command","text":"

The textual CLI has a subcommand which will let you explore the various border types interactively, when applied to the CSS rule border:

textual borders\n
"},{"location":"styles/outline/#examples","title":"Examples","text":""},{"location":"styles/outline/#basic-usage","title":"Basic usage","text":"

This example shows a widget with an outline. Note how the outline occludes the text area.

Outputoutline.pyoutline.tcss

OutlineApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eear\u00a0is\u00a0the\u00a0mind-killer.\u258a \u258eear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u258a \u258e\u00a0will\u00a0face\u00a0my\u00a0fear.\u258a \u258e\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u258a \u258end\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u258a \u258eath.\u258a \u258ehere\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

from textual.app import App\nfrom textual.widgets import Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass OutlineApp(App):\ndef compose(self):\nyield Label(TEXT)\napp = OutlineApp(css_path=\"outline.tcss\")\n
Screen {\nbackground: white;\ncolor: black;\n}\nLabel {\nmargin: 4 8;\nbackground: green 20%;\noutline: wide green;\nwidth: 100%;\n}\n
"},{"location":"styles/outline/#all-outline-types","title":"All outline types","text":"

The next example shows a grid with all the available outline types.

Outputoutline_all.pyoutline_all.tcss

AllOutlinesApp +------------------+\u250f\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u2513 |ascii|blank\u254fdashed\u254f +------------------+\u2517\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u251b \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2551double\u2551\u2503heavy\u2503hidden/none \u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d\u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2597\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2596 hkey\u2590inner\u258cnone \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u259d\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2598 \u259b\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u259c\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u258couter\u2590\u2502round\u2502\u2502solid\u2502 \u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258f\u2595\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258atall\u258e\u258fvkey\u2595\u258ewide\u258a \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258f\u2595\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594

from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Label\nclass AllOutlinesApp(App):\ndef compose(self):\nyield Grid(\nLabel(\"ascii\", id=\"ascii\"),\nLabel(\"blank\", id=\"blank\"),\nLabel(\"dashed\", id=\"dashed\"),\nLabel(\"double\", id=\"double\"),\nLabel(\"heavy\", id=\"heavy\"),\nLabel(\"hidden/none\", id=\"hidden\"),\nLabel(\"hkey\", id=\"hkey\"),\nLabel(\"inner\", id=\"inner\"),\nLabel(\"none\", id=\"none\"),\nLabel(\"outer\", id=\"outer\"),\nLabel(\"round\", id=\"round\"),\nLabel(\"solid\", id=\"solid\"),\nLabel(\"tall\", id=\"tall\"),\nLabel(\"vkey\", id=\"vkey\"),\nLabel(\"wide\", id=\"wide\"),\n)\napp = AllOutlinesApp(css_path=\"outline_all.tcss\")\n
#ascii {\noutline: ascii $accent;\n}\n#blank {\noutline: blank $accent;\n}\n#dashed {\noutline: dashed $accent;\n}\n#double {\noutline: double $accent;\n}\n#heavy {\noutline: heavy $accent;\n}\n#hidden {\noutline: hidden $accent;\n}\n#hkey {\noutline: hkey $accent;\n}\n#inner {\noutline: inner $accent;\n}\n#none {\noutline: none $accent;\n}\n#outer {\noutline: outer $accent;\n}\n#round {\noutline: round $accent;\n}\n#solid {\noutline: solid $accent;\n}\n#tall {\noutline: tall $accent;\n}\n#vkey {\noutline: vkey $accent;\n}\n#wide {\noutline: wide $accent;\n}\nGrid {\ngrid-size: 3 5;\nalign: center middle;\ngrid-gutter: 1 2;\n}\nLabel {\nwidth: 20;\nheight: 3;\ncontent-align: center middle;\n}\n
"},{"location":"styles/outline/#borders-and-outlines","title":"Borders and outlines","text":"

The next example makes the difference between border and outline clearer by having three labels side-by-side. They contain the same text, have the same width and height, and are styled exactly the same up to their border and outline styles.

This example also shows that a widget cannot contain both a border and an outline:

Outputoutline_vs_border.pyoutline_vs_border.tcss

OutlineBorderApp \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502ear\u00a0is\u00a0the\u00a0mind-killer.\u2502 \u2502ear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2502 \u2502\u00a0will\u00a0face\u00a0my\u00a0fear.\u2502 \u2502\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2502 \u2502nd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path\u2502 \u2502here\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503I\u00a0must\u00a0not\u00a0fear.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0mind-killer.\u2503 \u2503Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2503 \u2503I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2503 \u2503I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2503 \u2503And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502I\u00a0must\u00a0not\u00a0fear.\u2502 \u2502Fear\u00a0is\u00a0the\u00a0mind-killer.\u2502 \u2502Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2502 \u2502I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2502 \u2502I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u2502 \u2502And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path.\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

from textual.app import App\nfrom textual.widgets import Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass OutlineBorderApp(App):\ndef compose(self):\nyield Label(TEXT, classes=\"outline\")\nyield Label(TEXT, classes=\"border\")\nyield Label(TEXT, classes=\"outline border\")\napp = OutlineBorderApp(css_path=\"outline_vs_border.tcss\")\n
Label {\nheight: 8;\n}\n.outline {\noutline: $error round;\n}\n.border {\nborder: $success heavy;\n}\n
"},{"location":"styles/outline/#css","title":"CSS","text":"
/* Set a heavy white outline */\noutline:heavy white;\n/* set a red outline on the left */\noutline-left:outer red;\n
"},{"location":"styles/outline/#python","title":"Python","text":"
# Set a heavy white outline\nwidget.outline = (\"heavy\", \"white\")\n# Set a red outline on the left\nwidget.outline_left = (\"outer\", \"red\")\n
"},{"location":"styles/outline/#see-also","title":"See also","text":"
  • border to add a border around a widget.
"},{"location":"styles/overflow/","title":"Overflow","text":"

The overflow style specifies if and when scrollbars should be displayed.

"},{"location":"styles/overflow/#syntax","title":"Syntax","text":"
\noverflow: <overflow> <overflow>;\n\noverflow-x: <overflow>;\noverflow-y: <overflow>;\n

The style overflow accepts two values that determine when to display scrollbars in a container widget. The two values set the overflow for the horizontal and vertical axes, respectively.

Overflow may also be set individually for each axis:

  • overflow-x sets the overflow for the horizontal axis; and
  • overflow-y sets the overflow for the vertical axis.
"},{"location":"styles/overflow/#defaults","title":"Defaults","text":"

The default setting for containers is overflow: auto auto.

Warning

Some built-in containers like Horizontal and VerticalScroll override these defaults.

"},{"location":"styles/overflow/#example","title":"Example","text":"

Here we split the screen into left and right sections, each with three vertically scrolling widgets that do not fit into the height of the terminal.

The left side has overflow-y: auto (the default) and will automatically show a scrollbar. The right side has overflow-y: hidden which will prevent a scrollbar from being shown.

Outputoverflow.pyoverflow.tcss

OverflowApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258eI\u00a0must\u00a0not\u00a0fear.\u258a\u258eI\u00a0must\u00a0not\u00a0fear.\u258a \u258eFear\u00a0is\u00a0the\u00a0mind-killer.\u258a\u258eFear\u00a0is\u00a0the\u00a0mind-killer.\u258a \u258eFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0\u258a\u258eFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0\u258a \u258ebrings\u00a0total\u00a0obliteration.\u258a\u258ebrings\u00a0total\u00a0obliteration.\u258a \u258eI\u00a0will\u00a0face\u00a0my\u00a0fear.\u258a\u258eI\u00a0will\u00a0face\u00a0my\u00a0fear.\u258a \u258eI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u258a\u258eI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0\u258a \u258eand\u00a0through\u00a0me.\u258a\u258eand\u00a0through\u00a0me.\u258a \u258eAnd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0\u258a\u258eAnd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0\u258a \u258ewill\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0\u258a\u258eturn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0\u258a \u258eits\u00a0path.\u258a\u2581\u2581\u258epath.\u258a \u258eWhere\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0\u258a\u258eWhere\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u258a \u258ewill\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0\u258a\u258ebe\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\u258a \u258eremain.\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258eI\u00a0must\u00a0not\u00a0fear.\u258a \u258eI\u00a0must\u00a0not\u00a0fear.\u258a\u258eFear\u00a0is\u00a0the\u00a0mind-killer.\u258a \u258eFear\u00a0is\u00a0the\u00a0mind-killer.\u258a\u258eFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0\u258a \u258eFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0\u258a\u258ebrings\u00a0total\u00a0obliteration.\u258a \u258ebrings\u00a0total\u00a0obliteration.\u258a\u258eI\u00a0will\u00a0face\u00a0my\u00a0fear.\u258a \u258eI\u00a0will\u00a0face\u00a0my\u00a0fear.\u258a\u258eI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0\u258a \u258eI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u258a\u258eand\u00a0through\u00a0me.\u258a

from textual.app import App\nfrom textual.containers import Horizontal, VerticalScroll\nfrom textual.widgets import Static\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass OverflowApp(App):\ndef compose(self):\nyield Horizontal(\nVerticalScroll(Static(TEXT), Static(TEXT), Static(TEXT), id=\"left\"),\nVerticalScroll(Static(TEXT), Static(TEXT), Static(TEXT), id=\"right\"),\n)\napp = OverflowApp(css_path=\"overflow.tcss\")\n
Screen {\nbackground: $background;\ncolor: black;\n}\nVerticalScroll {\nwidth: 1fr;\n}\nStatic {\nmargin: 1 2;\nbackground: green 80%;\nborder: green wide;\ncolor: white 90%;\nheight: auto;\n}\n#right {\noverflow-y: hidden;\n}\n
"},{"location":"styles/overflow/#css","title":"CSS","text":"
/* Automatic scrollbars on both axes (the default) */\noverflow: auto auto;\n/* Hide the vertical scrollbar */\noverflow-y: hidden;\n/* Always show the horizontal scrollbar */\noverflow-x: scroll;\n
"},{"location":"styles/overflow/#python","title":"Python","text":"

Overflow cannot be programmatically set for both axes at the same time.

# Hide the vertical scrollbar\nwidget.styles.overflow_y = \"hidden\"\n# Always show the horizontal scrollbar\nwidget.styles.overflow_x = \"scroll\"\n
"},{"location":"styles/padding/","title":"Padding","text":"

The padding style specifies spacing around the content of a widget.

"},{"location":"styles/padding/#syntax","title":"Syntax","text":"
\npadding: <integer> # one value for all edges\n       | <integer> <integer>\n       # top/bot   left/right\n       | <integer> <integer> <integer> <integer>;\n       # top       right     bot       left\n\npadding-top: <integer>;\npadding-right: <integer>;\npadding-bottom: <integer>;\npadding-left: <integer>;\n

The padding specifies spacing around the content of a widget, thus this spacing is added inside the widget. The values of the <integer> determine how much spacing is added and the number of values define what edges get what padding:

  • 1 <integer> sets the same padding for the four edges of the widget;
  • 2 <integer> set padding for top/bottom and left/right edges, respectively.
  • 4 <integer> set padding for the top, right, bottom, and left edges, respectively.

Tip

To remember the order of the edges affected by the rule padding when it has 4 values, think of a clock. Its hand starts at the top and then goes clockwise: top, right, bottom, left.

Alternatively, padding can be set for each edge individually through the rules padding-top, padding-right, padding-bottom, and padding-left, respectively.

"},{"location":"styles/padding/#example","title":"Example","text":""},{"location":"styles/padding/#basic-usage","title":"Basic usage","text":"

This example adds padding around some text.

Outputpadding.pypadding.tcss

PaddingApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0 path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0 remain.

from textual.app import App\nfrom textual.widgets import Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass PaddingApp(App):\ndef compose(self):\nyield Label(TEXT)\napp = PaddingApp(css_path=\"padding.tcss\")\n
Screen {\nbackground: white;\ncolor: blue;\n}\nLabel {\npadding: 4 8;\nbackground: blue 20%;\nwidth: 100%;\n}\n
"},{"location":"styles/padding/#all-padding-settings","title":"All padding settings","text":"

The next example shows a grid. In each cell, we have a placeholder that has its padding set in different ways. The effect of each padding setting is noticeable in the colored background around the text of each placeholder.

Outputpadding_all.pypadding_all.tcss

PaddingAllApp no\u00a0padding padding:\u00a01padding:padding:\u00a01\u00a01 1\u00a052\u00a06 padding-right:\u00a03padding-bottom:\u00a04padding-left:\u00a03 padding-top:\u00a04

from textual.app import App\nfrom textual.containers import Container, Grid\nfrom textual.widgets import Placeholder\nclass PaddingAllApp(App):\ndef compose(self):\nyield Grid(\nPlaceholder(\"no padding\", id=\"p1\"),\nPlaceholder(\"padding: 1\", id=\"p2\"),\nPlaceholder(\"padding: 1 5\", id=\"p3\"),\nPlaceholder(\"padding: 1 1 2 6\", id=\"p4\"),\nPlaceholder(\"padding-top: 4\", id=\"p5\"),\nPlaceholder(\"padding-right: 3\", id=\"p6\"),\nPlaceholder(\"padding-bottom: 4\", id=\"p7\"),\nPlaceholder(\"padding-left: 3\", id=\"p8\"),\n)\napp = PaddingAllApp(css_path=\"padding_all.tcss\")\n
Screen {\nbackground: $background;\n}\nGrid {\ngrid-size: 4;\ngrid-gutter: 1 2;\n}\nPlaceholder {\nwidth: auto;\nheight: auto;\n}\n#p1 {\n/* default is no padding */\n}\n#p2 {\npadding: 1;\n}\n#p3 {\npadding: 1 5;\n}\n#p4 {\npadding: 1 1 2 6;\n}\n#p5 {\npadding-top: 4;\n}\n#p6 {\npadding-right: 3;\n}\n#p7 {\npadding-bottom: 4;\n}\n#p8 {\npadding-left: 3;\n}\n
"},{"location":"styles/padding/#css","title":"CSS","text":"
/* Set padding of 1 around all edges */\npadding: 1;\n/* Set padding of 2 on the top and bottom edges, and 4 on the left and right */\npadding: 2 4;\n/* Set padding of 1 on the top, 2 on the right,\n3 on the bottom, and 4 on the left */\npadding: 1 2 3 4;\npadding-top: 1;\npadding-right: 2;\npadding-bottom: 3;\npadding-left: 4;\n
"},{"location":"styles/padding/#python","title":"Python","text":"

In Python, you cannot set any of the individual padding styles padding-top, padding-right, padding-bottom, and padding-left.

However, you can set padding to a single integer, a tuple of 2 integers, or a tuple of 4 integers:

# Set padding of 1 around all edges\nwidget.styles.padding = 1\n# Set padding of 2 on the top and bottom edges, and 4 on the left and right\nwidget.styles.padding = (2, 4)\n# Set padding of 1 on top, 2 on the right, 3 on the bottom, and 4 on the left\nwidget.styles.padding = (1, 2, 3, 4)\n
"},{"location":"styles/padding/#see-also","title":"See also","text":"
  • box-sizing to specify how to account for padding in a widget's dimensions.
  • padding to add spacing around a widget.
"},{"location":"styles/scrollbar_gutter/","title":"Scrollbar-gutter","text":"

The scrollbar-gutter style allows reserving space for a vertical scrollbar.

"},{"location":"styles/scrollbar_gutter/#syntax","title":"Syntax","text":"
\nscrollbar-gutter: auto | stable;\n
"},{"location":"styles/scrollbar_gutter/#values","title":"Values","text":"Value Description auto (default) No space is reserved for a vertical scrollbar. stable Space is reserved for a vertical scrollbar.

Setting the value to stable prevents unwanted layout changes when the scrollbar becomes visible, whereas the default value of auto means that the layout of your application is recomputed when a vertical scrollbar becomes needed.

"},{"location":"styles/scrollbar_gutter/#example","title":"Example","text":"

In the example below, notice the gap reserved for the scrollbar on the right side of the terminal window.

Outputscrollbar_gutter.pyscrollbar_gutter.tcss

ScrollbarGutterApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.

from textual.app import App\nfrom textual.widgets import Static\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass ScrollbarGutterApp(App):\ndef compose(self):\nyield Static(TEXT, id=\"text-box\")\napp = ScrollbarGutterApp(css_path=\"scrollbar_gutter.tcss\")\n
Screen {\nscrollbar-gutter: stable;\n}\n#text-box {\ncolor: floralwhite;\nbackground: darkmagenta;\n}\n
"},{"location":"styles/scrollbar_gutter/#css","title":"CSS","text":"
scrollbar-gutter: auto;    /* Don't reserve space for a vertical scrollbar. */\nscrollbar-gutter: stable;  /* Reserve space for a vertical scrollbar. */\n
"},{"location":"styles/scrollbar_gutter/#python","title":"Python","text":"
self.styles.scrollbar_gutter = \"auto\"    # Don't reserve space for a vertical scrollbar.\nself.styles.scrollbar_gutter = \"stable\"  # Reserve space for a vertical scrollbar.\n
"},{"location":"styles/scrollbar_size/","title":"Scrollbar-size","text":"

The scrollbar-size style defines the width of the scrollbars.

"},{"location":"styles/scrollbar_size/#syntax","title":"Syntax","text":"
\nscrollbar-size: <integer> <integer>;\n              # horizontal vertical\n\nscrollbar-size-horizontal: <integer>;\nscrollbar-size-vertical: <integer>;\n

The scrollbar-size style takes two <integer> to set the horizontal and vertical scrollbar sizes, respectively. This customisable size is the width of the scrollbar, given that its length will always be 100% of the container.

The scrollbar widths may also be set individually with scrollbar-size-horizontal and scrollbar-size-vertical.

"},{"location":"styles/scrollbar_size/#examples","title":"Examples","text":""},{"location":"styles/scrollbar_size/#basic-usage","title":"Basic usage","text":"

In this example we modify the size of the widget's scrollbar to be much larger than usual.

Outputscrollbar_size.pyscrollbar_size.tcss

ScrollbarApp I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.\u2581\u2581\u2581\u2581 I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain. I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear.

from textual.app import App\nfrom textual.containers import ScrollableContainer\nfrom textual.widgets import Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\nclass ScrollbarApp(App):\ndef compose(self):\nyield ScrollableContainer(Label(TEXT * 5), classes=\"panel\")\napp = ScrollbarApp(css_path=\"scrollbar_size.tcss\")\n
Screen {\nbackground: white;\ncolor: blue 80%;\nlayout: horizontal;\n}\nLabel {\npadding: 1 2;\nwidth: 200;\n}\n.panel {\nscrollbar-size: 10 4;\npadding: 1 2;\n}\n
"},{"location":"styles/scrollbar_size/#scrollbar-sizes-comparison","title":"Scrollbar sizes comparison","text":"

In the next example we show three containers with differently sized scrollbars.

Outputscrollbar_size2.pyscrollbar_size2.tcss

ScrollbarApp I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0Fear\u00a0is\u00a0the\u00a0little-death\u00a0tFear\u00a0is\u00a0the\u00a0little-death\u00a0 I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0oI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0pastAnd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0tWhere\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0thWhere\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0t I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0Fear\u00a0is\u00a0the\u00a0little-death\u00a0tFear\u00a0is\u00a0the\u00a0little-death\u00a0 I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2587I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear.\u2587\u2587 I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0oI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0 And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0pastAnd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0tWhere\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0thWhere\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0t I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear.\u2582I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0Fear\u00a0is\u00a0the\u00a0little-death\u00a0tFear\u00a0is\u00a0the\u00a0little-death\u00a0 I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0oI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0 \u258fAnd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u258f \u258fWhere\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0th\u258f \u258fI\u00a0must\u00a0not\u00a0fear.\u258f \u258fFear\u00a0is\u00a0the\u00a0mind-killer.\u258f \u258f\u2589\u258f

from textual.app import App\nfrom textual.containers import Horizontal, ScrollableContainer\nfrom textual.widgets import Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\nclass ScrollbarApp(App):\ndef compose(self):\nyield Horizontal(\nScrollableContainer(Label(TEXT * 5), id=\"v1\"),\nScrollableContainer(Label(TEXT * 5), id=\"v2\"),\nScrollableContainer(Label(TEXT * 5), id=\"v3\"),\n)\napp = ScrollbarApp(css_path=\"scrollbar_size2.tcss\")\nif __name__ == \"__main__\":\napp.run()\n
ScrollableContainer {\nwidth: 1fr;\n}\n#v1 {\nscrollbar-size: 5 1;\nbackground: red 20%;\n}\n#v2 {\nscrollbar-size-vertical: 1;\nbackground: green 20%;\n}\n#v3 {\nscrollbar-size-horizontal: 5;\nbackground: blue 20%;\n}\n
"},{"location":"styles/scrollbar_size/#css","title":"CSS","text":"
/* Set horizontal scrollbar to 10, and vertical scrollbar to 4 */\nscrollbar-size: 10 4;\n/* Set horizontal scrollbar to 10 */\nscrollbar-size-horizontal: 10;\n/* Set vertical scrollbar to 4 */\nscrollbar-size-vertical: 4;\n
"},{"location":"styles/scrollbar_size/#python","title":"Python","text":"

The style scrollbar-size has no Python equivalent. The scrollbar sizes must be set independently:

# Set horizontal scrollbar to 10:\nwidget.styles.scrollbar_size_horizontal = 10\n# Set vertical scrollbar to 4:\nwidget.styles.scrollbar_size_vertical = 4\n
"},{"location":"styles/text_align/","title":"Text-align","text":"

The text-align style sets the text alignment in a widget.

"},{"location":"styles/text_align/#syntax","title":"Syntax","text":"
\ntext-align: <text-align>;\n

The text-align style accepts a value of the type <text-align> that defines how text is aligned inside the widget.

"},{"location":"styles/text_align/#defaults","title":"Defaults","text":"

The default value is start.

"},{"location":"styles/text_align/#example","title":"Example","text":"

This example shows, from top to bottom: left, center, right, and justify text alignments.

Outputtext_align.pytext_align.tcss

TextAlign Left\u00a0alignedCenter\u00a0aligned I\u00a0must\u00a0not\u00a0fear.\u00a0Fear\u00a0is\u00a0the\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0I\u00a0must\u00a0not\u00a0fear.\u00a0Fear\u00a0is\u00a0the\u00a0\u00a0\u00a0\u00a0 mind-killer.\u00a0Fear\u00a0is\u00a0the\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0mind-killer.\u00a0Fear\u00a0is\u00a0the\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 little-death\u00a0that\u00a0brings\u00a0total\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0\u00a0\u00a0 obliteration.\u00a0I\u00a0will\u00a0face\u00a0my\u00a0fear.\u00a0Iobliteration.\u00a0I\u00a0will\u00a0face\u00a0my\u00a0fear.\u00a0I will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0\u00a0\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0 through\u00a0me.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0through\u00a0me.\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 Right\u00a0alignedJustified \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0I\u00a0must\u00a0not\u00a0fear.\u00a0Fear\u00a0is\u00a0theI\u00a0\u00a0must\u00a0\u00a0not\u00a0\u00a0fear.\u00a0\u00a0Fear\u00a0\u00a0\u00a0is\u00a0\u00a0\u00a0the \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0mind-killer.\u00a0Fear\u00a0is\u00a0themind-killer.\u00a0\u00a0\u00a0\u00a0\u00a0Fear\u00a0\u00a0\u00a0\u00a0\u00a0is\u00a0\u00a0\u00a0\u00a0\u00a0the \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0little-death\u00a0that\u00a0brings\u00a0totallittle-death\u00a0\u00a0\u00a0that\u00a0\u00a0\u00a0brings\u00a0\u00a0\u00a0total obliteration.\u00a0I\u00a0will\u00a0face\u00a0my\u00a0fear.\u00a0Iobliteration.\u00a0I\u00a0will\u00a0face\u00a0my\u00a0fear.\u00a0I \u00a0\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0andwill\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0\u00a0me\u00a0\u00a0and \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0through\u00a0me.through\u00a0me.

from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Label\nTEXT = (\n\"I must not fear. Fear is the mind-killer. Fear is the little-death that \"\n\"brings total obliteration. I will face my fear. I will permit it to pass over \"\n\"me and through me.\"\n)\nclass TextAlign(App):\ndef compose(self):\nyield Grid(\nLabel(\"[b]Left aligned[/]\\n\" + TEXT, id=\"one\"),\nLabel(\"[b]Center aligned[/]\\n\" + TEXT, id=\"two\"),\nLabel(\"[b]Right aligned[/]\\n\" + TEXT, id=\"three\"),\nLabel(\"[b]Justified[/]\\n\" + TEXT, id=\"four\"),\n)\napp = TextAlign(css_path=\"text_align.tcss\")\n
#one {\ntext-align: left;\nbackground: lightblue;\n}\n#two {\ntext-align: center;\nbackground: indianred;\n}\n#three {\ntext-align: right;\nbackground: palegreen;\n}\n#four {\ntext-align: justify;\nbackground: palevioletred;\n}\nLabel {\npadding: 1 2;\nheight: 100%;\ncolor: auto;\n}\nGrid {\ngrid-size: 2 2;\n}\n
"},{"location":"styles/text_align/#css","title":"CSS","text":"
/* Set text in the widget to be right aligned */\ntext-align: right;\n
"},{"location":"styles/text_align/#python","title":"Python","text":"
# Set text in the widget to be right aligned\nwidget.styles.text_align = \"right\"\n
"},{"location":"styles/text_align/#see-also","title":"See also","text":"
  • align to set the alignment of children widgets inside a container.
  • content-align to set the alignment of content inside a widget.
"},{"location":"styles/text_opacity/","title":"Text-opacity","text":"

The text-opacity style blends the foreground color (i.e. text) with the background color.

"},{"location":"styles/text_opacity/#syntax","title":"Syntax","text":"
\ntext-opacity: <number> | <percentage>;\n

The text opacity of a widget can be set as a <number> or a <percentage>. If given as a number, then text-opacity should be a value between 0 and 1, where 0 makes the foreground color match the background (effectively making text invisible) and 1 will display text as normal. If given as a percentage, 0% will result in invisible text, and 100% will display fully opaque text.

Typically, if you set this value it would be somewhere between the two extremes. For instance, setting text-opacity to 70% would result in slightly faded text. Setting it to 0.3 would result in very dim text.

Warning

Be careful not to set text opacity so low as to make it hard to read.

"},{"location":"styles/text_opacity/#example","title":"Example","text":"

This example shows, from top to bottom, increasing text-opacity values.

Outputtext_opacity.pytext_opacity.tcss

TextOpacityApp \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0text-opacity:\u00a025%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0text-opacity:\u00a050%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0text-opacity:\u00a075%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0text-opacity:\u00a0100%\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0

from textual.app import App\nfrom textual.widgets import Label\nclass TextOpacityApp(App):\ndef compose(self):\nyield Label(\"text-opacity: 0%\", id=\"zero-opacity\")\nyield Label(\"text-opacity: 25%\", id=\"quarter-opacity\")\nyield Label(\"text-opacity: 50%\", id=\"half-opacity\")\nyield Label(\"text-opacity: 75%\", id=\"three-quarter-opacity\")\nyield Label(\"text-opacity: 100%\", id=\"full-opacity\")\napp = TextOpacityApp(css_path=\"text_opacity.tcss\")\n
#zero-opacity {\ntext-opacity: 0%;\n}\n#quarter-opacity {\ntext-opacity: 25%;\n}\n#half-opacity {\ntext-opacity: 50%;\n}\n#three-quarter-opacity {\ntext-opacity: 75%;\n}\n#full-opacity {\ntext-opacity: 100%;\n}\nLabel {\nheight: 1fr;\nwidth: 100%;\ntext-align: center;\ntext-style: bold;\n}\n
"},{"location":"styles/text_opacity/#css","title":"CSS","text":"
/* Set the text to be \"half-faded\" against the background of the widget */\ntext-opacity: 50%;\n
"},{"location":"styles/text_opacity/#python","title":"Python","text":"
# Set the text to be \"half-faded\" against the background of the widget\nwidget.styles.text_opacity = \"50%\"\n
"},{"location":"styles/text_opacity/#see-also","title":"See also","text":"
  • opacity to specify the opacity of a whole widget.
"},{"location":"styles/text_style/","title":"Text-style","text":"

The text-style style sets the style for the text in a widget.

"},{"location":"styles/text_style/#syntax","title":"Syntax","text":"
\ntext-style: <text-style>;\n

text-style will take all the values specified and will apply that styling combination to the text in the widget.

"},{"location":"styles/text_style/#examples","title":"Examples","text":""},{"location":"styles/text_style/#basic-usage","title":"Basic usage","text":"

Each of the three text panels has a different text style, respectively bold, italic, and reverse, from left to right.

Outputtext_style.pytext_style.tcss

TextStyleApp I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0Fear\u00a0is\u00a0the\u00a0little-death\u00a0Fear\u00a0is\u00a0the\u00a0little-death\u00a0 that\u00a0brings\u00a0total\u00a0that\u00a0brings\u00a0total\u00a0that\u00a0brings\u00a0total\u00a0 obliteration.obliteration.obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0 over\u00a0me\u00a0and\u00a0through\u00a0me.over\u00a0me\u00a0and\u00a0through\u00a0me.over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0 I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0 to\u00a0see\u00a0its\u00a0path.to\u00a0see\u00a0its\u00a0path.to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0 there\u00a0will\u00a0be\u00a0nothing.\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Onlythere\u00a0will\u00a0be\u00a0nothing.\u00a0Only Only\u00a0I\u00a0will\u00a0remain.I\u00a0will\u00a0remain.I\u00a0will\u00a0remain.

from textual.app import App\nfrom textual.widgets import Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass TextStyleApp(App):\ndef compose(self):\nyield Label(TEXT, id=\"lbl1\")\nyield Label(TEXT, id=\"lbl2\")\nyield Label(TEXT, id=\"lbl3\")\napp = TextStyleApp(css_path=\"text_style.tcss\")\n
Screen {\nlayout: horizontal;\n}\nLabel {\nwidth: 1fr;\n}\n#lbl1 {\nbackground: red 30%;\ntext-style: bold;\n}\n#lbl2 {\nbackground: green 30%;\ntext-style: italic;\n}\n#lbl3 {\nbackground: blue 30%;\ntext-style: reverse;\n}\n
"},{"location":"styles/text_style/#all-text-styles","title":"All text styles","text":"

The next example shows all different text styles on their own, as well as some combinations of styles in a single widget.

Outputtext_style_all.pytext_style_all.tcss

AllTextStyleApp nonebolditalicreverse I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0 mind-killer.mind-killer.mind-killer.mind-killer. Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0 little-death\u00a0thatlittle-death\u00a0that\u00a0little-death\u00a0thatlittle-death\u00a0that\u00a0 brings\u00a0total\u00a0brings\u00a0total\u00a0brings\u00a0total\u00a0brings\u00a0total\u00a0 obliteration.obliteration.obliteration.obliteration. I\u00a0will\u00a0face\u00a0my\u00a0I\u00a0will\u00a0face\u00a0my\u00a0I\u00a0will\u00a0face\u00a0my\u00a0I\u00a0will\u00a0face\u00a0my\u00a0 fear.fear.fear.fear. strikeunderlinebold\u00a0italicreverse\u00a0strike I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0 mind-killer.mind-killer.mind-killer.mind-killer. Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0Fear\u00a0is\u00a0the\u00a0 little-death\u00a0thatlittle-death\u00a0that\u00a0little-death\u00a0thatlittle-death\u00a0that\u00a0 brings\u00a0total\u00a0brings\u00a0total\u00a0brings\u00a0total\u00a0brings\u00a0total\u00a0 obliteration.obliteration.obliteration.obliteration. I\u00a0will\u00a0face\u00a0my\u00a0I\u00a0will\u00a0face\u00a0my\u00a0I\u00a0will\u00a0face\u00a0my\u00a0I\u00a0will\u00a0face\u00a0my\u00a0 fear.fear.fear.fear. I\u00a0will\u00a0permit\u00a0it\u00a0I\u00a0will\u00a0permit\u00a0it\u00a0I\u00a0will\u00a0permit\u00a0it\u00a0I\u00a0will\u00a0permit\u00a0it\u00a0

from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass AllTextStyleApp(App):\ndef compose(self):\nyield Grid(\nLabel(\"none\\n\" + TEXT, id=\"lbl1\"),\nLabel(\"bold\\n\" + TEXT, id=\"lbl2\"),\nLabel(\"italic\\n\" + TEXT, id=\"lbl3\"),\nLabel(\"reverse\\n\" + TEXT, id=\"lbl4\"),\nLabel(\"strike\\n\" + TEXT, id=\"lbl5\"),\nLabel(\"underline\\n\" + TEXT, id=\"lbl6\"),\nLabel(\"bold italic\\n\" + TEXT, id=\"lbl7\"),\nLabel(\"reverse strike\\n\" + TEXT, id=\"lbl8\"),\n)\napp = AllTextStyleApp(css_path=\"text_style_all.tcss\")\n
#lbl1 {\ntext-style: none;\n}\n#lbl2 {\ntext-style: bold;\n}\n#lbl3 {\ntext-style: italic;\n}\n#lbl4 {\ntext-style: reverse;\n}\n#lbl5 {\ntext-style: strike;\n}\n#lbl6 {\ntext-style: underline;\n}\n#lbl7 {\ntext-style: bold italic;\n}\n#lbl8 {\ntext-style: reverse strike;\n}\nGrid {\ngrid-size: 4;\ngrid-gutter: 1 2;\nmargin: 1 2;\nheight: 100%;\n}\nLabel {\nheight: 100%;\n}\n
"},{"location":"styles/text_style/#css","title":"CSS","text":"
text-style: italic;\n
"},{"location":"styles/text_style/#python","title":"Python","text":"
widget.styles.text_style = \"italic\"\n
"},{"location":"styles/tint/","title":"Tint","text":"

The tint style blends a color with the whole widget.

"},{"location":"styles/tint/#syntax","title":"Syntax","text":"
\ntint: <color> [<percentage>];\n

The tint style blends a <color> with the widget. The color should likely have an alpha component (specified directly in the color used or by the optional <percentage>), otherwise the end result will obscure the widget content.

"},{"location":"styles/tint/#example","title":"Example","text":"

This examples shows a green tint with gradually increasing alpha.

Outputtint.pytint.tcss

TintApp tint:\u00a0green\u00a00%; tint:\u00a0green\u00a010%; tint:\u00a0green\u00a020%; tint:\u00a0green\u00a030%; tint:\u00a0green\u00a040%; tint:\u00a0green\u00a050%; \u2584\u2584 tint:\u00a0green\u00a060%; tint:\u00a0green\u00a070%;

from textual.app import App\nfrom textual.color import Color\nfrom textual.widgets import Label\nclass TintApp(App):\ndef compose(self):\ncolor = Color.parse(\"green\")\nfor tint_alpha in range(0, 101, 10):\nwidget = Label(f\"tint: green {tint_alpha}%;\")\nwidget.styles.tint = color.with_alpha(tint_alpha / 100)  # (1)!\nyield widget\napp = TintApp(css_path=\"tint.tcss\")\n
  1. We set the tint to a Color instance with varying levels of opacity, set through the method with_alpha.
Label {\nheight: 3;\nwidth: 100%;\ntext-style: bold;\nbackground: white;\ncolor: black;\ncontent-align: center middle;\n}\n
"},{"location":"styles/tint/#css","title":"CSS","text":"
/* A red tint (could indicate an error) */\ntint: red 20%;\n/* A green tint */\ntint: rgba(0, 200, 0, 0.3);\n
"},{"location":"styles/tint/#python","title":"Python","text":"
# A red tint\nfrom textual.color import Color\nwidget.styles.tint = Color.parse(\"red\").with_alpha(0.2);\n# A green tint\nwidget.styles.tint = \"rgba(0, 200, 0, 0.3)\"\n
"},{"location":"styles/visibility/","title":"Visibility","text":"

The visibility style determines whether a widget is visible or not.

"},{"location":"styles/visibility/#syntax","title":"Syntax","text":"
\nvisibility: hidden | visible;\n

visibility takes one of two values to set the visibility of a widget.

"},{"location":"styles/visibility/#values","title":"Values","text":"Value Description hidden The widget will be invisible. visible (default) The widget will be displayed as normal."},{"location":"styles/visibility/#visibility-inheritance","title":"Visibility inheritance","text":"

Note

Children of an invisible container can be visible.

By default, children inherit the visibility of their parents. So, if a container is set to be invisible, its children widgets will also be invisible by default. However, those widgets can be made visible if their visibility is explicitly set to visibility: visible. This is shown in the second example below.

"},{"location":"styles/visibility/#examples","title":"Examples","text":""},{"location":"styles/visibility/#basic-usage","title":"Basic usage","text":"

Note that the second widget is hidden while leaving a space where it would have been rendered.

Outputvisibility.pyvisibility.tcss

VisibilityApp \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Widget\u00a01\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503Widget\u00a03\u2503 \u2503\u2503 \u2503\u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b

from textual.app import App\nfrom textual.widgets import Label\nclass VisibilityApp(App):\ndef compose(self):\nyield Label(\"Widget 1\")\nyield Label(\"Widget 2\", classes=\"invisible\")\nyield Label(\"Widget 3\")\napp = VisibilityApp(css_path=\"visibility.tcss\")\n
Screen {\nbackground: green;\n}\nLabel {\nheight: 5;\nwidth: 100%;\nbackground: white;\ncolor: blue;\nborder: heavy blue;\n}\nLabel.invisible {\nvisibility: hidden;\n}\n
"},{"location":"styles/visibility/#overriding-container-visibility","title":"Overriding container visibility","text":"

The next example shows the interaction of the visibility style with invisible containers that have visible children. The app below has three rows with a Horizontal container per row and three placeholders per row. The containers all have a white background, and then:

  • the top container is visible by default (we can see the white background around the placeholders);
  • the middle container is invisible and the children placeholders inherited that setting;
  • the bottom container is invisible but the children placeholders are visible because they were set to be visible.
Outputvisibility_containers.pyvisibility_containers.tcss

VisibilityContainersApp PlaceholderPlaceholderPlaceholder PlaceholderPlaceholderPlaceholder

from textual.app import App\nfrom textual.containers import Horizontal, VerticalScroll\nfrom textual.widgets import Placeholder\nclass VisibilityContainersApp(App):\ndef compose(self):\nyield VerticalScroll(\nHorizontal(\nPlaceholder(),\nPlaceholder(),\nPlaceholder(),\nid=\"top\",\n),\nHorizontal(\nPlaceholder(),\nPlaceholder(),\nPlaceholder(),\nid=\"middle\",\n),\nHorizontal(\nPlaceholder(),\nPlaceholder(),\nPlaceholder(),\nid=\"bot\",\n),\n)\napp = VisibilityContainersApp(css_path=\"visibility_containers.tcss\")\n
Horizontal {\npadding: 1 2;     /* (1)! */\nbackground: white;\nheight: 1fr;\n}\n#top {}               /* (2)! */\n#middle {             /* (3)! */\nvisibility: hidden;\n}\n#bot {                /* (4)! */\nvisibility: hidden;\n}\n#bot > Placeholder {  /* (5)! */\nvisibility: visible;\n}\nPlaceholder {\nwidth: 1fr;\n}\n
  1. The padding and the white background let us know when the Horizontal is visible.
  2. The top Horizontal is visible by default, and so are its children.
  3. The middle Horizontal is made invisible and its children will inherit that setting.
  4. The bottom Horizontal is made invisible...
  5. ... but its children override that setting and become visible.
"},{"location":"styles/visibility/#css","title":"CSS","text":"
/* Widget is invisible */\nvisibility: hidden;\n/* Widget is visible */\nvisibility: visible;\n
"},{"location":"styles/visibility/#python","title":"Python","text":"
# Widget is invisible\nself.styles.visibility = \"hidden\"\n# Widget is visible\nself.styles.visibility = \"visible\"\n

There is also a shortcut to set a Widget's visibility. The visible property on Widget may be set to True or False.

# Make a widget invisible\nwidget.visible = False\n# Make the widget visible again\nwidget.visible = True\n
"},{"location":"styles/visibility/#see-also","title":"See also","text":"
  • display to specify whether a widget is displayed or not.
"},{"location":"styles/width/","title":"Width","text":"

The width style sets a widget's width.

"},{"location":"styles/width/#syntax","title":"Syntax","text":"
\nwidth: <scalar>;\n

The style width needs a <scalar> to determine the horizontal length of the width. By default, it sets the width of the content area, but if box-sizing is set to border-box it sets the width of the border area.

"},{"location":"styles/width/#examples","title":"Examples","text":""},{"location":"styles/width/#basic-usage","title":"Basic usage","text":"

This example adds a widget with 50% width of the screen.

Outputwidth.pywidth.tcss

WidthApp Widget

from textual.app import App\nfrom textual.widget import Widget\nclass WidthApp(App):\ndef compose(self):\nyield Widget()\napp = WidthApp(css_path=\"width.tcss\")\n
Screen > Widget {\nbackground: green;\nwidth: 50%;\ncolor: white;\n}\n
"},{"location":"styles/width/#all-width-formats","title":"All width formats","text":"Outputwidth_comparison.pywidth_comparison.tcss

WidthComparisonApp #cells#percent#w#h#vw#vh#auto#fr1#fr3 \u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022\u00b7\u00b7\u00b7\u00b7\u2022

from textual.app import App\nfrom textual.containers import Horizontal\nfrom textual.widgets import Label, Placeholder, Static\nclass Ruler(Static):\ndef compose(self):\nruler_text = \"\u00b7\u00b7\u00b7\u00b7\u2022\" * 100\nyield Label(ruler_text)\nclass WidthComparisonApp(App):\ndef compose(self):\nyield Horizontal(\nPlaceholder(id=\"cells\"),  # (1)!\nPlaceholder(id=\"percent\"),\nPlaceholder(id=\"w\"),\nPlaceholder(id=\"h\"),\nPlaceholder(id=\"vw\"),\nPlaceholder(id=\"vh\"),\nPlaceholder(id=\"auto\"),\nPlaceholder(id=\"fr1\"),\nPlaceholder(id=\"fr3\"),\n)\nyield Ruler()\napp = WidthComparisonApp(css_path=\"width_comparison.tcss\")\nif __name__ == \"__main__\":\napp.run()\n
  1. The id of the placeholder identifies which unit will be used to set the width of the widget.
#cells {\nwidth: 9;      /* (1)! */\n}\n#percent {\nwidth: 12.5%;  /* (2)! */\n}\n#w {\nwidth: 10w;    /* (3)! */\n}\n#h {\nwidth: 25h;    /* (4)! */\n}\n#vw {\nwidth: 15vw;   /* (5)! */\n}\n#vh {\nwidth: 25vh;   /* (6)! */\n}\n#auto {\nwidth: auto;   /* (7)! */\n}\n#fr1 {\nwidth: 1fr;    /* (8)! */\n}\n#fr3 {\nwidth: 3fr;    /* (9)! */\n}\nScreen {\nlayers: ruler;\n}\nRuler {\nlayer: ruler;\ndock: bottom;\noverflow: hidden;\nheight: 1;\nbackground: $accent;\n}\n
  1. This sets the width to 9 columns.
  2. This sets the width to 12.5% of the space made available by the container. The container is 80 columns wide, so 12.5% of 80 is 10.
  3. This sets the width to 10% of the width of the direct container, which is the Horizontal container. Because it expands to fit all of the terminal, the width of the Horizontal is 80 and 10% of 80 is 8.
  4. This sets the width to 25% of the height of the direct container, which is the Horizontal container. Because it expands to fit all of the terminal, the height of the Horizontal is 24 and 25% of 24 is 6.
  5. This sets the width to 15% of the viewport width, which is 80. 15% of 80 is 12.
  6. This sets the width to 25% of the viewport height, which is 24. 25% of 24 is 6.
  7. This sets the width of the placeholder to be the optimal size that fits the content without scrolling. Because the content is the string \"#auto\", the placeholder has its width set to 5.
  8. This sets the width to 1fr, which means this placeholder will have a third of the width of a placeholder with 3fr.
  9. This sets the width to 3fr, which means this placeholder will have triple the width of a placeholder with 1fr.
"},{"location":"styles/width/#css","title":"CSS","text":"
/* Explicit cell width */\nwidth: 10;\n/* Percentage width */\nwidth: 50%;\n/* Automatic width */\nwidth: auto;\n
"},{"location":"styles/width/#python","title":"Python","text":"
widget.styles.width = 10\nwidget.styles.width = \"50%\nwidget.styles.width = \"auto\"\n
"},{"location":"styles/width/#see-also","title":"See also","text":"
  • max-width and min-width to limit the width of a widget.
  • height to set the height of a widget.
"},{"location":"styles/grid/","title":"Grid","text":"

There are a number of styles relating to the Textual grid layout.

For an in-depth look at the grid layout, visit the grid guide.

Property Description column-span Number of columns a cell spans. grid-columns Width of grid columns. grid-gutter Spacing between grid cells. grid-rows Height of grid rows. grid-size Number of columns and rows in the grid layout. row-span Number of rows a cell spans."},{"location":"styles/grid/#syntax","title":"Syntax","text":"
\ncolumn-span: <integer>;\n\ngrid-columns: <scalar>+;\n\ngrid-gutter: <scalar> [<scalar>];\n\ngrid-rows: <scalar>+;\n\ngrid-size: <integer> [<integer>];\n\nrow-span: <integer>;\n

Visit each style's reference page to learn more about how the values are used.

"},{"location":"styles/grid/#example","title":"Example","text":"

The example below shows all the styles above in action. The grid-size: 3 4; declaration sets the grid to 3 columns and 4 rows. The first cell of the grid, tinted magenta, shows a cell spanning multiple rows and columns. The spacing between grid cells is defined by the grid-gutter style.

Outputgrid.pygrid.tcss

GridApp Grid\u00a0cell\u00a01Grid\u00a0cell\u00a02 row-span:\u00a03; column-span:\u00a02; Grid\u00a0cell\u00a03 Grid\u00a0cell\u00a04 Grid\u00a0cell\u00a05Grid\u00a0cell\u00a06Grid\u00a0cell\u00a07

from textual.app import App\nfrom textual.widgets import Static\nclass GridApp(App):\ndef compose(self):\nyield Static(\"Grid cell 1\\n\\nrow-span: 3;\\ncolumn-span: 2;\", id=\"static1\")\nyield Static(\"Grid cell 2\", id=\"static2\")\nyield Static(\"Grid cell 3\", id=\"static3\")\nyield Static(\"Grid cell 4\", id=\"static4\")\nyield Static(\"Grid cell 5\", id=\"static5\")\nyield Static(\"Grid cell 6\", id=\"static6\")\nyield Static(\"Grid cell 7\", id=\"static7\")\napp = GridApp(css_path=\"grid.tcss\")\n
Screen {\nlayout: grid;\ngrid-size: 3 4;\ngrid-rows: 1fr;\ngrid-columns: 1fr;\ngrid-gutter: 1;\n}\nStatic {\ncolor: auto;\nbackground: lightblue;\nheight: 100%;\npadding: 1 2;\n}\n#static1 {\ntint: magenta 40%;\nrow-span: 3;\ncolumn-span: 2;\n}\n

Warning

The styles listed on this page will only work when the layout is grid.

"},{"location":"styles/grid/#see-also","title":"See also","text":"
  • The grid layout guide.
"},{"location":"styles/grid/column_span/","title":"Column-span","text":"

The column-span style specifies how many columns a widget will span in a grid layout.

Note

This style only affects widgets that are direct children of a widget with layout: grid.

"},{"location":"styles/grid/column_span/#syntax","title":"Syntax","text":"
\ncolumn-span: <integer>;\n

The column-span style accepts a single non-negative <integer> that quantifies how many columns the given widget spans.

"},{"location":"styles/grid/column_span/#example","title":"Example","text":"

The example below shows a 4 by 4 grid where many placeholders span over several columns.

Outputcolumn_span.pycolumn_span.tcss

MyApp #p1 #p2#p3 #p4#p5 #p6#p7

from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Placeholder\nclass MyApp(App):\ndef compose(self):\nyield Grid(\nPlaceholder(id=\"p1\"),\nPlaceholder(id=\"p2\"),\nPlaceholder(id=\"p3\"),\nPlaceholder(id=\"p4\"),\nPlaceholder(id=\"p5\"),\nPlaceholder(id=\"p6\"),\nPlaceholder(id=\"p7\"),\n)\napp = MyApp(css_path=\"column_span.tcss\")\n
#p1 {\ncolumn-span: 4;\n}\n#p2 {\ncolumn-span: 3;\n}\n#p3 {\ncolumn-span: 1;  /* Didn't need to be set explicitly. */\n}\n#p4 {\ncolumn-span: 2;\n}\n#p5 {\ncolumn-span: 2;\n}\n#p6 {\n/* Default value is 1. */\n}\n#p7 {\ncolumn-span: 3;\n}\nGrid {\ngrid-size: 4 4;\ngrid-gutter: 1 2;\n}\nPlaceholder {\nheight: 100%;\n}\n
"},{"location":"styles/grid/column_span/#css","title":"CSS","text":"
column-span: 3;\n
"},{"location":"styles/grid/column_span/#python","title":"Python","text":"
widget.styles.column_span = 3\n
"},{"location":"styles/grid/column_span/#see-also","title":"See also","text":"
  • row-span to specify how many rows a widget spans.
"},{"location":"styles/grid/grid_columns/","title":"Grid-columns","text":"

The grid-columns style allows to define the width of the columns of the grid.

Note

This style only affects widgets with layout: grid.

"},{"location":"styles/grid/grid_columns/#syntax","title":"Syntax","text":"
\ngrid-columns: <scalar>+;\n

The grid-columns style takes one or more <scalar> that specify the length of the columns of the grid.

If there are more columns in the grid than scalars specified in grid-columns, they are reused cyclically. If the number of <scalar> is in excess, the excess is ignored.

"},{"location":"styles/grid/grid_columns/#example","title":"Example","text":"

The example below shows a grid with 10 labels laid out in a grid with 2 rows and 5 columns.

We set grid-columns: 1fr 16 2fr. Because there are more rows than scalars in the style definition, the scalars will be reused:

  • columns 1 and 4 have width 1fr;
  • columns 2 and 5 have width 16; and
  • column 3 has width 2fr.
Outputgrid_columns.pygrid_columns.tcss

MyApp \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u25021fr\u2502\u2502width\u00a0=\u00a016\u2502\u25022fr\u2502\u25021fr\u2502\u2502width\u00a0=\u00a016\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u25021fr\u2502\u2502width\u00a0=\u00a016\u2502\u25022fr\u2502\u25021fr\u2502\u2502width\u00a0=\u00a016\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Label\nclass MyApp(App):\ndef compose(self):\nyield Grid(\nLabel(\"1fr\"),\nLabel(\"width = 16\"),\nLabel(\"2fr\"),\nLabel(\"1fr\"),\nLabel(\"width = 16\"),\nLabel(\"1fr\"),\nLabel(\"width = 16\"),\nLabel(\"2fr\"),\nLabel(\"1fr\"),\nLabel(\"width = 16\"),\n)\napp = MyApp(css_path=\"grid_columns.tcss\")\n
Grid {\ngrid-size: 5 2;\ngrid-columns: 1fr 16 2fr;\n}\nLabel {\nborder: round white;\ncontent-align-horizontal: center;\nwidth: 100%;\nheight: 100%;\n}\n
"},{"location":"styles/grid/grid_columns/#css","title":"CSS","text":"
/* Set all columns to have 50% width */\ngrid-columns: 50%;\n/* Every other column is twice as wide as the first one */\ngrid-columns: 1fr 2fr;\n
"},{"location":"styles/grid/grid_columns/#python","title":"Python","text":"
grid.styles.grid_columns = \"50%\"\ngrid.styles.grid_columns = \"1fr 2fr\"\n
"},{"location":"styles/grid/grid_columns/#see-also","title":"See also","text":"
  • grid-rows to specify the height of the grid rows.
"},{"location":"styles/grid/grid_gutter/","title":"Grid-gutter","text":"

The grid-gutter style sets the size of the gutter in the grid layout. That is, it sets the space between adjacent cells in the grid.

Gutter is only applied between the edges of cells. No spacing is added between the edges of the cells and the edges of the container.

Note

This style only affects widgets with layout: grid.

"},{"location":"styles/grid/grid_gutter/#syntax","title":"Syntax","text":"
\ngrid-gutter: <integer> [<integer>];\n

The grid-gutter style takes one or two <integer> that set the length of the gutter along the vertical and horizontal axes. If only one <integer> is supplied, it sets the vertical and horizontal gutters. If two are supplied, they set the vertical and horizontal gutters, respectively.

"},{"location":"styles/grid/grid_gutter/#example","title":"Example","text":"

The example below employs a common trick to apply visually consistent spacing around all grid cells.

Outputgrid_gutter.pygrid_gutter.tcss

MyApp \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u25021\u2502\u25022\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u25023\u2502\u25024\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u25025\u2502\u25026\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u25027\u2502\u25028\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Label\nclass MyApp(App):\ndef compose(self):\nyield Grid(\nLabel(\"1\"),\nLabel(\"2\"),\nLabel(\"3\"),\nLabel(\"4\"),\nLabel(\"5\"),\nLabel(\"6\"),\nLabel(\"7\"),\nLabel(\"8\"),\n)\napp = MyApp(css_path=\"grid_gutter.tcss\")\n
Grid {\ngrid-size: 2 4;\ngrid-gutter: 1 2;  /* (1)! */\n}\nLabel {\nborder: round white;\ncontent-align: center middle;\nwidth: 100%;\nheight: 100%;\n}\n
  1. We set the horizontal gutter to be double the vertical gutter because terminal cells are typically two times taller than they are wide. Thus, the result shows visually consistent spacing around grid cells.
"},{"location":"styles/grid/grid_gutter/#css","title":"CSS","text":"
/* Set vertical and horizontal gutters to be the same */\ngrid-gutter: 5;\n/* Set vertical and horizontal gutters separately */\ngrid-gutter: 1 2;\n
"},{"location":"styles/grid/grid_gutter/#python","title":"Python","text":"

Vertical and horizontal gutters correspond to different Python properties, so they must be set separately:

widget.styles.grid_gutter_vertical = \"1\"\nwidget.styles.grid_gutter_horizontal = \"2\"\n
"},{"location":"styles/grid/grid_rows/","title":"Grid-rows","text":"

The grid-rows style allows to define the height of the rows of the grid.

Note

This style only affects widgets with layout: grid.

"},{"location":"styles/grid/grid_rows/#syntax","title":"Syntax","text":"
\ngrid-rows: <scalar>+;\n

The grid-rows style takes one or more <scalar> that specify the length of the rows of the grid.

If there are more rows in the grid than scalars specified in grid-rows, they are reused cyclically. If the number of <scalar> is in excess, the excess is ignored.

"},{"location":"styles/grid/grid_rows/#example","title":"Example","text":"

The example below shows a grid with 10 labels laid out in a grid with 5 rows and 2 columns.

We set grid-rows: 1fr 6 25%. Because there are more rows than scalars in the style definition, the scalars will be reused:

  • rows 1 and 4 have height 1fr;
  • rows 2 and 5 have height 6; and
  • row 3 has height 25%.
Outputgrid_rows.pygrid_rows.tcss

MyApp \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u25021fr\u2502\u25021fr\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u2502height\u00a0=\u00a06\u2502\u2502height\u00a0=\u00a06\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u250225%\u2502\u250225%\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u25021fr\u2502\u25021fr\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u2502height\u00a0=\u00a06\u2502\u2502height\u00a0=\u00a06\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Label\nclass MyApp(App):\ndef compose(self):\nyield Grid(\nLabel(\"1fr\"),\nLabel(\"1fr\"),\nLabel(\"height = 6\"),\nLabel(\"height = 6\"),\nLabel(\"25%\"),\nLabel(\"25%\"),\nLabel(\"1fr\"),\nLabel(\"1fr\"),\nLabel(\"height = 6\"),\nLabel(\"height = 6\"),\n)\napp = MyApp(css_path=\"grid_rows.tcss\")\n
Grid {\ngrid-size: 2 5;\ngrid-rows: 1fr 6 25%;\n}\nLabel {\nborder: round white;\ncontent-align: center middle;\nwidth: 100%;\nheight: 100%;\n}\n
"},{"location":"styles/grid/grid_rows/#css","title":"CSS","text":"
/* Set all rows to have 50% height */\ngrid-rows: 50%;\n/* Every other row is twice as tall as the first one */\ngrid-rows: 1fr 2fr;\n
"},{"location":"styles/grid/grid_rows/#python","title":"Python","text":"
grid.styles.grid_rows = \"50%\"\ngrid.styles.grid_rows = \"1fr 2fr\"\n
"},{"location":"styles/grid/grid_rows/#see-also","title":"See also","text":"
  • grid-columns to specify the width of the grid columns.
"},{"location":"styles/grid/grid_size/","title":"Grid-size","text":"

The grid-size style sets the number of columns and rows in a grid layout.

The number of rows can be left unspecified and it will be computed automatically.

Note

This style only affects widgets with layout: grid.

"},{"location":"styles/grid/grid_size/#syntax","title":"Syntax","text":"
\ngrid-size: <integer> [<integer>];\n

The grid-size style takes one or two non-negative <integer>. The first defines how many columns there are in the grid. If present, the second one sets the number of rows \u2013 regardless of the number of children of the grid \u2013, otherwise the number of rows is computed automatically.

"},{"location":"styles/grid/grid_size/#examples","title":"Examples","text":""},{"location":"styles/grid/grid_size/#columns-and-rows","title":"Columns and rows","text":"

In the first example, we create a grid with 2 columns and 5 rows, although we do not have enough labels to fill in the whole grid:

Outputgrid_size_both.pygrid_size_both.tcss

MyApp \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u25021\u2502\u25022\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u25023\u2502\u25024\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502 \u25025\u2502 \u2502\u2502 \u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Label\nclass MyApp(App):\ndef compose(self):\nyield Grid(\nLabel(\"1\"),\nLabel(\"2\"),\nLabel(\"3\"),\nLabel(\"4\"),\nLabel(\"5\"),\n)\napp = MyApp(css_path=\"grid_size_both.tcss\")\n
Grid {\ngrid-size: 2 4;  /* (1)! */\n}\nLabel {\nborder: round white;\ncontent-align: center middle;\nwidth: 100%;\nheight: 100%;\n}\n
  1. Create a grid with 2 columns and 4 rows.
"},{"location":"styles/grid/grid_size/#columns-only","title":"Columns only","text":"

In the second example, we create a grid with 2 columns and however many rows are needed to display all of the grid children:

Outputgrid_size_columns.pygrid_size_columns.tcss

MyApp \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u25021\u2502\u25022\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u25023\u2502\u25024\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2502 \u2502\u2502 \u25025\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Label\nclass MyApp(App):\ndef compose(self):\nyield Grid(\nLabel(\"1\"),\nLabel(\"2\"),\nLabel(\"3\"),\nLabel(\"4\"),\nLabel(\"5\"),\n)\napp = MyApp(css_path=\"grid_size_columns.tcss\")\n
Grid {\ngrid-size: 2;  /* (1)! */\n}\nLabel {\nborder: round white;\ncontent-align: center middle;\nwidth: 100%;\nheight: 100%;\n}\n
  1. Create a grid with 2 columns and however many rows.
"},{"location":"styles/grid/grid_size/#css","title":"CSS","text":"
/* Grid with 3 rows and 5 columns */\ngrid-size: 3 5;\n/* Grid with 4 columns and as many rows as needed */\ngrid-size: 4;\n
"},{"location":"styles/grid/grid_size/#python","title":"Python","text":"

To programmatically change the grid size, the number of rows and columns must be specified separately:

widget.styles.grid_size_rows = 3\nwidget.styles.grid_size_columns = 6\n
"},{"location":"styles/grid/row_span/","title":"Row-span","text":"

The row-span style specifies how many rows a widget will span in a grid layout.

Note

This style only affects widgets that are direct children of a widget with layout: grid.

"},{"location":"styles/grid/row_span/#syntax","title":"Syntax","text":"
\nrow-span: <integer>;\n

The row-span style accepts a single non-negative <integer> that quantifies how many rows the given widget spans.

"},{"location":"styles/grid/row_span/#example","title":"Example","text":"

The example below shows a 4 by 4 grid where many placeholders span over several rows.

Notice that grid cells are filled from left to right, top to bottom. After placing the placeholders #p1, #p2, #p3, and #p4, the next available cell is in the second row, fourth column, which is where the top of #p5 is.

Outputrow_span.pyrow_span.tcss

MyApp #p4 #p3 #p2 #p1 #p5 #p6 #p7

from textual.app import App\nfrom textual.containers import Grid\nfrom textual.widgets import Placeholder\nclass MyApp(App):\ndef compose(self):\nyield Grid(\nPlaceholder(id=\"p1\"),\nPlaceholder(id=\"p2\"),\nPlaceholder(id=\"p3\"),\nPlaceholder(id=\"p4\"),\nPlaceholder(id=\"p5\"),\nPlaceholder(id=\"p6\"),\nPlaceholder(id=\"p7\"),\n)\napp = MyApp(css_path=\"row_span.tcss\")\n
#p1 {\nrow-span: 4;\n}\n#p2 {\nrow-span: 3;\n}\n#p3 {\nrow-span: 2;\n}\n#p4 {\nrow-span: 1;  /* Didn't need to be set explicitly. */\n}\n#p5 {\nrow-span: 3;\n}\n#p6 {\nrow-span: 2;\n}\n#p7 {\n/* Default value is 1. */\n}\nGrid {\ngrid-size: 4 4;\ngrid-gutter: 1 2;\n}\nPlaceholder {\nheight: 100%;\n}\n
"},{"location":"styles/grid/row_span/#css","title":"CSS","text":"
row-span: 3\n
"},{"location":"styles/grid/row_span/#python","title":"Python","text":"
widget.styles.row_span = 3\n
"},{"location":"styles/grid/row_span/#see-also","title":"See also","text":"
  • column-span to specify how many columns a widget spans.
"},{"location":"styles/links/","title":"Links","text":"

Textual supports the concept of inline \"links\" embedded in text which trigger an action when pressed. There are a number of styles which influence the appearance of these links within a widget.

Note

These CSS rules only target Textual action links. Internet hyperlinks are not affected by these styles.

Property Description link-background The background color of the link text. link-color The color of the link text. link-hover-background The background color of the link text when the cursor is over it. link-hover-color The color of the link text when the cursor is over it. link-hover-style The style of the link text when the cursor is over it. link-style The style of the link text (e.g. underline)."},{"location":"styles/links/#syntax","title":"Syntax","text":"
\nlink-background: <color> [<percentage>];\n\nlink-color: <color> [<percentage>];\n\nlink-style: <text-style>;\n\nlink-hover-background: <color> [<percentage>];\n\nlink-hover-color: <color> [<percentage>];\n\nlink-hover-style: <text-style>;\n

Visit each style's reference page to learn more about how the values are used.

"},{"location":"styles/links/#example","title":"Example","text":"

In the example below, the first label illustrates default link styling. The second label uses CSS to customize the link color, background, and style.

Outputlinks.pylinks.tcss

LinksApp Here\u00a0is\u00a0a\u00a0link\u00a0which\u00a0you\u00a0can\u00a0click! Here\u00a0is\u00a0a\u00a0link\u00a0which\u00a0you\u00a0can\u00a0click!

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nTEXT = \"\"\"\\\nHere is a [@click='app.bell']link[/] which you can click!\n\"\"\"\nclass LinksApp(App):\ndef compose(self) -> ComposeResult:\nyield Static(TEXT)\nyield Static(TEXT, id=\"custom\")\napp = LinksApp(css_path=\"links.tcss\")\n
#custom {\nlink-color: black 90%;\nlink-background: dodgerblue;\nlink-style: bold italic underline;\n}\n
"},{"location":"styles/links/#additional-notes","title":"Additional Notes","text":"
  • Inline links are not widgets, and thus cannot be focused.
"},{"location":"styles/links/#see-also","title":"See Also","text":"
  • An introduction to links in the Actions guide.
"},{"location":"styles/links/link_background/","title":"Link-background","text":"

The link-background style sets the background color of the link.

Note

link-background only applies to Textual action links as described in the actions guide and not to regular hyperlinks.

"},{"location":"styles/links/link_background/#syntax","title":"Syntax","text":"
\nlink-background: <color> [<percentage>];\n

link-background accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the background color of text enclosed in Textual action links.

"},{"location":"styles/links/link_background/#example","title":"Example","text":"

The example below shows some links with their background color changed. It also shows that link-background does not affect hyperlinks.

Outputlink_background.pylink_background.tcss

LinkBackgroundApp Visit\u00a0the\u00a0Textualize\u00a0website. Click\u00a0here\u00a0for\u00a0the\u00a0bell\u00a0sound. You\u00a0can\u00a0also\u00a0click\u00a0here\u00a0for\u00a0the\u00a0bell\u00a0sound. Exit\u00a0this\u00a0application.

from textual.app import App\nfrom textual.widgets import Label\nclass LinkBackgroundApp(App):\ndef compose(self):\nyield Label(\n\"Visit the [link=https://textualize.io]Textualize[/link] website.\",\nid=\"lbl1\",  # (1)!\n)\nyield Label(\n\"Click [@click=app.bell]here[/] for the bell sound.\",\nid=\"lbl2\",  # (2)!\n)\nyield Label(\n\"You can also click [@click=app.bell]here[/] for the bell sound.\",\nid=\"lbl3\",  # (3)!\n)\nyield Label(\n\"[@click=app.quit]Exit this application.[/]\",\nid=\"lbl4\",  # (4)!\n)\napp = LinkBackgroundApp(css_path=\"link_background.tcss\")\n
  1. This label has an hyperlink so it won't be affected by the link-background rule.
  2. This label has an \"action link\" that can be styled with link-background.
  3. This label has an \"action link\" that can be styled with link-background.
  4. This label has an \"action link\" that can be styled with link-background.
#lbl1, #lbl2 {\nlink-background: red;  /* (1)! */\n}\n#lbl3 {\nlink-background: hsl(60,100%,50%) 50%;\n}\n#lbl4 {\nlink-background: $accent;\n}\n
  1. This will only affect one of the labels because action links are the only links that this rule affects.
"},{"location":"styles/links/link_background/#css","title":"CSS","text":"
link-background: red 70%;\nlink-background: $accent;\n
"},{"location":"styles/links/link_background/#python","title":"Python","text":"
widget.styles.link_background = \"red 70%\"\nwidget.styles.link_background = \"$accent\"\n# You can also use a `Color` object directly:\nwidget.styles.link_background = Color(100, 30, 173)\n
"},{"location":"styles/links/link_background/#see-also","title":"See also","text":"
  • link-color to set the color of link text.
  • `link-hover-background to set the background color of link text when the mouse pointer is over it.
"},{"location":"styles/links/link_color/","title":"Link-color","text":"

The link-color style sets the color of the link text.

Note

link-color only applies to Textual action links as described in the actions guide and not to regular hyperlinks.

"},{"location":"styles/links/link_color/#syntax","title":"Syntax","text":"
\nlink-color: <color> [<percentage>];\n

link-color accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the color of text enclosed in Textual action links.

"},{"location":"styles/links/link_color/#example","title":"Example","text":"

The example below shows some links with their color changed. It also shows that link-color does not affect hyperlinks.

Outputlink_color.pylink_color.tcss

LinkColorApp Visit\u00a0the\u00a0Textualize\u00a0website. Click\u00a0here\u00a0for\u00a0the\u00a0bell\u00a0sound. You\u00a0can\u00a0also\u00a0click\u00a0here\u00a0for\u00a0the\u00a0bell\u00a0sound. Exit\u00a0this\u00a0application.

from textual.app import App\nfrom textual.widgets import Label\nclass LinkColorApp(App):\ndef compose(self):\nyield Label(\n\"Visit the [link=https://textualize.io]Textualize[/link] website.\",\nid=\"lbl1\",  # (1)!\n)\nyield Label(\n\"Click [@click=app.bell]here[/] for the bell sound.\",\nid=\"lbl2\",  # (2)!\n)\nyield Label(\n\"You can also click [@click=app.bell]here[/] for the bell sound.\",\nid=\"lbl3\",  # (3)!\n)\nyield Label(\n\"[@click=app.quit]Exit this application.[/]\",\nid=\"lbl4\",  # (4)!\n)\napp = LinkColorApp(css_path=\"link_color.tcss\")\n
  1. This label has an hyperlink so it won't be affected by the link-color rule.
  2. This label has an \"action link\" that can be styled with link-color.
  3. This label has an \"action link\" that can be styled with link-color.
  4. This label has an \"action link\" that can be styled with link-color.
#lbl1, #lbl2 {\nlink-color: red;  /* (1)! */\n}\n#lbl3 {\nlink-color: hsl(60,100%,50%) 50%;\n}\n#lbl4 {\nlink-color: $accent;\n}\n
  1. This will only affect one of the labels because action links are the only links that this rule affects.
"},{"location":"styles/links/link_color/#css","title":"CSS","text":"
link-color: red 70%;\nlink-color: $accent;\n
"},{"location":"styles/links/link_color/#python","title":"Python","text":"
widget.styles.link_color = \"red 70%\"\nwidget.styles.link_color = \"$accent\"\n# You can also use a `Color` object directly:\nwidget.styles.link_color = Color(100, 30, 173)\n
"},{"location":"styles/links/link_color/#see-also","title":"See also","text":"
  • link-background to set the background color of link text.
  • `link-hover-color to set the color of link text when the mouse pointer is over it.
"},{"location":"styles/links/link_hover_background/","title":"Link-hover-background","text":"

The link-hover-background style sets the background color of the link when the mouse cursor is over the link.

Note

link-hover-background only applies to Textual action links as described in the actions guide and not to regular hyperlinks.

"},{"location":"styles/links/link_hover_background/#syntax","title":"Syntax","text":"
\nlink-hover-background: <color> [<percentage>];\n

link-hover-background accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the background color of text enclosed in Textual action links when the mouse pointer is over it.

"},{"location":"styles/links/link_hover_background/#defaults","title":"Defaults","text":"

If not provided, a Textual action link will have link-hover-background set to $accent.

"},{"location":"styles/links/link_hover_background/#example","title":"Example","text":"

The example below shows some links that have their background colour changed when the mouse moves over it and it shows that there is a default color for link-hover-background.

It also shows that link-hover-background does not affect hyperlinks.

Outputlink_hover_background.pylink_hover_background.tcss

Note

The GIF has reduced quality to make it easier to load in the documentation. Try running the example yourself with textual run docs/examples/styles/link_hover_background.py.

from textual.app import App\nfrom textual.widgets import Label\nclass LinkHoverBackgroundApp(App):\ndef compose(self):\nyield Label(\n\"Visit the [link=https://textualize.io]Textualize[/link] website.\",\nid=\"lbl1\",  # (1)!\n)\nyield Label(\n\"Click [@click=app.bell]here[/] for the bell sound.\",\nid=\"lbl2\",  # (2)!\n)\nyield Label(\n\"You can also click [@click=app.bell]here[/] for the bell sound.\",\nid=\"lbl3\",  # (3)!\n)\nyield Label(\n\"[@click=app.quit]Exit this application.[/]\",\nid=\"lbl4\",  # (4)!\n)\napp = LinkHoverBackgroundApp(css_path=\"link_hover_background.tcss\")\n
  1. This label has an hyperlink so it won't be affected by the link-hover-background rule.
  2. This label has an \"action link\" that can be styled with link-hover-background.
  3. This label has an \"action link\" that can be styled with link-hover-background.
  4. This label has an \"action link\" that can be styled with link-hover-background.
#lbl1, #lbl2 {\nlink-hover-background: red;  /* (1)! */\n}\n#lbl3 {\nlink-hover-background: hsl(60,100%,50%) 50%;\n}\n#lbl4 {\n/* Empty to show the default hover background */ /* (2)! */\n}\n
  1. This will only affect one of the labels because action links are the only links that this rule affects.
  2. The default behavior for links on hover is to change to a different background color, so we don't need to change anything if all we want is to add emphasis to the link under the mouse.
"},{"location":"styles/links/link_hover_background/#css","title":"CSS","text":"
link-hover-background: red 70%;\nlink-hover-background: $accent;\n
"},{"location":"styles/links/link_hover_background/#python","title":"Python","text":"
widget.styles.link_hover_background = \"red 70%\"\nwidget.styles.link_hover_background = \"$accent\"\n# You can also use a `Color` object directly:\nwidget.styles.link_hover_background = Color(100, 30, 173)\n
"},{"location":"styles/links/link_hover_background/#see-also","title":"See also","text":"
  • link-background to set the background color of link text.
  • `link-hover-color to set the color of link text when the mouse pointer is over it.
  • `link-hover-style to set the style of link text when the mouse pointer is over it.
"},{"location":"styles/links/link_hover_color/","title":"Link-hover-color","text":"

The link-hover-color style sets the color of the link text when the mouse cursor is over the link.

Note

link-hover-color only applies to Textual action links as described in the actions guide and not to regular hyperlinks.

"},{"location":"styles/links/link_hover_color/#syntax","title":"Syntax","text":"
\nlink-hover-color: <color> [<percentage>];\n

link-hover-color accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the color of text enclosed in Textual action links when the mouse pointer is over it.

"},{"location":"styles/links/link_hover_color/#defaults","title":"Defaults","text":"

If not provided, a Textual action link will have link-hover-color set to white.

"},{"location":"styles/links/link_hover_color/#example","title":"Example","text":"

The example below shows some links that have their colour changed when the mouse moves over it. It also shows that link-hover-color does not affect hyperlinks.

Outputlink_hover_color.pylink_hover_color.tcss

Note

The background color also changes when the mouse moves over the links because that is the default behavior. That can be customised by setting link-hover-background but we haven't done so in this example.

Note

The GIF has reduced quality to make it easier to load in the documentation. Try running the example yourself with textual run docs/examples/styles/link_hover_color.py.

from textual.app import App\nfrom textual.widgets import Label\nclass LinkHoverColorApp(App):\ndef compose(self):\nyield Label(\n\"Visit the [link=https://textualize.io]Textualize[/link] website.\",\nid=\"lbl1\",  # (1)!\n)\nyield Label(\n\"Click [@click=app.bell]here[/] for the bell sound.\",\nid=\"lbl2\",  # (2)!\n)\nyield Label(\n\"You can also click [@click=app.bell]here[/] for the bell sound.\",\nid=\"lbl3\",  # (3)!\n)\nyield Label(\n\"[@click=app.quit]Exit this application.[/]\",\nid=\"lbl4\",  # (4)!\n)\napp = LinkHoverColorApp(css_path=\"link_hover_color.tcss\")\n
  1. This label has an hyperlink so it won't be affected by the link-hover-color rule.
  2. This label has an \"action link\" that can be styled with link-hover-color.
  3. This label has an \"action link\" that can be styled with link-hover-color.
  4. This label has an \"action link\" that can be styled with link-hover-color.
#lbl1, #lbl2 {\nlink-hover-color: red;  /* (1)! */\n}\n#lbl3 {\nlink-hover-color: hsl(60,100%,50%) 50%;\n}\n#lbl4 {\nlink-hover-color: black;\n}\n
  1. This will only affect one of the labels because action links are the only links that this rule affects.
"},{"location":"styles/links/link_hover_color/#css","title":"CSS","text":"
link-hover-color: red 70%;\nlink-hover-color: black;\n
"},{"location":"styles/links/link_hover_color/#python","title":"Python","text":"
widget.styles.link_hover_color = \"red 70%\"\nwidget.styles.link_hover_color = \"black\"\n# You can also use a `Color` object directly:\nwidget.styles.link_hover_color = Color(100, 30, 173)\n
"},{"location":"styles/links/link_hover_color/#see-also","title":"See also","text":"
  • link-color to set the color of link text.
  • `link-hover-background to set the background color of link text when the mouse pointer is over it.
  • `link-hover-style to set the style of link text when the mouse pointer is over it.
"},{"location":"styles/links/link_hover_style/","title":"Link-hover-style","text":"

The link-hover-style style sets the text style for the link text when the mouse cursor is over the link.

Note

link-hover-style only applies to Textual action links as described in the actions guide and not to regular hyperlinks.

"},{"location":"styles/links/link_hover_style/#syntax","title":"Syntax","text":"
\nlink-hover-style: <text-style>;\n

link-hover-style applies its <text-style> to the text of Textual action links when the mouse pointer is over them.

"},{"location":"styles/links/link_hover_style/#defaults","title":"Defaults","text":"

If not provided, a Textual action link will have link-hover-style set to bold.

"},{"location":"styles/links/link_hover_style/#example","title":"Example","text":"

The example below shows some links that have their colour changed when the mouse moves over it. It also shows that link-hover-style does not affect hyperlinks.

Outputlink_hover_style.pylink_hover_style.tcss

Note

The background color also changes when the mouse moves over the links because that is the default behavior. That can be customised by setting link-hover-background but we haven't done so in this example.

Note

The GIF has reduced quality to make it easier to load in the documentation. Try running the example yourself with textual run docs/examples/styles/link_hover_style.py.

from textual.app import App\nfrom textual.widgets import Label\nclass LinkHoverStyleApp(App):\ndef compose(self):\nyield Label(\n\"Visit the [link=https://textualize.io]Textualize[/link] website.\",\nid=\"lbl1\",  # (1)!\n)\nyield Label(\n\"Click [@click=app.bell]here[/] for the bell sound.\",\nid=\"lbl2\",  # (2)!\n)\nyield Label(\n\"You can also click [@click=app.bell]here[/] for the bell sound.\",\nid=\"lbl3\",  # (3)!\n)\nyield Label(\n\"[@click=app.quit]Exit this application.[/]\",\nid=\"lbl4\",  # (4)!\n)\napp = LinkHoverStyleApp(css_path=\"link_hover_style.tcss\")\n
  1. This label has an hyperlink so it won't be affected by the link-hover-style rule.
  2. This label has an \"action link\" that can be styled with link-hover-style.
  3. This label has an \"action link\" that can be styled with link-hover-style.
  4. This label has an \"action link\" that can be styled with link-hover-style.
#lbl1, #lbl2 {\nlink-hover-style: bold italic;  /* (1)! */\n}\n#lbl3 {\nlink-hover-style: reverse strike;\n}\n#lbl4 {\nlink-hover-style: bold;\n}\n
  1. This will only affect one of the labels because action links are the only links that this rule affects.
  2. The default behavior for links on hover is to change to a different text style, so we don't need to change anything if all we want is to add emphasis to the link under the mouse.
"},{"location":"styles/links/link_hover_style/#css","title":"CSS","text":"
link-hover-style: bold;\nlink-hover-style: bold italic reverse;\n
"},{"location":"styles/links/link_hover_style/#python","title":"Python","text":"
widget.styles.link_hover_style = \"bold\"\nwidget.styles.link_hover_style = \"bold italic reverse\"\n
"},{"location":"styles/links/link_hover_style/#see-also","title":"See also","text":"
  • `link-hover-background to set the background color of link text when the mouse pointer is over it.
  • `link-hover-color to set the color of link text when the mouse pointer is over it.
  • link-style to set the style of link text.
  • text-style to set the style of text in a widget.
"},{"location":"styles/links/link_style/","title":"Link-style","text":"

The link-style style sets the text style for the link text.

Note

link-style only applies to Textual action links as described in the actions guide and not to regular hyperlinks.

"},{"location":"styles/links/link_style/#syntax","title":"Syntax","text":"
\nlink-style: <text-style>;\n

link-style will take all the values specified and will apply that styling to text that is enclosed by a Textual action link.

"},{"location":"styles/links/link_style/#defaults","title":"Defaults","text":"

If not provided, a Textual action link will have link-style set to underline.

"},{"location":"styles/links/link_style/#example","title":"Example","text":"

The example below shows some links with different styles applied to their text. It also shows that link-style does not affect hyperlinks.

Outputlink_style.pylink_style.tcss

LinkStyleApp Visit\u00a0the\u00a0Textualize\u00a0website. Click\u00a0here\u00a0for\u00a0the\u00a0bell\u00a0sound. You\u00a0can\u00a0also\u00a0click\u00a0here\u00a0for\u00a0the\u00a0bell\u00a0sound. Exit\u00a0this\u00a0application.

from textual.app import App\nfrom textual.widgets import Label\nclass LinkStyleApp(App):\ndef compose(self):\nyield Label(\n\"Visit the [link=https://textualize.io]Textualize[/link] website.\",\nid=\"lbl1\",  # (1)!\n)\nyield Label(\n\"Click [@click=app.bell]here[/] for the bell sound.\",\nid=\"lbl2\",  # (2)!\n)\nyield Label(\n\"You can also click [@click=app.bell]here[/] for the bell sound.\",\nid=\"lbl3\",  # (3)!\n)\nyield Label(\n\"[@click=app.quit]Exit this application.[/]\",\nid=\"lbl4\",  # (4)!\n)\napp = LinkStyleApp(css_path=\"link_style.tcss\")\n
  1. This label has an hyperlink so it won't be affected by the link-style rule.
  2. This label has an \"action link\" that can be styled with link-style.
  3. This label has an \"action link\" that can be styled with link-style.
  4. This label has an \"action link\" that can be styled with link-style.
#lbl1, #lbl2 {\nlink-style: bold italic;  /* (1)! */\n}\n#lbl3 {\nlink-style: reverse strike;\n}\n#lbl4 {\nlink-style: bold;\n}\n
  1. This will only affect one of the labels because action links are the only links that this rule affects.
"},{"location":"styles/links/link_style/#css","title":"CSS","text":"
link-style: bold;\nlink-style: bold italic reverse;\n
"},{"location":"styles/links/link_style/#python","title":"Python","text":"
widget.styles.link_style = \"bold\"\nwidget.styles.link_style = \"bold italic reverse\"\n
"},{"location":"styles/links/link_style/#see-also","title":"See also","text":"
  • `link-hover-style to set the style of link text when the mouse pointer is over it.
  • text-style to set the style of text in a widget.
"},{"location":"styles/scrollbar_colors/","title":"Scrollbar colors","text":"

There are a number of styles to set the colors used in Textual scrollbars. You won't typically need to do this, as the default themes have carefully chosen colors, but you can if you want to.

Style Applies to scrollbar-background Scrollbar background. scrollbar-background-active Scrollbar background when the thumb is being dragged. scrollbar-background-hover Scrollbar background when the mouse is hovering over it. scrollbar-color Scrollbar \"thumb\" (movable part). scrollbar-color-active Scrollbar thumb when it is active (being dragged). scrollbar-color-hover Scrollbar thumb when the mouse is hovering over it. scrollbar-corner-color The gap between the horizontal and vertical scrollbars."},{"location":"styles/scrollbar_colors/#syntax","title":"Syntax","text":"
\nscrollbar-background: <color> [<percentage>];\n\nscrollbar-background-active: <color> [<percentage>];\n\nscrollbar-background-hover: <color> [<percentage>];\n\nscrollbar-color: <color> [<percentage>];\n\nscrollbar-color-active: <color> [<percentage>];\n\nscrollbar-color-hover: <color> [<percentage>];\n\nscrollbar-corner-color: <color> [<percentage>];\n

Visit each style's reference page to learn more about how the values are used.

"},{"location":"styles/scrollbar_colors/#example","title":"Example","text":"

This example shows two panels that contain oversized text. The right panel sets scrollbar-background, scrollbar-color, and scrollbar-corner-color, and the left panel shows the default colors for comparison.

Outputscrollbars.pyscrollbars.tcss

ScrollbarApp I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0tFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0t I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0tI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0t And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turnAnd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn see\u00a0its\u00a0path.see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0 will\u00a0remain.will\u00a0remain. I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0tFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0t I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0tI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0t And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turnAnd\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn see\u00a0its\u00a0path.\u2583\u2583see\u00a0its\u00a0path.\u2583\u2583 Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0 will\u00a0remain.will\u00a0remain. I\u00a0must\u00a0not\u00a0fear.I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer.Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0tFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0t I\u00a0will\u00a0face\u00a0my\u00a0fear.I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0tI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0t \u258d\u258d

from textual.app import App\nfrom textual.containers import Horizontal, ScrollableContainer\nfrom textual.widgets import Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\nclass ScrollbarApp(App):\ndef compose(self):\nyield Horizontal(\nScrollableContainer(Label(TEXT * 10)),\nScrollableContainer(Label(TEXT * 10), classes=\"right\"),\n)\napp = ScrollbarApp(css_path=\"scrollbars.tcss\")\nif __name__ == \"__main__\":\napp.run()\n
Label {\nwidth: 150%;\nheight: 150%;\n}\n.right {\nscrollbar-background: red;\nscrollbar-color: green;\nscrollbar-corner-color: blue;\n}\nHorizontal > ScrollableContainer {\nwidth: 50%;\n}\n
"},{"location":"styles/scrollbar_colors/scrollbar_background/","title":"Scrollbar-background","text":"

The scrollbar-background style sets the background color of the scrollbar.

"},{"location":"styles/scrollbar_colors/scrollbar_background/#syntax","title":"Syntax","text":"
\nscrollbar-background: <color> [<percentage>];\n

scrollbar-background accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the background color of a scrollbar.

"},{"location":"styles/scrollbar_colors/scrollbar_background/#example","title":"Example","text":"Outputscrollbars2.pyscrollbars2.tcss

Note

The GIF above has reduced quality to make it easier to load in the documentation. Try running the example yourself with textual run docs/examples/styles/scrollbars2.py.

from textual.app import App\nfrom textual.widgets import Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\nclass Scrollbar2App(App):\ndef compose(self):\nyield Label(TEXT * 10)\napp = Scrollbar2App(css_path=\"scrollbars2.tcss\")\n
Screen {\nscrollbar-background: blue;\nscrollbar-background-active: red;\nscrollbar-background-hover: purple;\nscrollbar-color: cyan;\nscrollbar-color-active: yellow;\nscrollbar-color-hover: pink;\n}\n
"},{"location":"styles/scrollbar_colors/scrollbar_background/#css","title":"CSS","text":"
scrollbar-backround: blue;\n
"},{"location":"styles/scrollbar_colors/scrollbar_background/#python","title":"Python","text":"
widget.styles.scrollbar_background = \"blue\"\n
"},{"location":"styles/scrollbar_colors/scrollbar_background/#see-also","title":"See also","text":"
  • scrollbar-bakcground-active to set the scrollbar bakcground color when the scrollbar is being dragged.
  • scrollbar-bakcground-hover to set the scrollbar bakcground color when the mouse pointer is over it.
  • scrollbar-color to set the color of scrollbars.
  • scrollbar-corner-color to set the color of the corner where horizontal and vertical scrollbars meet.
"},{"location":"styles/scrollbar_colors/scrollbar_background_active/","title":"Scrollbar-background-active","text":"

The scrollbar-background-active style sets the background color of the scrollbar when the thumb is being dragged.

"},{"location":"styles/scrollbar_colors/scrollbar_background_active/#syntax","title":"Syntax","text":"
\nscrollbar-background-active: <color> [<percentage>];\n

scrollbar-background-active accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the background color of a scrollbar when its thumb is being dragged.

"},{"location":"styles/scrollbar_colors/scrollbar_background_active/#example","title":"Example","text":"Outputscrollbars2.pyscrollbars2.tcss

Note

The GIF above has reduced quality to make it easier to load in the documentation. Try running the example yourself with textual run docs/examples/styles/scrollbars2.py.

from textual.app import App\nfrom textual.widgets import Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\nclass Scrollbar2App(App):\ndef compose(self):\nyield Label(TEXT * 10)\napp = Scrollbar2App(css_path=\"scrollbars2.tcss\")\n
Screen {\nscrollbar-background: blue;\nscrollbar-background-active: red;\nscrollbar-background-hover: purple;\nscrollbar-color: cyan;\nscrollbar-color-active: yellow;\nscrollbar-color-hover: pink;\n}\n
"},{"location":"styles/scrollbar_colors/scrollbar_background_active/#css","title":"CSS","text":"
scrollbar-backround-active: red;\n
"},{"location":"styles/scrollbar_colors/scrollbar_background_active/#python","title":"Python","text":"
widget.styles.scrollbar_background_active = \"red\"\n
"},{"location":"styles/scrollbar_colors/scrollbar_background_active/#see-also","title":"See also","text":"
  • scrollbar-background to set the background color of scrollbars.
  • scrollbar-bakcground-hover to set the scrollbar bakcground color when the mouse pointer is over it.
  • scrollbar-color-active to set the scrollbar color when the scrollbar is being dragged.
"},{"location":"styles/scrollbar_colors/scrollbar_background_hover/","title":"Scrollbar-background-hover","text":"

The scrollbar-background-hover style sets the background color of the scrollbar when the cursor is over it.

"},{"location":"styles/scrollbar_colors/scrollbar_background_hover/#syntax","title":"Syntax","text":"
\nscrollbar-background-hover: <color> [<percentage>];\n

scrollbar-background-hover accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the background color of a scrollbar when the cursor is over it.

"},{"location":"styles/scrollbar_colors/scrollbar_background_hover/#example","title":"Example","text":"Outputscrollbars2.pyscrollbars2.tcss

Note

The GIF above has reduced quality to make it easier to load in the documentation. Try running the example yourself with textual run docs/examples/styles/scrollbars2.py.

from textual.app import App\nfrom textual.widgets import Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\nclass Scrollbar2App(App):\ndef compose(self):\nyield Label(TEXT * 10)\napp = Scrollbar2App(css_path=\"scrollbars2.tcss\")\n
Screen {\nscrollbar-background: blue;\nscrollbar-background-active: red;\nscrollbar-background-hover: purple;\nscrollbar-color: cyan;\nscrollbar-color-active: yellow;\nscrollbar-color-hover: pink;\n}\n
"},{"location":"styles/scrollbar_colors/scrollbar_background_hover/#css","title":"CSS","text":"
scrollbar-background-hover: purple;\n
"},{"location":"styles/scrollbar_colors/scrollbar_background_hover/#python","title":"Python","text":"
widget.styles.scrollbar_background_hover = \"purple\"\n
"},{"location":"styles/scrollbar_colors/scrollbar_background_hover/#see-also","title":"See also","text":""},{"location":"styles/scrollbar_colors/scrollbar_background_hover/#see-also_1","title":"See also","text":"
  • scrollbar-background to set the background color of scrollbars.
  • scrollbar-bakcground-active to set the scrollbar bakcground color when the scrollbar is being dragged.
  • scrollbar-color-hover to set the scrollbar color when the mouse pointer is over it.
"},{"location":"styles/scrollbar_colors/scrollbar_color/","title":"Scrollbar-color","text":"

The scrollbar-color style sets the color of the scrollbar.

"},{"location":"styles/scrollbar_colors/scrollbar_color/#syntax","title":"Syntax","text":"
\nscrollbar-color: <color> [<percentage>];\n

scrollbar-color accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the color of a scrollbar.

"},{"location":"styles/scrollbar_colors/scrollbar_color/#example","title":"Example","text":"Outputscrollbars2.pyscrollbars2.tcss

Note

The GIF above has reduced quality to make it easier to load in the documentation. Try running the example yourself with textual run docs/examples/styles/scrollbars2.py.

from textual.app import App\nfrom textual.widgets import Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\nclass Scrollbar2App(App):\ndef compose(self):\nyield Label(TEXT * 10)\napp = Scrollbar2App(css_path=\"scrollbars2.tcss\")\n
Screen {\nscrollbar-background: blue;\nscrollbar-background-active: red;\nscrollbar-background-hover: purple;\nscrollbar-color: cyan;\nscrollbar-color-active: yellow;\nscrollbar-color-hover: pink;\n}\n
"},{"location":"styles/scrollbar_colors/scrollbar_color/#css","title":"CSS","text":"
scrollbar-color: cyan;\n
"},{"location":"styles/scrollbar_colors/scrollbar_color/#python","title":"Python","text":"
widget.styles.scrollbar_color = \"cyan\"\n
"},{"location":"styles/scrollbar_colors/scrollbar_color/#see-also","title":"See also","text":"
  • scrollbar-background to set the background color of scrollbars.
  • scrollbar-color-active to set the scrollbar color when the scrollbar is being dragged.
  • scrollbar-color-hover to set the scrollbar color when the mouse pointer is over it.
  • scrollbar-corner-color to set the color of the corner where horizontal and vertical scrollbars meet.
"},{"location":"styles/scrollbar_colors/scrollbar_color_active/","title":"Scrollbar-color-active","text":"

The scrollbar-color-active style sets the color of the scrollbar when the thumb is being dragged.

"},{"location":"styles/scrollbar_colors/scrollbar_color_active/#syntax","title":"Syntax","text":"
\nscrollbar-color-active: <color> [<percentage>];\n

scrollbar-color-active accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the color of a scrollbar when its thumb is being dragged.

"},{"location":"styles/scrollbar_colors/scrollbar_color_active/#example","title":"Example","text":"Outputscrollbars2.pyscrollbars2.tcss

Note

The GIF above has reduced quality to make it easier to load in the documentation. Try running the example yourself with textual run docs/examples/styles/scrollbars2.py.

from textual.app import App\nfrom textual.widgets import Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\nclass Scrollbar2App(App):\ndef compose(self):\nyield Label(TEXT * 10)\napp = Scrollbar2App(css_path=\"scrollbars2.tcss\")\n
Screen {\nscrollbar-background: blue;\nscrollbar-background-active: red;\nscrollbar-background-hover: purple;\nscrollbar-color: cyan;\nscrollbar-color-active: yellow;\nscrollbar-color-hover: pink;\n}\n
"},{"location":"styles/scrollbar_colors/scrollbar_color_active/#css","title":"CSS","text":"
scrollbar-color-active: yellow;\n
"},{"location":"styles/scrollbar_colors/scrollbar_color_active/#python","title":"Python","text":"
widget.styles.scrollbar_color_active = \"yellow\"\n
"},{"location":"styles/scrollbar_colors/scrollbar_color_active/#see-also","title":"See also","text":"
  • scrollbar-bakcground-active to set the scrollbar bakcground color when the scrollbar is being dragged.
  • scrollbar-color to set the color of scrollbars.
  • scrollbar-color-hover to set the scrollbar color when the mouse pointer is over it.
"},{"location":"styles/scrollbar_colors/scrollbar_color_hover/","title":"Scrollbar-color-hover","text":"

The scrollbar-color-hover style sets the color of the scrollbar when the cursor is over it.

"},{"location":"styles/scrollbar_colors/scrollbar_color_hover/#syntax","title":"Syntax","text":"
\nscrollbar-color-hover: <color> [<percentage>];\n

scrollbar-color-hover accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the color of a scrollbar when the cursor is over it.

"},{"location":"styles/scrollbar_colors/scrollbar_color_hover/#example","title":"Example","text":"Outputscrollbars2.pyscrollbars2.tcss

Note

The GIF above has reduced quality to make it easier to load in the documentation. Try running the example yourself with textual run docs/examples/styles/scrollbars2.py.

from textual.app import App\nfrom textual.widgets import Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\nclass Scrollbar2App(App):\ndef compose(self):\nyield Label(TEXT * 10)\napp = Scrollbar2App(css_path=\"scrollbars2.tcss\")\n
Screen {\nscrollbar-background: blue;\nscrollbar-background-active: red;\nscrollbar-background-hover: purple;\nscrollbar-color: cyan;\nscrollbar-color-active: yellow;\nscrollbar-color-hover: pink;\n}\n
"},{"location":"styles/scrollbar_colors/scrollbar_color_hover/#css","title":"CSS","text":"
scrollbar-color-hover: pink;\n
"},{"location":"styles/scrollbar_colors/scrollbar_color_hover/#python","title":"Python","text":"
widget.styles.scrollbar_color_hover = \"pink\"\n
"},{"location":"styles/scrollbar_colors/scrollbar_color_hover/#see-also","title":"See also","text":"
  • scrollbar-bakcground-hover to set the scrollbar bakcground color when the mouse pointer is over it.
  • scrollbar-color to set the color of scrollbars.
  • scrollbar-color-active to set the scrollbar color when the scrollbar is being dragged.
"},{"location":"styles/scrollbar_colors/scrollbar_corner_color/","title":"Scrollbar-corner-color","text":"

The scrollbar-corner-color style sets the color of the gap between the horizontal and vertical scrollbars.

"},{"location":"styles/scrollbar_colors/scrollbar_corner_color/#syntax","title":"Syntax","text":"
\nscrollbar-corner-color: <color> [<percentage>];\n

scrollbar-corner-color accepts a <color> (with an optional opacity level defined by a <percentage>) that is used to define the color of the gap between the horizontal and vertical scrollbars of a widget.

"},{"location":"styles/scrollbar_colors/scrollbar_corner_color/#example","title":"Example","text":"

The example below sets the scrollbar corner (bottom-right corner of the screen) to white.

Outputscrollbar_corner_color.pyscrollbar_corner_color.tcss

ScrollbarCornerColorApp I\u00a0must\u00a0not\u00a0fear.\u00a0Fear\u00a0is\u00a0the\u00a0mind-killer.\u00a0Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration.

from textual.app import App\nfrom textual.widgets import Label\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\n\"\"\"\nclass ScrollbarCornerColorApp(App):\ndef compose(self):\nyield Label(TEXT.replace(\"\\n\", \" \") + \"\\n\" + TEXT * 10)\napp = ScrollbarCornerColorApp(css_path=\"scrollbar_corner_color.tcss\")\n
Screen {\noverflow: auto auto;\nscrollbar-corner-color: white;\n}\n
"},{"location":"styles/scrollbar_colors/scrollbar_corner_color/#css","title":"CSS","text":"
scrollbar-corner-color: white;\n
"},{"location":"styles/scrollbar_colors/scrollbar_corner_color/#python","title":"Python","text":"
widget.styles.scrollbar_corner_color = \"white\"\n
"},{"location":"styles/scrollbar_colors/scrollbar_corner_color/#see-also","title":"See also","text":"
  • scrollbar-background to set the background color of scrollbars.
  • scrollbar-color to set the color of scrollbars.
"},{"location":"widgets/","title":"Widgets","text":"

A reference to the builtin widgets.

See the links to the left of the page, or in the hamburger menu (three horizontal bars, top left).

"},{"location":"widgets/button/","title":"Button","text":"

A simple button widget which can be pressed using a mouse click or by pressing Enter when it has focus.

  • Focusable
  • Container
"},{"location":"widgets/button/#example","title":"Example","text":"

The example below shows each button variant, and its disabled equivalent. Clicking any of the non-disabled buttons in the example app below will result in the app exiting and the details of the selected button being printed to the console.

Outputbutton.pybutton.tcss

ButtonsApp Standard\u00a0ButtonsDisabled\u00a0Buttons \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Default\u00a0\u00a0Default\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Primary!\u00a0\u00a0Primary!\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Success!\u00a0\u00a0Success!\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Warning!\u00a0\u00a0Warning!\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0Error!\u00a0\u00a0Error!\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal, VerticalScroll\nfrom textual.widgets import Button, Static\nclass ButtonsApp(App[str]):\nCSS_PATH = \"button.tcss\"\ndef compose(self) -> ComposeResult:\nyield Horizontal(\nVerticalScroll(\nStatic(\"Standard Buttons\", classes=\"header\"),\nButton(\"Default\"),\nButton(\"Primary!\", variant=\"primary\"),\nButton.success(\"Success!\"),\nButton.warning(\"Warning!\"),\nButton.error(\"Error!\"),\n),\nVerticalScroll(\nStatic(\"Disabled Buttons\", classes=\"header\"),\nButton(\"Default\", disabled=True),\nButton(\"Primary!\", variant=\"primary\", disabled=True),\nButton.success(\"Success!\", disabled=True),\nButton.warning(\"Warning!\", disabled=True),\nButton.error(\"Error!\", disabled=True),\n),\n)\ndef on_button_pressed(self, event: Button.Pressed) -> None:\nself.exit(str(event.button))\nif __name__ == \"__main__\":\napp = ButtonsApp()\nprint(app.run())\n
Button {\nmargin: 1 2;\n}\nHorizontal > VerticalScroll {\nwidth: 24;\n}\n.header {\nmargin: 1 0 0 2;\ntext-style: bold;\n}\n
"},{"location":"widgets/button/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description label str \"\" The text that appears inside the button. variant ButtonVariant \"default\" Semantic styling variant. One of default, primary, success, warning, error. disabled bool False Whether the button is disabled or not. Disabled buttons cannot be focused or clicked, and are styled in a way that suggests this."},{"location":"widgets/button/#messages","title":"Messages","text":"
  • Button.Pressed
"},{"location":"widgets/button/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/button/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/button/#additional-notes","title":"Additional Notes","text":"
  • The spacing between the text and the edges of a button are not due to padding. The default styling for a Button has the height set to 3 lines and a min-width of 16 columns. To create a button with zero visible padding, you will need to change these values and also remove the border with border: none;.
"},{"location":"widgets/button/#textual.widgets.Button","title":"textual.widgets.Button class","text":"
def __init__(\nself,\nlabel=None,\nvariant=\"default\",\n*,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: Static

A simple clickable button.

Parameters Name Type Description Default label TextType | None

The text that appears within the button.

None variant ButtonVariant

The variant of the button.

'default' name str | None

The name of the button.

None id str | None

The ID of the button in the DOM.

None classes str | None

The CSS classes of the button.

None disabled bool

Whether the button is disabled or not.

False"},{"location":"widgets/button/#textual.widgets._button.Button.ACTIVE_EFFECT_DURATION","title":"ACTIVE_EFFECT_DURATION instance-attribute class-attribute","text":"
ACTIVE_EFFECT_DURATION = 0.3\n

When buttons are clicked they get the -active class for this duration (in seconds)

"},{"location":"widgets/button/#textual.widgets._button.Button.label","title":"label instance-attribute class-attribute","text":"
label: reactive[TextType] = self.validate_label(label)\n

The text label that appears within the button.

"},{"location":"widgets/button/#textual.widgets._button.Button.variant","title":"variant instance-attribute class-attribute","text":"
variant = self.validate_variant(variant)\n

The variant name for the button.

"},{"location":"widgets/button/#textual.widgets._button.Button.Pressed","title":"Pressed class","text":"
def __init__(self, button):\n

Bases: Message

Event sent when a Button is pressed.

Can be handled using on_button_pressed in a subclass of Button or in a parent widget in the DOM.

"},{"location":"widgets/button/#textual.widgets._button.Button.Pressed.button","title":"button instance-attribute","text":"
button: Button = button\n

The button that was pressed.

"},{"location":"widgets/button/#textual.widgets._button.Button.Pressed.control","title":"control property","text":"
control: Button\n

An alias for Pressed.button.

This will be the same value as Pressed.button.

"},{"location":"widgets/button/#textual.widgets._button.Button.action_press","title":"action_press method","text":"
def action_press(self):\n

Activate a press of the button.

"},{"location":"widgets/button/#textual.widgets._button.Button.error","title":"error classmethod","text":"
def error(\ncls,\nlabel=None,\n*,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Utility constructor for creating an error Button variant.

Parameters Name Type Description Default label TextType | None

The text that appears within the button.

None disabled bool

Whether the button is disabled or not.

False name str | None

The name of the button.

None id str | None

The ID of the button in the DOM.

None classes str | None

The CSS classes of the button.

None disabled bool

Whether the button is disabled or not.

False Returns Type Description Button

A Button widget of the 'error' variant.

"},{"location":"widgets/button/#textual.widgets._button.Button.press","title":"press method","text":"
def press(self):\n

Respond to a button press.

Returns Type Description Self

The button instance.

"},{"location":"widgets/button/#textual.widgets._button.Button.success","title":"success classmethod","text":"
def success(\ncls,\nlabel=None,\n*,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Utility constructor for creating a success Button variant.

Parameters Name Type Description Default label TextType | None

The text that appears within the button.

None disabled bool

Whether the button is disabled or not.

False name str | None

The name of the button.

None id str | None

The ID of the button in the DOM.

None classes str | None

The CSS classes of the button.

None disabled bool

Whether the button is disabled or not.

False Returns Type Description Button

A Button widget of the 'success' variant.

"},{"location":"widgets/button/#textual.widgets._button.Button.validate_label","title":"validate_label method","text":"
def validate_label(self, label):\n

Parse markup for self.label

"},{"location":"widgets/button/#textual.widgets._button.Button.warning","title":"warning classmethod","text":"
def warning(\ncls,\nlabel=None,\n*,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Utility constructor for creating a warning Button variant.

Parameters Name Type Description Default label TextType | None

The text that appears within the button.

None disabled bool

Whether the button is disabled or not.

False name str | None

The name of the button.

None id str | None

The ID of the button in the DOM.

None classes str | None

The CSS classes of the button.

None disabled bool

Whether the button is disabled or not.

False Returns Type Description Button

A Button widget of the 'warning' variant.

"},{"location":"widgets/button/#textual.widgets.button","title":"textual.widgets.button","text":""},{"location":"widgets/button/#textual.widgets.button.ButtonVariant","title":"ButtonVariant module-attribute","text":"
ButtonVariant = Literal[\n\"default\", \"primary\", \"success\", \"warning\", \"error\"\n]\n

The names of the valid button variants.

These are the variants that can be used with a Button.

"},{"location":"widgets/checkbox/","title":"Checkbox","text":"

Added in version 0.13.0

A simple checkbox widget which stores a boolean value.

  • Focusable
  • Container
"},{"location":"widgets/checkbox/#example","title":"Example","text":"

The example below shows check boxes in various states.

Outputcheckbox.pycheckbox.tcss

CheckboxApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258c\u00a0Arrakis\u00a0\ud83d\ude13\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258c\u00a0Caladan\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258c\u00a0Chusuk\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258cGiedi\u00a0Prime\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258cGinaz\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590X\u258c\u00a0Grumman\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2583\u2583 \u258a\u2590X\u258cKaitain\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e

from textual.app import App, ComposeResult\nfrom textual.containers import VerticalScroll\nfrom textual.widgets import Checkbox\nclass CheckboxApp(App[None]):\nCSS_PATH = \"checkbox.tcss\"\ndef compose(self) -> ComposeResult:\nwith VerticalScroll():\nyield Checkbox(\"Arrakis :sweat:\")\nyield Checkbox(\"Caladan\")\nyield Checkbox(\"Chusuk\")\nyield Checkbox(\"[b]Giedi Prime[/b]\")\nyield Checkbox(\"[magenta]Ginaz[/]\")\nyield Checkbox(\"Grumman\", True)\nyield Checkbox(\"Kaitain\", id=\"initial_focus\")\nyield Checkbox(\"Novebruns\", True)\ndef on_mount(self):\nself.query_one(\"#initial_focus\", Checkbox).focus()\nif __name__ == \"__main__\":\nCheckboxApp().run()\n
Screen {\nalign: center middle;\n}\nVerticalScroll {\nwidth: auto;\nheight: auto;\nbackground: $boost;\npadding: 2;\n}\n
"},{"location":"widgets/checkbox/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description value bool False The value of the checkbox."},{"location":"widgets/checkbox/#messages","title":"Messages","text":"
  • Checkbox.Changed
"},{"location":"widgets/checkbox/#bindings","title":"Bindings","text":"

The checkbox widget defines the following bindings:

Key(s) Description enter, space Toggle the value."},{"location":"widgets/checkbox/#component-classes","title":"Component Classes","text":"

The checkbox widget inherits the following component classes:

Class Description toggle--button Targets the toggle button itself. toggle--label Targets the text label of the toggle button."},{"location":"widgets/checkbox/#textual.widgets.Checkbox","title":"textual.widgets.Checkbox class","text":"

Bases: ToggleButton

A check box widget that represents a boolean value.

"},{"location":"widgets/checkbox/#textual.widgets._checkbox.Checkbox.Changed","title":"Changed class","text":"

Bases: ToggleButton.Changed

Posted when the value of the checkbox changes.

This message can be handled using an on_checkbox_changed method.

"},{"location":"widgets/checkbox/#textual.widgets._checkbox.Checkbox.Changed.checkbox","title":"checkbox property","text":"
checkbox: Checkbox\n

The checkbox that was changed.

"},{"location":"widgets/checkbox/#textual.widgets._checkbox.Checkbox.Changed.control","title":"control property","text":"
control: Checkbox\n

An alias for Changed.checkbox.

"},{"location":"widgets/collapsible/","title":"Collapsible","text":"

Added in version 0.37

A container with a title that can be used to show (expand) or hide (collapse) content, either by clicking or focusing and pressing Enter.

  • Focusable
  • Container
"},{"location":"widgets/collapsible/#composing","title":"Composing","text":"

You can add content to a Collapsible widget either by passing in children to the constructor, or with a context manager (with statement).

Here is an example of using the constructor to add content:

def compose(self) -> ComposeResult:\nyield Collapsible(Label(\"Hello, world.\"))\n

Here's how the to use it with the context manager:

def compose(self) -> ComposeResult:\nwith Collapsible():\nyield Label(\"Hello, world.\")\n

The second form is generally preferred, but the end result is the same.

"},{"location":"widgets/collapsible/#title","title":"Title","text":"

The default title \"Toggle\" can be customized by setting the title parameter of the constructor:

def compose(self) -> ComposeResult:\nwith Collapsible(title=\"An interesting story.\"):\nyield Label(\"Interesting but verbose story.\")\n
"},{"location":"widgets/collapsible/#initial-state","title":"Initial State","text":"

The initial state of the Collapsible widget can be customized via the collapsed parameter of the constructor:

def compose(self) -> ComposeResult:\nwith Collapsible(title=\"Contents 1\", collapsed=False):\nyield Label(\"Hello, world.\")\nwith Collapsible(title=\"Contents 2\", collapsed=True):  # Default.\nyield Label(\"Hello, world.\")\n
"},{"location":"widgets/collapsible/#collapseexpand-symbols","title":"Collapse/Expand Symbols","text":"

The symbols used to show the collapsed / expanded state can be customized by setting the parameters collapsed_symbol and expanded_symbol:

def compose(self) -> ComposeResult:\nwith Collapsible(collapsed_symbol=\">>>\", expanded_symbol=\"v\"):\nyield Label(\"Hello, world.\")\n
"},{"location":"widgets/collapsible/#examples","title":"Examples","text":"

The following example contains three Collapsibles in different states.

All expandedAll collapsedMixedcollapsible.py

CollapsibleApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25bc\u00a0Leto #\u00a0Duke\u00a0Leto\u00a0I\u00a0Atreides Head\u00a0of\u00a0House\u00a0Atreides. \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25bc\u00a0Jessica \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eLady\u00a0Jessica\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Bene\u00a0Gesserit\u00a0and\u00a0concubine\u00a0of\u00a0Leto,\u00a0and\u00a0mother\u00a0of\u00a0Paul\u00a0and\u00a0Alia. \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25bc\u00a0Paul \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 C\u00a0\u00a0Collapse\u00a0All\u00a0\u00a0E\u00a0\u00a0Expand\u00a0All\u00a0

CollapsibleApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25b6\u00a0Leto \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25b6\u00a0Jessica \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25b6\u00a0Paul \u00a0C\u00a0\u00a0Collapse\u00a0All\u00a0\u00a0E\u00a0\u00a0Expand\u00a0All\u00a0

CollapsibleApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25bc\u00a0Leto #\u00a0Duke\u00a0Leto\u00a0I\u00a0Atreides Head\u00a0of\u00a0House\u00a0Atreides. \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25bc\u00a0Jessica \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eLady\u00a0Jessica\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Bene\u00a0Gesserit\u00a0and\u00a0concubine\u00a0of\u00a0Leto,\u00a0and\u00a0mother\u00a0of\u00a0Paul\u00a0and\u00a0Alia. \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25b6\u00a0Paul \u00a0C\u00a0\u00a0Collapse\u00a0All\u00a0\u00a0E\u00a0\u00a0Expand\u00a0All\u00a0

from textual.app import App, ComposeResult\nfrom textual.widgets import Collapsible, Footer, Label, Markdown\nLETO = \"\"\"\\\n# Duke Leto I Atreides\nHead of House Atreides.\"\"\"\nJESSICA = \"\"\"\n# Lady Jessica\nBene Gesserit and concubine of Leto, and mother of Paul and Alia.\n\"\"\"\nPAUL = \"\"\"\n# Paul Atreides\nSon of Leto and Jessica.\n\"\"\"\nclass CollapsibleApp(App[None]):\n\"\"\"An example of collapsible container.\"\"\"\nBINDINGS = [\n(\"c\", \"collapse_or_expand(True)\", \"Collapse All\"),\n(\"e\", \"collapse_or_expand(False)\", \"Expand All\"),\n]\ndef compose(self) -> ComposeResult:\n\"\"\"Compose app with collapsible containers.\"\"\"\nyield Footer()\nwith Collapsible(collapsed=False, title=\"Leto\"):\nyield Label(LETO)\nyield Collapsible(Markdown(JESSICA), collapsed=False, title=\"Jessica\")\nwith Collapsible(collapsed=True, title=\"Paul\"):\nyield Markdown(PAUL)\ndef action_collapse_or_expand(self, collapse: bool) -> None:\nfor child in self.walk_children(Collapsible):\nchild.collapsed = collapse\nif __name__ == \"__main__\":\napp = CollapsibleApp()\napp.run()\n
"},{"location":"widgets/collapsible/#setting-initial-state","title":"Setting Initial State","text":"

The example below shows nested Collapsible widgets and how to set their initial state.

Outputcollapsible_nested.py

CollapsibleApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25bc\u00a0Toggle \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u25b6\u00a0Toggle

from textual.app import App, ComposeResult\nfrom textual.widgets import Collapsible, Label\nclass CollapsibleApp(App[None]):\ndef compose(self) -> ComposeResult:\nwith Collapsible(collapsed=False):\nwith Collapsible():\nyield Label(\"Hello, world.\")\nif __name__ == \"__main__\":\napp = CollapsibleApp()\napp.run()\n
"},{"location":"widgets/collapsible/#custom-symbols","title":"Custom Symbols","text":"

The following example shows Collapsible widgets with custom expand/collapse symbols.

Outputcollapsible_custom_symbol.py

CollapsibleApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 >>>\u00a0Togglev\u00a0Toggle Hello,\u00a0world.

from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.widgets import Collapsible, Label\nclass CollapsibleApp(App[None]):\ndef compose(self) -> ComposeResult:\nwith Horizontal():\nwith Collapsible(\ncollapsed_symbol=\">>>\",\nexpanded_symbol=\"v\",\n):\nyield Label(\"Hello, world.\")\nwith Collapsible(\ncollapsed_symbol=\">>>\",\nexpanded_symbol=\"v\",\ncollapsed=False,\n):\nyield Label(\"Hello, world.\")\nif __name__ == \"__main__\":\napp = CollapsibleApp()\napp.run()\n
"},{"location":"widgets/collapsible/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description collapsed bool True Controls the collapsed/expanded state of the widget."},{"location":"widgets/collapsible/#messages","title":"Messages","text":"

This widget posts no messages.

"},{"location":"widgets/collapsible/#bindings","title":"Bindings","text":"

The collapsible widget defines the following binding on its title:

Key(s) Description enter Toggle the collapsible."},{"location":"widgets/collapsible/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/collapsible/#textual.widgets.Collapsible","title":"textual.widgets.Collapsible class","text":"
def __init__(\nself,\n*children,\ntitle=\"Toggle\",\ncollapsed=True,\ncollapsed_symbol=\"\u25b6\",\nexpanded_symbol=\"\u25bc\",\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: Widget

A collapsible container.

Parameters Name Type Description Default *children Widget

Contents that will be collapsed/expanded.

() title str

Title of the collapsed/expanded contents.

'Toggle' collapsed bool

Default status of the contents.

True collapsed_symbol str

Collapsed symbol before the title.

'\u25b6' expanded_symbol str

Expanded symbol before the title.

'\u25bc' name str | None

The name of the collapsible.

None id str | None

The ID of the collapsible in the DOM.

None classes str | None

The CSS classes of the collapsible.

None disabled bool

Whether the collapsible is disabled or not.

False"},{"location":"widgets/content_switcher/","title":"ContentSwitcher","text":"

Added in version 0.14.0

A widget for containing and switching display between multiple child widgets.

  • Focusable
  • Container
"},{"location":"widgets/content_switcher/#example","title":"Example","text":"

The example below uses a ContentSwitcher in combination with two Buttons to create a simple tabbed view. Note how each Button has an ID set, and how each child of the ContentSwitcher has a corresponding ID; then a Button.Clicked handler is used to set ContentSwitcher.current to switch between the different views.

Outputcontent_switcher.pycontent_switcher.tcss

ContentSwitcherApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0DataTable\u00a0\u00a0Markdown\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u00a0Book\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Year\u00a0\u2502 \u2502\u00a0Dune\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01965\u00a0\u2502 \u2502\u00a0Dune\u00a0Messiah\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01969\u00a0\u2502 \u2502\u00a0Children\u00a0of\u00a0Dune\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01976\u00a0\u2502 \u2502\u00a0God\u00a0Emperor\u00a0of\u00a0Dune\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01981\u00a0\u2502 \u2502\u00a0Heretics\u00a0of\u00a0Dune\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01984\u00a0\u2502 \u2502\u00a0Chapterhouse:\u00a0Dune\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01985\u00a0\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

from rich.align import VerticalCenter\nfrom textual.app import App, ComposeResult\nfrom textual.containers import Horizontal, VerticalScroll\nfrom textual.widgets import Button, ContentSwitcher, DataTable, Markdown\nMARKDOWN_EXAMPLE = \"\"\"# Three Flavours Cornetto\nThe Three Flavours Cornetto trilogy is an anthology series of British\ncomedic genre films directed by Edgar Wright.\n## Shaun of the Dead\n| Flavour | UK Release Date | Director |\n| -- | -- | -- |\n| Strawberry | 2004-04-09 | Edgar Wright |\n## Hot Fuzz\n| Flavour | UK Release Date | Director |\n| -- | -- | -- |\n| Classico | 2007-02-17 | Edgar Wright |\n## The World's End\n| Flavour | UK Release Date | Director |\n| -- | -- | -- |\n| Mint | 2013-07-19 | Edgar Wright |\n\"\"\"\nclass ContentSwitcherApp(App[None]):\nCSS_PATH = \"content_switcher.tcss\"\ndef compose(self) -> ComposeResult:\nwith Horizontal(id=\"buttons\"):  # (1)!\nyield Button(\"DataTable\", id=\"data-table\")  # (2)!\nyield Button(\"Markdown\", id=\"markdown\")  # (3)!\nwith ContentSwitcher(initial=\"data-table\"):  # (4)!\nyield DataTable(id=\"data-table\")\nwith VerticalScroll(id=\"markdown\"):\nyield Markdown(MARKDOWN_EXAMPLE)\ndef on_button_pressed(self, event: Button.Pressed) -> None:\nself.query_one(ContentSwitcher).current = event.button.id  # (5)!\ndef on_mount(self) -> None:\ntable = self.query_one(DataTable)\ntable.add_columns(\"Book\", \"Year\")\ntable.add_rows(\n[\n(title.ljust(35), year)\nfor title, year in (\n(\"Dune\", 1965),\n(\"Dune Messiah\", 1969),\n(\"Children of Dune\", 1976),\n(\"God Emperor of Dune\", 1981),\n(\"Heretics of Dune\", 1984),\n(\"Chapterhouse: Dune\", 1985),\n)\n]\n)\nif __name__ == \"__main__\":\nContentSwitcherApp().run()\n
  1. A Horizontal to hold the buttons, each with a unique ID.
  2. This button will select the DataTable in the ContentSwitcher.
  3. This button will select the Markdown in the ContentSwitcher.
  4. Note that the initial visible content is set by its ID, see below.
  5. When a button is pressed, its ID is used to switch to a different widget in the ContentSwitcher. Remember that IDs are unique within parent, so the buttons and the widgets in the ContentSwitcher can share IDs.
Screen {\nalign: center middle;\npadding: 1;\n}\n#buttons {\nheight: 3;\nwidth: auto;\n}\nContentSwitcher {\nbackground: $panel;\nborder: round $primary;\nwidth: 90%;\nheight: 1fr;\n}\nDataTable {\nbackground: $panel;\n}\nMarkdownH2 {\nbackground: $primary;\ncolor: yellow;\nborder: none;\npadding: 0;\n}\n

When the user presses the \"Markdown\" button the view is switched:

ContentSwitcherApp \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u00a0DataTable\u00a0\u00a0Markdown\u00a0 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2502 \u2502\u258e\u258a\u2502 \u2502\u258eThree\u00a0Flavours\u00a0Cornetto\u258a\u2502 \u2502\u258e\u258a\u2502 \u2502\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2502 \u2502The\u00a0Three\u00a0Flavours\u00a0Cornetto\u00a0trilogy\u00a0is\u00a0an\u00a0anthology\u00a0series\u2502 \u2502of\u00a0British\u00a0comedic\u00a0genre\u00a0films\u00a0directed\u00a0by\u00a0Edgar\u00a0Wright.\u2502 \u2502\u2502 \u2502\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Shaun\u00a0of\u00a0the\u00a0Dead\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502 \u2502\u2502 \u2502\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2502 \u2502\u258e\u258a\u2502 \u2502\u258eFlavour\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0UK\u00a0Release\u00a0Date\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Director\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a\u2502 \u2502\u258e\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0\u258a\u2502 \u2502\u258eStrawberry\u00a0\u00a0\u00a0\u00a0\u00a02004-04-09\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Edgar\u00a0Wright\u00a0\u00a0\u00a0\u00a0\u258a\u2502 \u2502\u258e\u258a\u2502 \u2502\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2502 \u2502\u2502 \u2502\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Hot\u00a0Fuzz\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502 \u2502\u2502 \u2502\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2502 \u2502\u258e\u258a\u2502 \u2502\u258eFlavour\u00a0\u00a0\u00a0\u00a0\u00a0UK\u00a0Release\u00a0Date\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Director\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a\u2502 \u2502\u258e\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0\u258a\u2502 \u2502\u258eClassico\u00a0\u00a0\u00a0\u00a02007-02-17\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Edgar\u00a0Wright\u00a0\u00a0\u00a0\u00a0\u00a0\u258a\u2502 \u2502\u258e\u258a\u2502 \u2502\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2502 \u2502\u2502 \u2502\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0The\u00a0World's\u00a0End\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502 \u2502\u2502 \u2502\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2502 \u2502\u258e\u258a\u2502 \u2502\u258eFlavour\u00a0\u00a0\u00a0\u00a0UK\u00a0Release\u00a0Date\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Director\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f

"},{"location":"widgets/content_switcher/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description current str | None None The ID of the currently-visible child. None means nothing is visible."},{"location":"widgets/content_switcher/#messages","title":"Messages","text":"

This widget posts no messages.

"},{"location":"widgets/content_switcher/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/content_switcher/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/content_switcher/#textual.widgets.ContentSwitcher","title":"textual.widgets.ContentSwitcher class","text":"
def __init__(\nself,\n*children,\nname=None,\nid=None,\nclasses=None,\ndisabled=False,\ninitial=None\n):\n

Bases: Container

A widget for switching between different children.

Note

All child widgets that are to be switched between need a unique ID. Children that have no ID will be hidden and ignored.

Parameters Name Type Description Default *children Widget

The widgets to switch between.

() name str | None

The name of the content switcher.

None id str | None

The ID of the content switcher in the DOM.

None classes str | None

The CSS classes of the content switcher.

None disabled bool

Whether the content switcher is disabled or not.

False initial str | None

The ID of the initial widget to show, None or empty string for the first tab.

None Note

If initial is not supplied no children will be shown to start with.

"},{"location":"widgets/content_switcher/#textual.widgets._content_switcher.ContentSwitcher.current","title":"current instance-attribute class-attribute","text":"
current: reactive[str | None] = reactive[Optional[str]](\nNone, init=False\n)\n

The ID of the currently-displayed widget.

If set to None then no widget is visible.

Note

If set to an unknown ID, this will result in NoMatches being raised.

"},{"location":"widgets/content_switcher/#textual.widgets._content_switcher.ContentSwitcher.visible_content","title":"visible_content property","text":"
visible_content: Widget | None\n

A reference to the currently-visible widget.

None if nothing is visible.

"},{"location":"widgets/content_switcher/#textual.widgets._content_switcher.ContentSwitcher.watch_current","title":"watch_current method","text":"
def watch_current(self, old, new):\n

React to the current visible child choice being changed.

Parameters Name Type Description Default old str | None

The old widget ID (or None if there was no widget).

required new str | None

The new widget ID (or None if nothing should be shown).

required"},{"location":"widgets/data_table/","title":"DataTable","text":"

A table widget optimized for displaying a lot of data.

  • Focusable
  • Container
"},{"location":"widgets/data_table/#guide","title":"Guide","text":""},{"location":"widgets/data_table/#adding-data","title":"Adding data","text":"

The following example shows how to fill a table with data. First, we use add_columns to include the lane, swimmer, country, and time columns in the table. After that, we use the add_rows method to insert the rows into the table.

Outputdata_table.py

TableApp \u00a0lane\u00a0\u00a0swimmer\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0country\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0time\u00a0\u00a0 \u00a04\u00a0\u00a0\u00a0\u00a0\u00a0Joseph\u00a0Schooling\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Singapore\u00a0\u00a0\u00a0\u00a0\u00a0\u00a050.39\u00a0 \u00a02\u00a0\u00a0\u00a0\u00a0\u00a0Michael\u00a0Phelps\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.14\u00a0 \u00a05\u00a0\u00a0\u00a0\u00a0\u00a0Chad\u00a0le\u00a0Clos\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0South\u00a0Africa\u00a0\u00a0\u00a051.14\u00a0 \u00a06\u00a0\u00a0\u00a0\u00a0\u00a0L\u00e1szl\u00f3\u00a0Cseh\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Hungary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.14\u00a0 \u00a03\u00a0\u00a0\u00a0\u00a0\u00a0Li\u00a0Zhuhao\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0China\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.26\u00a0 \u00a08\u00a0\u00a0\u00a0\u00a0\u00a0Mehdy\u00a0Metella\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0France\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.58\u00a0 \u00a07\u00a0\u00a0\u00a0\u00a0\u00a0Tom\u00a0Shields\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.73\u00a0 \u00a01\u00a0\u00a0\u00a0\u00a0\u00a0Aleksandr\u00a0Sadovnikov\u00a0\u00a0Russia\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0 \u00a010\u00a0\u00a0\u00a0\u00a0Darren\u00a0Burns\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Scotland\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0

from textual.app import App, ComposeResult\nfrom textual.widgets import DataTable\nROWS = [\n(\"lane\", \"swimmer\", \"country\", \"time\"),\n(4, \"Joseph Schooling\", \"Singapore\", 50.39),\n(2, \"Michael Phelps\", \"United States\", 51.14),\n(5, \"Chad le Clos\", \"South Africa\", 51.14),\n(6, \"L\u00e1szl\u00f3 Cseh\", \"Hungary\", 51.14),\n(3, \"Li Zhuhao\", \"China\", 51.26),\n(8, \"Mehdy Metella\", \"France\", 51.58),\n(7, \"Tom Shields\", \"United States\", 51.73),\n(1, \"Aleksandr Sadovnikov\", \"Russia\", 51.84),\n(10, \"Darren Burns\", \"Scotland\", 51.84),\n]\nclass TableApp(App):\ndef compose(self) -> ComposeResult:\nyield DataTable()\ndef on_mount(self) -> None:\ntable = self.query_one(DataTable)\ntable.add_columns(*ROWS[0])\ntable.add_rows(ROWS[1:])\napp = TableApp()\nif __name__ == \"__main__\":\napp.run()\n

To add a single row or column use add_row and add_column, respectively.

"},{"location":"widgets/data_table/#styling-and-justifying-cells","title":"Styling and justifying cells","text":"

Cells can contain more than just plain strings - Rich renderables such as Text are also supported. Text objects provide an easy way to style and justify cell content:

Outputdata_table_renderables.py

TableApp \u00a0lane\u00a0\u00a0swimmer\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0country\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0time\u00a0\u00a0 \u00a0\u00a0\u00a04\u00a0\u00a0\u00a0\u00a0Joseph\u00a0Schooling\u00a0\u00a0\u00a0\u00a0Singapore50.39 \u00a0\u00a0\u00a02\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Michael\u00a0PhelpsUnited\u00a0States51.14 \u00a0\u00a0\u00a05\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Chad\u00a0le\u00a0Clos\u00a0South\u00a0Africa51.14 \u00a0\u00a0\u00a06\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0L\u00e1szl\u00f3\u00a0Cseh\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Hungary51.14 \u00a0\u00a0\u00a03\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Li\u00a0Zhuhao\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0China51.26 \u00a0\u00a0\u00a08\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Mehdy\u00a0Metella\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0France51.58 \u00a0\u00a0\u00a07\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Tom\u00a0ShieldsUnited\u00a0States51.73 \u00a0\u00a0\u00a01Aleksandr\u00a0Sadovnikov\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Russia51.84 \u00a0\u00a010\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Darren\u00a0Burns\u00a0\u00a0\u00a0\u00a0\u00a0Scotland51.84

from rich.text import Text\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import DataTable\nROWS = [\n(\"lane\", \"swimmer\", \"country\", \"time\"),\n(4, \"Joseph Schooling\", \"Singapore\", 50.39),\n(2, \"Michael Phelps\", \"United States\", 51.14),\n(5, \"Chad le Clos\", \"South Africa\", 51.14),\n(6, \"L\u00e1szl\u00f3 Cseh\", \"Hungary\", 51.14),\n(3, \"Li Zhuhao\", \"China\", 51.26),\n(8, \"Mehdy Metella\", \"France\", 51.58),\n(7, \"Tom Shields\", \"United States\", 51.73),\n(1, \"Aleksandr Sadovnikov\", \"Russia\", 51.84),\n(10, \"Darren Burns\", \"Scotland\", 51.84),\n]\nclass TableApp(App):\ndef compose(self) -> ComposeResult:\nyield DataTable()\ndef on_mount(self) -> None:\ntable = self.query_one(DataTable)\ntable.add_columns(*ROWS[0])\nfor row in ROWS[1:]:\n# Adding styled and justified `Text` objects instead of plain strings.\nstyled_row = [\nText(str(cell), style=\"italic #03AC13\", justify=\"right\") for cell in row\n]\ntable.add_row(*styled_row)\napp = TableApp()\nif __name__ == \"__main__\":\napp.run()\n
"},{"location":"widgets/data_table/#keys","title":"Keys","text":"

When adding a row to the table, you can supply a key to add_row. A key is a unique identifier for that row. If you don't supply a key, Textual will generate one for you and return it from add_row. This key can later be used to reference the row, regardless of its current position in the table.

When working with data from a database, for example, you may wish to set the row key to the primary key of the data to ensure uniqueness. The method add_column also accepts a key argument and works similarly.

Keys are important because cells in a data table can change location due to factors like row deletion and sorting. Thus, using keys instead of coordinates allows us to refer to data without worrying about its current location in the table.

If you want to change the table based solely on coordinates, you can use the coordinate_to_cell_key method to convert a coordinate to a cell key, which is a (row_key, column_key) pair.

"},{"location":"widgets/data_table/#cursors","title":"Cursors","text":"

The coordinate of the cursor is exposed via the cursor_coordinate reactive attribute. Three types of cursors are supported: cell, row, and column. Change the cursor type by assigning to the cursor_type reactive attribute.

Column CursorRow CursorCell Cursordata_table_cursors.py

TableApp \u00a0lane\u00a0\u00a0swimmer\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0country\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0time\u00a0\u00a0 \u00a04\u00a0\u00a0\u00a0\u00a0\u00a0Joseph\u00a0Schooling\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Singapore\u00a0\u00a0\u00a0\u00a0\u00a0\u00a050.39\u00a0 \u00a02\u00a0\u00a0\u00a0\u00a0\u00a0Michael\u00a0Phelps\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.14\u00a0 \u00a05\u00a0\u00a0\u00a0\u00a0\u00a0Chad\u00a0le\u00a0Clos\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0South\u00a0Africa\u00a0\u00a0\u00a051.14\u00a0 \u00a06\u00a0\u00a0\u00a0\u00a0\u00a0L\u00e1szl\u00f3\u00a0Cseh\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Hungary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.14\u00a0 \u00a03\u00a0\u00a0\u00a0\u00a0\u00a0Li\u00a0Zhuhao\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0China\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.26\u00a0 \u00a08\u00a0\u00a0\u00a0\u00a0\u00a0Mehdy\u00a0Metella\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0France\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.58\u00a0 \u00a07\u00a0\u00a0\u00a0\u00a0\u00a0Tom\u00a0Shields\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.73\u00a0 \u00a01\u00a0\u00a0\u00a0\u00a0\u00a0Aleksandr\u00a0Sadovnikov\u00a0\u00a0Russia\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0 \u00a010\u00a0\u00a0\u00a0\u00a0Darren\u00a0Burns\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Scotland\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0

TableApp \u00a0lane\u00a0\u00a0swimmer\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0country\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0time\u00a0\u00a0 \u00a04\u00a0\u00a0\u00a0\u00a0\u00a0Joseph\u00a0Schooling\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Singapore\u00a0\u00a0\u00a0\u00a0\u00a0\u00a050.39\u00a0 \u00a02\u00a0\u00a0\u00a0\u00a0\u00a0Michael\u00a0Phelps\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.14\u00a0 \u00a05\u00a0\u00a0\u00a0\u00a0\u00a0Chad\u00a0le\u00a0Clos\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0South\u00a0Africa\u00a0\u00a0\u00a051.14\u00a0 \u00a06\u00a0\u00a0\u00a0\u00a0\u00a0L\u00e1szl\u00f3\u00a0Cseh\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Hungary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.14\u00a0 \u00a03\u00a0\u00a0\u00a0\u00a0\u00a0Li\u00a0Zhuhao\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0China\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.26\u00a0 \u00a08\u00a0\u00a0\u00a0\u00a0\u00a0Mehdy\u00a0Metella\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0France\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.58\u00a0 \u00a07\u00a0\u00a0\u00a0\u00a0\u00a0Tom\u00a0Shields\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.73\u00a0 \u00a01\u00a0\u00a0\u00a0\u00a0\u00a0Aleksandr\u00a0Sadovnikov\u00a0\u00a0Russia\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0 \u00a010\u00a0\u00a0\u00a0\u00a0Darren\u00a0Burns\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Scotland\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0

TableApp \u00a0lane\u00a0\u00a0swimmer\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0country\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0time\u00a0\u00a0 \u00a04\u00a0\u00a0\u00a0\u00a0\u00a0Joseph\u00a0Schooling\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Singapore\u00a0\u00a0\u00a0\u00a0\u00a0\u00a050.39\u00a0 \u00a02\u00a0\u00a0\u00a0\u00a0\u00a0Michael\u00a0Phelps\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.14\u00a0 \u00a05\u00a0\u00a0\u00a0\u00a0\u00a0Chad\u00a0le\u00a0Clos\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0South\u00a0Africa\u00a0\u00a0\u00a051.14\u00a0 \u00a06\u00a0\u00a0\u00a0\u00a0\u00a0L\u00e1szl\u00f3\u00a0Cseh\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Hungary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.14\u00a0 \u00a03\u00a0\u00a0\u00a0\u00a0\u00a0Li\u00a0Zhuhao\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0China\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.26\u00a0 \u00a08\u00a0\u00a0\u00a0\u00a0\u00a0Mehdy\u00a0Metella\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0France\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.58\u00a0 \u00a07\u00a0\u00a0\u00a0\u00a0\u00a0Tom\u00a0Shields\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.73\u00a0 \u00a01\u00a0\u00a0\u00a0\u00a0\u00a0Aleksandr\u00a0Sadovnikov\u00a0\u00a0Russia\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0 \u00a010\u00a0\u00a0\u00a0\u00a0Darren\u00a0Burns\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Scotland\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0

from itertools import cycle\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import DataTable\nROWS = [\n(\"lane\", \"swimmer\", \"country\", \"time\"),\n(4, \"Joseph Schooling\", \"Singapore\", 50.39),\n(2, \"Michael Phelps\", \"United States\", 51.14),\n(5, \"Chad le Clos\", \"South Africa\", 51.14),\n(6, \"L\u00e1szl\u00f3 Cseh\", \"Hungary\", 51.14),\n(3, \"Li Zhuhao\", \"China\", 51.26),\n(8, \"Mehdy Metella\", \"France\", 51.58),\n(7, \"Tom Shields\", \"United States\", 51.73),\n(1, \"Aleksandr Sadovnikov\", \"Russia\", 51.84),\n(10, \"Darren Burns\", \"Scotland\", 51.84),\n]\ncursors = cycle([\"column\", \"row\", \"cell\"])\nclass TableApp(App):\ndef compose(self) -> ComposeResult:\nyield DataTable()\ndef on_mount(self) -> None:\ntable = self.query_one(DataTable)\ntable.cursor_type = next(cursors)\ntable.zebra_stripes = True\ntable.add_columns(*ROWS[0])\ntable.add_rows(ROWS[1:])\ndef key_c(self):\ntable = self.query_one(DataTable)\ntable.cursor_type = next(cursors)\napp = TableApp()\nif __name__ == \"__main__\":\napp.run()\n

You can change the position of the cursor using the arrow keys, Page Up, Page Down, Home and End, or by assigning to the cursor_coordinate reactive attribute.

"},{"location":"widgets/data_table/#updating-data","title":"Updating data","text":"

Cells can be updated in the DataTable by using the update_cell and update_cell_at methods.

"},{"location":"widgets/data_table/#removing-data","title":"Removing data","text":"

To remove all data in the table, use the clear method. To remove individual rows, use remove_row. The remove_row method accepts a key argument, which identifies the row to be removed.

If you wish to remove the row below the cursor in the DataTable, use coordinate_to_cell_key to get the row key of the row under the current cursor_coordinate, then supply this key to remove_row:

# Get the keys for the row and column under the cursor.\nrow_key, _ = table.coordinate_to_cell_key(table.cursor_coordinate)\n# Supply the row key to `remove_row` to delete the row.\ntable.remove_row(row_key)\n
"},{"location":"widgets/data_table/#removing-columns","title":"Removing columns","text":"

To remove individual columns, use remove_column. The remove_column method accepts a key argument, which identifies the column to be removed.

You can remove the column below the cursor using the same coordinate_to_cell_key method described above:

# Get the keys for the row and column under the cursor.\n_, column_key = table.coordinate_to_cell_key(table.cursor_coordinate)\n# Supply the column key to `column_row` to delete the column.\ntable.remove_column(column_key)\n
"},{"location":"widgets/data_table/#fixed-data","title":"Fixed data","text":"

You can fix a number of rows and columns in place, keeping them pinned to the top and left of the table respectively. To do this, assign an integer to the fixed_rows or fixed_columns reactive attributes of the DataTable.

Fixed datadata_table_fixed.py

TableApp \u00a0A\u00a0\u00a0\u00a0B\u00a0\u00a0\u00a0\u00a0C\u00a0\u00a0\u00a0 \u00a01\u00a0\u00a0\u00a02\u00a0\u00a0\u00a0\u00a03\u00a0\u00a0\u00a0 \u00a02\u00a0\u00a0\u00a04\u00a0\u00a0\u00a0\u00a06\u00a0\u00a0\u00a0 \u00a079\u00a0\u00a0158\u00a0\u00a0237\u00a0 \u00a080\u00a0\u00a0160\u00a0\u00a0240\u00a0 \u00a081\u00a0\u00a0162\u00a0\u00a0243\u00a0 \u00a082\u00a0\u00a0164\u00a0\u00a0246\u00a0 \u00a083\u00a0\u00a0166\u00a0\u00a0249\u00a0 \u00a084\u00a0\u00a0168\u00a0\u00a0252\u00a0 \u00a085\u00a0\u00a0170\u00a0\u00a0255\u00a0 \u00a086\u00a0\u00a0172\u00a0\u00a0258\u00a0 \u00a087\u00a0\u00a0174\u00a0\u00a0261\u00a0 \u00a088\u00a0\u00a0176\u00a0\u00a0264\u00a0 \u00a089\u00a0\u00a0178\u00a0\u00a0267\u00a0 \u00a090\u00a0\u00a0180\u00a0\u00a0270\u00a0 \u00a091\u00a0\u00a0182\u00a0\u00a0273\u00a0 \u00a092\u00a0\u00a0184\u00a0\u00a0276\u00a0 \u00a093\u00a0\u00a0186\u00a0\u00a0279\u00a0 \u00a094\u00a0\u00a0188\u00a0\u00a0282\u00a0\u2587\u2587 \u00a095\u00a0\u00a0190\u00a0\u00a0285\u00a0 \u00a096\u00a0\u00a0192\u00a0\u00a0288\u00a0 \u00a097\u00a0\u00a0194\u00a0\u00a0291\u00a0 \u00a098\u00a0\u00a0196\u00a0\u00a0294\u00a0 \u00a099\u00a0\u00a0198\u00a0\u00a0297\u00a0

from textual.app import App, ComposeResult\nfrom textual.widgets import DataTable\nclass TableApp(App):\nCSS = \"DataTable {height: 1fr}\"\ndef compose(self) -> ComposeResult:\nyield DataTable()\ndef on_mount(self) -> None:\ntable = self.query_one(DataTable)\ntable.focus()\ntable.add_columns(\"A\", \"B\", \"C\")\nfor number in range(1, 100):\ntable.add_row(str(number), str(number * 2), str(number * 3))\ntable.fixed_rows = 2\ntable.fixed_columns = 1\ntable.cursor_type = \"row\"\ntable.zebra_stripes = True\napp = TableApp()\nif __name__ == \"__main__\":\napp.run()\n

In the example above, we set fixed_rows to 2, and fixed_columns to 1, meaning the first two rows and the leftmost column do not scroll - they always remain visible as you scroll through the data table.

"},{"location":"widgets/data_table/#sorting","title":"Sorting","text":"

The DataTable can be sorted using the sort method. In order to sort your data by a column, you must have supplied a key to the add_column method when you added it. You can then pass this key to the sort method to sort by that column. Additionally, you can sort by multiple columns by passing multiple keys to sort.

"},{"location":"widgets/data_table/#labelled-rows","title":"Labelled rows","text":"

A \"label\" can be attached to a row using the add_row method. This will add an extra column to the left of the table which the cursor cannot interact with. This column is similar to the leftmost column in a spreadsheet containing the row numbers. The example below shows how to attach simple numbered labels to rows.

Labelled rowsdata_table_labels.py

TableApp \u00a0lane\u00a0\u00a0swimmer\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0country\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0time\u00a0\u00a0 1\u00a04\u00a0\u00a0\u00a0\u00a0\u00a0Joseph\u00a0Schooling\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Singapore\u00a0\u00a0\u00a0\u00a0\u00a0\u00a050.39\u00a0 2\u00a02\u00a0\u00a0\u00a0\u00a0\u00a0Michael\u00a0Phelps\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.14\u00a0 3\u00a05\u00a0\u00a0\u00a0\u00a0\u00a0Chad\u00a0le\u00a0Clos\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0South\u00a0Africa\u00a0\u00a0\u00a051.14\u00a0 4\u00a06\u00a0\u00a0\u00a0\u00a0\u00a0L\u00e1szl\u00f3\u00a0Cseh\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Hungary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.14\u00a0 5\u00a03\u00a0\u00a0\u00a0\u00a0\u00a0Li\u00a0Zhuhao\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0China\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.26\u00a0 6\u00a08\u00a0\u00a0\u00a0\u00a0\u00a0Mehdy\u00a0Metella\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0France\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.58\u00a0 7\u00a07\u00a0\u00a0\u00a0\u00a0\u00a0Tom\u00a0Shields\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0United\u00a0States\u00a0\u00a051.73\u00a0 8\u00a01\u00a0\u00a0\u00a0\u00a0\u00a0Aleksandr\u00a0Sadovnikov\u00a0\u00a0Russia\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0 9\u00a010\u00a0\u00a0\u00a0\u00a0Darren\u00a0Burns\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Scotland\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a051.84\u00a0

from rich.text import Text\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import DataTable\nROWS = [\n(\"lane\", \"swimmer\", \"country\", \"time\"),\n(4, \"Joseph Schooling\", \"Singapore\", 50.39),\n(2, \"Michael Phelps\", \"United States\", 51.14),\n(5, \"Chad le Clos\", \"South Africa\", 51.14),\n(6, \"L\u00e1szl\u00f3 Cseh\", \"Hungary\", 51.14),\n(3, \"Li Zhuhao\", \"China\", 51.26),\n(8, \"Mehdy Metella\", \"France\", 51.58),\n(7, \"Tom Shields\", \"United States\", 51.73),\n(1, \"Aleksandr Sadovnikov\", \"Russia\", 51.84),\n(10, \"Darren Burns\", \"Scotland\", 51.84),\n]\nclass TableApp(App):\ndef compose(self) -> ComposeResult:\nyield DataTable()\ndef on_mount(self) -> None:\ntable = self.query_one(DataTable)\ntable.add_columns(*ROWS[0])\nfor number, row in enumerate(ROWS[1:], start=1):\nlabel = Text(str(number), style=\"#B0FC38 italic\")\ntable.add_row(*row, label=label)\napp = TableApp()\nif __name__ == \"__main__\":\napp.run()\n
"},{"location":"widgets/data_table/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description show_header bool True Show the table header show_row_labels bool True Show the row labels (if applicable) fixed_rows int 0 Number of fixed rows (rows which do not scroll) fixed_columns int 0 Number of fixed columns (columns which do not scroll) zebra_stripes bool False Display alternating colors on rows header_height int 1 Height of header row show_cursor bool True Show the cursor cursor_type str \"cell\" One of \"cell\", \"row\", \"column\", or \"none\" cursor_coordinate Coordinate Coordinate(0, 0) The current coordinate of the cursor hover_coordinate Coordinate Coordinate(0, 0) The coordinate the mouse cursor is above"},{"location":"widgets/data_table/#messages","title":"Messages","text":"
  • DataTable.CellHighlighted
  • DataTable.CellSelected
  • DataTable.RowHighlighted
  • DataTable.RowSelected
  • DataTable.ColumnHighlighted
  • DataTable.ColumnSelected
  • DataTable.HeaderSelected
  • DataTable.RowLabelSelected
"},{"location":"widgets/data_table/#bindings","title":"Bindings","text":"

The data table widget defines the following bindings:

Key(s) Description enter Select cells under the cursor. up Move the cursor up. down Move the cursor down. right Move the cursor right. left Move the cursor left."},{"location":"widgets/data_table/#component-classes","title":"Component Classes","text":"

The data table widget provides the following component classes:

Class Description datatable--cursor Target the cursor. datatable--hover Target the cells under the hover cursor. datatable--fixed Target fixed columns and fixed rows. datatable--fixed-cursor Target highlighted and fixed columns or header. datatable--header Target the header of the data table. datatable--header-cursor Target cells highlighted by the cursor. datatable--header-hover Target hovered header or row label cells. datatable--even-row Target even rows (row indices start at 0). datatable--odd-row Target odd rows (row indices start at 0)."},{"location":"widgets/data_table/#textual.widgets.DataTable","title":"textual.widgets.DataTable class","text":"
def __init__(\nself,\n*,\nshow_header=True,\nshow_row_labels=True,\nfixed_rows=0,\nfixed_columns=0,\nzebra_stripes=False,\nheader_height=1,\nshow_cursor=True,\ncursor_foreground_priority=\"css\",\ncursor_background_priority=\"renderable\",\ncursor_type=\"cell\",\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: ScrollView, Generic[CellType]

A tabular widget that contains data.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.BINDINGS","title":"BINDINGS class-attribute","text":"
BINDINGS: list[BindingType] = [\nBinding(\"enter\", \"select_cursor\", \"Select\", show=False),\nBinding(\"up\", \"cursor_up\", \"Cursor Up\", show=False),\nBinding(\n\"down\", \"cursor_down\", \"Cursor Down\", show=False\n),\nBinding(\n\"right\", \"cursor_right\", \"Cursor Right\", show=False\n),\nBinding(\n\"left\", \"cursor_left\", \"Cursor Left\", show=False\n),\nBinding(\"pageup\", \"page_up\", \"Page Up\", show=False),\nBinding(\n\"pagedown\", \"page_down\", \"Page Down\", show=False\n),\n]\n
Key(s) Description enter Select cells under the cursor. up Move the cursor up. down Move the cursor down. right Move the cursor right. left Move the cursor left."},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
COMPONENT_CLASSES: set[str] = {\n\"datatable--cursor\",\n\"datatable--hover\",\n\"datatable--fixed\",\n\"datatable--fixed-cursor\",\n\"datatable--header\",\n\"datatable--header-cursor\",\n\"datatable--header-hover\",\n\"datatable--odd-row\",\n\"datatable--even-row\",\n}\n
Class Description datatable--cursor Target the cursor. datatable--hover Target the cells under the hover cursor. datatable--fixed Target fixed columns and fixed rows. datatable--fixed-cursor Target highlighted and fixed columns or header. datatable--header Target the header of the data table. datatable--header-cursor Target cells highlighted by the cursor. datatable--header-hover Target hovered header or row label cells. datatable--even-row Target even rows (row indices start at 0). datatable--odd-row Target odd rows (row indices start at 0)."},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.columns","title":"columns instance-attribute","text":"
columns: dict[ColumnKey, Column] = {}\n

Metadata about the columns of the table, indexed by their key.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.cursor_background_priority","title":"cursor_background_priority instance-attribute","text":"
cursor_background_priority = cursor_background_priority\n

Should we prioritize the cursor component class CSS background or the renderable background in the event where a cell contains a renderable with a background color.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.cursor_column","title":"cursor_column property","text":"
cursor_column: int\n

The index of the column that the DataTable cursor is currently on.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.cursor_coordinate","title":"cursor_coordinate instance-attribute class-attribute","text":"
cursor_coordinate: Reactive[Coordinate] = Reactive(\nCoordinate(0, 0), repaint=False, always_update=True\n)\n

Current cursor Coordinate.

This can be set programmatically or changed via the method move_cursor.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.cursor_foreground_priority","title":"cursor_foreground_priority instance-attribute","text":"
cursor_foreground_priority = cursor_foreground_priority\n

Should we prioritize the cursor component class CSS foreground or the renderable foreground in the event where a cell contains a renderable with a foreground color.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.cursor_row","title":"cursor_row property","text":"
cursor_row: int\n

The index of the row that the DataTable cursor is currently on.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.cursor_type","title":"cursor_type instance-attribute class-attribute","text":"
cursor_type: Reactive[CursorType] = cursor_type\n

The type of cursor of the DataTable.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.fixed_columns","title":"fixed_columns instance-attribute class-attribute","text":"
fixed_columns = fixed_columns\n

The number of columns to fix (prevented from scrolling).

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.fixed_rows","title":"fixed_rows instance-attribute class-attribute","text":"
fixed_rows = fixed_rows\n

The number of rows to fix (prevented from scrolling).

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.header_height","title":"header_height instance-attribute class-attribute","text":"
header_height = header_height\n

The height of the header row (the row of column labels).

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.hover_column","title":"hover_column property","text":"
hover_column: int\n

The index of the column that the mouse cursor is currently hovering above.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.hover_coordinate","title":"hover_coordinate instance-attribute class-attribute","text":"
hover_coordinate: Reactive[Coordinate] = Reactive(\nCoordinate(0, 0), repaint=False, always_update=True\n)\n

The coordinate of the DataTable that is being hovered.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.hover_row","title":"hover_row property","text":"
hover_row: int\n

The index of the row that the mouse cursor is currently hovering above.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ordered_columns","title":"ordered_columns property","text":"
ordered_columns: list[Column]\n

The list of Columns in the DataTable, ordered as they appear on screen.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ordered_rows","title":"ordered_rows property","text":"
ordered_rows: list[Row]\n

The list of Rows in the DataTable, ordered as they appear on screen.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.row_count","title":"row_count property","text":"
row_count: int\n

The number of rows currently present in the DataTable.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.rows","title":"rows instance-attribute","text":"
rows: dict[RowKey, Row] = {}\n

Metadata about the rows of the table, indexed by their key.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.show_cursor","title":"show_cursor instance-attribute class-attribute","text":"
show_cursor = show_cursor\n

Show/hide both the keyboard and hover cursor.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.show_header","title":"show_header instance-attribute class-attribute","text":"
show_header = show_header\n

Show/hide the header row (the row of column labels).

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.show_row_labels","title":"show_row_labels instance-attribute class-attribute","text":"
show_row_labels = show_row_labels\n

Show/hide the column containing the labels of rows.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.zebra_stripes","title":"zebra_stripes instance-attribute class-attribute","text":"
zebra_stripes = zebra_stripes\n

Apply zebra effect on row backgrounds (light, dark, light, dark, ...).

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellHighlighted","title":"CellHighlighted class","text":"
def __init__(self, data_table, value, coordinate, cell_key):\n

Bases: Message

Posted when the cursor moves to highlight a new cell.

This is only relevant when the cursor_type is \"cell\". It's also posted when the cell cursor is re-enabled (by setting show_cursor=True), and when the cursor type is changed to \"cell\". Can be handled using on_data_table_cell_highlighted in a subclass of DataTable or in a parent widget in the DOM.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellHighlighted.cell_key","title":"cell_key instance-attribute","text":"
cell_key: CellKey = cell_key\n

The key for the highlighted cell.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellHighlighted.control","title":"control property","text":"
control: DataTable\n

Alias for the data table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellHighlighted.coordinate","title":"coordinate instance-attribute","text":"
coordinate: Coordinate = coordinate\n

The coordinate of the highlighted cell.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellHighlighted.data_table","title":"data_table instance-attribute","text":"
data_table = data_table\n

The data table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellHighlighted.value","title":"value instance-attribute","text":"
value: CellType = value\n

The value in the highlighted cell.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellSelected","title":"CellSelected class","text":"
def __init__(self, data_table, value, coordinate, cell_key):\n

Bases: Message

Posted by the DataTable widget when a cell is selected.

This is only relevant when the cursor_type is \"cell\". Can be handled using on_data_table_cell_selected in a subclass of DataTable or in a parent widget in the DOM.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellSelected.cell_key","title":"cell_key instance-attribute","text":"
cell_key: CellKey = cell_key\n

The key for the selected cell.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellSelected.control","title":"control property","text":"
control: DataTable\n

Alias for the data table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellSelected.coordinate","title":"coordinate instance-attribute","text":"
coordinate: Coordinate = coordinate\n

The coordinate of the cell that was selected.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellSelected.data_table","title":"data_table instance-attribute","text":"
data_table = data_table\n

The data table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.CellSelected.value","title":"value instance-attribute","text":"
value: CellType = value\n

The value in the cell that was selected.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnHighlighted","title":"ColumnHighlighted class","text":"
def __init__(self, data_table, cursor_column, column_key):\n

Bases: Message

Posted when a column is highlighted.

This message is only posted when the cursor_type is set to \"column\". Can be handled using on_data_table_column_highlighted in a subclass of DataTable or in a parent widget in the DOM.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnHighlighted.column_key","title":"column_key instance-attribute","text":"
column_key = column_key\n

The key of the column that was highlighted.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnHighlighted.control","title":"control property","text":"
control: DataTable\n

Alias for the data table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnHighlighted.cursor_column","title":"cursor_column instance-attribute","text":"
cursor_column: int = cursor_column\n

The x-coordinate of the column that was highlighted.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnHighlighted.data_table","title":"data_table instance-attribute","text":"
data_table = data_table\n

The data table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnSelected","title":"ColumnSelected class","text":"
def __init__(self, data_table, cursor_column, column_key):\n

Bases: Message

Posted when a column is selected.

This message is only posted when the cursor_type is set to \"column\". Can be handled using on_data_table_column_selected in a subclass of DataTable or in a parent widget in the DOM.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnSelected.column_key","title":"column_key instance-attribute","text":"
column_key = column_key\n

The key of the column that was selected.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnSelected.control","title":"control property","text":"
control: DataTable\n

Alias for the data table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnSelected.cursor_column","title":"cursor_column instance-attribute","text":"
cursor_column: int = cursor_column\n

The x-coordinate of the column that was selected.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.ColumnSelected.data_table","title":"data_table instance-attribute","text":"
data_table = data_table\n

The data table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.HeaderSelected","title":"HeaderSelected class","text":"
def __init__(self, data_table, column_key, column_index, label):\n

Bases: Message

Posted when a column header/label is clicked.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.HeaderSelected.column_index","title":"column_index instance-attribute","text":"
column_index = column_index\n

The index for the column.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.HeaderSelected.column_key","title":"column_key instance-attribute","text":"
column_key = column_key\n

The key for the column.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.HeaderSelected.control","title":"control property","text":"
control: DataTable\n

Alias for the data table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.HeaderSelected.data_table","title":"data_table instance-attribute","text":"
data_table = data_table\n

The data table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.HeaderSelected.label","title":"label instance-attribute","text":"
label = label\n

The text of the label.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowHighlighted","title":"RowHighlighted class","text":"
def __init__(self, data_table, cursor_row, row_key):\n

Bases: Message

Posted when a row is highlighted.

This message is only posted when the cursor_type is set to \"row\". Can be handled using on_data_table_row_highlighted in a subclass of DataTable or in a parent widget in the DOM.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowHighlighted.control","title":"control property","text":"
control: DataTable\n

Alias for the data table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowHighlighted.cursor_row","title":"cursor_row instance-attribute","text":"
cursor_row: int = cursor_row\n

The y-coordinate of the cursor that highlighted the row.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowHighlighted.data_table","title":"data_table instance-attribute","text":"
data_table = data_table\n

The data table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowHighlighted.row_key","title":"row_key instance-attribute","text":"
row_key: RowKey = row_key\n

The key of the row that was highlighted.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowLabelSelected","title":"RowLabelSelected class","text":"
def __init__(self, data_table, row_key, row_index, label):\n

Bases: Message

Posted when a row label is clicked.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowLabelSelected.control","title":"control property","text":"
control: DataTable\n

Alias for the data table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowLabelSelected.data_table","title":"data_table instance-attribute","text":"
data_table = data_table\n

The data table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowLabelSelected.label","title":"label instance-attribute","text":"
label = label\n

The text of the label.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowLabelSelected.row_index","title":"row_index instance-attribute","text":"
row_index = row_index\n

The index for the column.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowLabelSelected.row_key","title":"row_key instance-attribute","text":"
row_key = row_key\n

The key for the column.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowSelected","title":"RowSelected class","text":"
def __init__(self, data_table, cursor_row, row_key):\n

Bases: Message

Posted when a row is selected.

This message is only posted when the cursor_type is set to \"row\". Can be handled using on_data_table_row_selected in a subclass of DataTable or in a parent widget in the DOM.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowSelected.control","title":"control property","text":"
control: DataTable\n

Alias for the data table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowSelected.cursor_row","title":"cursor_row instance-attribute","text":"
cursor_row: int = cursor_row\n

The y-coordinate of the cursor that made the selection.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowSelected.data_table","title":"data_table instance-attribute","text":"
data_table = data_table\n

The data table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.RowSelected.row_key","title":"row_key instance-attribute","text":"
row_key: RowKey = row_key\n

The key of the row that was selected.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.action_page_down","title":"action_page_down method","text":"
def action_page_down(self):\n

Move the cursor one page down.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.action_page_up","title":"action_page_up method","text":"
def action_page_up(self):\n

Move the cursor one page up.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.action_scroll_end","title":"action_scroll_end method","text":"
def action_scroll_end(self):\n

Scroll to the bottom of the data table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.action_scroll_home","title":"action_scroll_home method","text":"
def action_scroll_home(self):\n

Scroll to the top of the data table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.add_column","title":"add_column method","text":"
def add_column(\nself, label, *, width=None, key=None, default=None\n):\n

Add a column to the table.

Parameters Name Type Description Default label TextType

A str or Text object containing the label (shown top of column).

required width int | None

Width of the column in cells or None to fit content.

None key str | None

A key which uniquely identifies this column. If None, it will be generated for you.

None default CellType | None

The value to insert into pre-existing rows.

None Returns Type Description ColumnKey

Uniquely identifies this column. Can be used to retrieve this column regardless of its current location in the DataTable (it could have moved after being added due to sorting/insertion/deletion of other columns).

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.add_columns","title":"add_columns method","text":"
def add_columns(self, *labels):\n

Add a number of columns.

Parameters Name Type Description Default *labels TextType

Column headers.

() Returns Type Description list[ColumnKey]

A list of the keys for the columns that were added. See the add_column method docstring for more information on how these keys are used.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.add_row","title":"add_row method","text":"
def add_row(self, *cells, height=1, key=None, label=None):\n

Add a row at the bottom of the DataTable.

Parameters Name Type Description Default *cells CellType

Positional arguments should contain cell data.

() height int | None

The height of a row (in lines). Use None to auto-detect the optimal height.

1 key str | None

A key which uniquely identifies this row. If None, it will be generated for you and returned.

None label TextType | None

The label for the row. Will be displayed to the left if supplied.

None Returns Type Description RowKey

Unique identifier for this row. Can be used to retrieve this row regardless of its current location in the DataTable (it could have moved after being added due to sorting or insertion/deletion of other rows).

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.add_rows","title":"add_rows method","text":"
def add_rows(self, rows):\n

Add a number of rows at the bottom of the DataTable.

Parameters Name Type Description Default rows Iterable[Iterable[CellType]]

Iterable of rows. A row is an iterable of cells.

required Returns Type Description list[RowKey]

A list of the keys for the rows that were added. See the add_row method docstring for more information on how these keys are used.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.clear","title":"clear method","text":"
def clear(self, columns=False):\n

Clear the table.

Parameters Name Type Description Default columns bool

Also clear the columns.

False Returns Type Description Self

The DataTable instance.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.coordinate_to_cell_key","title":"coordinate_to_cell_key method","text":"
def coordinate_to_cell_key(self, coordinate):\n

Return the key for the cell currently occupying this coordinate.

Parameters Name Type Description Default coordinate Coordinate

The coordinate to exam the current cell key of.

required Returns Type Description CellKey

The key of the cell currently occupying this coordinate.

Raises Type Description CellDoesNotExist

If the coordinate is not valid.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_cell","title":"get_cell method","text":"
def get_cell(self, row_key, column_key):\n

Given a row key and column key, return the value of the corresponding cell.

Parameters Name Type Description Default row_key RowKey | str

The row key of the cell.

required column_key ColumnKey | str

The column key of the cell.

required Returns Type Description CellType

The value of the cell identified by the row and column keys.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_cell_at","title":"get_cell_at method","text":"
def get_cell_at(self, coordinate):\n

Get the value from the cell occupying the given coordinate.

Parameters Name Type Description Default coordinate Coordinate

The coordinate to retrieve the value from.

required Returns Type Description CellType

The value of the cell at the coordinate.

Raises Type Description CellDoesNotExist

If there is no cell with the given coordinate.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_cell_coordinate","title":"get_cell_coordinate method","text":"
def get_cell_coordinate(self, row_key, column_key):\n

Given a row key and column key, return the corresponding cell coordinate.

Parameters Name Type Description Default row_key RowKey | str

The row key of the cell.

required column_key Column | str

The column key of the cell.

required Returns Type Description Coordinate

The current coordinate of the cell identified by the row and column keys.

Raises Type Description CellDoesNotExist

If the specified cell does not exist.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_column","title":"get_column method","text":"
def get_column(self, column_key):\n

Get the values from the column identified by the given column key.

Parameters Name Type Description Default column_key ColumnKey | str

The key of the column.

required Returns Type Description Iterable[CellType]

A generator which yields the cells in the column.

Raises Type Description ColumnDoesNotExist

If there is no column corresponding to the key.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_column_at","title":"get_column_at method","text":"
def get_column_at(self, column_index):\n

Get the values from the column at a given index.

Parameters Name Type Description Default column_index int

The index of the column.

required Returns Type Description Iterable[CellType]

A generator which yields the cells in the column.

Raises Type Description ColumnDoesNotExist

If there is no column with the given index.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_column_index","title":"get_column_index method","text":"
def get_column_index(self, column_key):\n

Return the current index for the column identified by column_key.

Parameters Name Type Description Default column_key ColumnKey | str

The column key to find the current index of.

required Returns Type Description int

The current index of the specified column key.

Raises Type Description ColumnDoesNotExist

If the column key does not exist.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_row","title":"get_row method","text":"
def get_row(self, row_key):\n

Get the values from the row identified by the given row key.

Parameters Name Type Description Default row_key RowKey | str

The key of the row.

required Returns Type Description list[CellType]

A list of the values contained within the row.

Raises Type Description RowDoesNotExist

When there is no row corresponding to the key.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_row_at","title":"get_row_at method","text":"
def get_row_at(self, row_index):\n

Get the values from the cells in a row at a given index. This will return the values from a row based on the rows current position in the table.

Parameters Name Type Description Default row_index int

The index of the row.

required Returns Type Description list[CellType]

A list of the values contained in the row.

Raises Type Description RowDoesNotExist

If there is no row with the given index.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_row_height","title":"get_row_height method","text":"
def get_row_height(self, row_key):\n

Given a row key, return the height of that row in terminal cells.

Parameters Name Type Description Default row_key RowKey

The key of the row.

required Returns Type Description int

The height of the row, measured in terminal character cells.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.get_row_index","title":"get_row_index method","text":"
def get_row_index(self, row_key):\n

Return the current index for the row identified by row_key.

Parameters Name Type Description Default row_key RowKey | str

The row key to find the current index of.

required Returns Type Description int

The current index of the specified row key.

Raises Type Description RowDoesNotExist

If the row key does not exist.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.is_valid_column_index","title":"is_valid_column_index method","text":"
def is_valid_column_index(self, column_index):\n

Return a boolean indicating whether the column_index is within table bounds.

Parameters Name Type Description Default column_index int

The column index to check.

required Returns Type Description bool

True if the column index is within the bounds of the table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.is_valid_coordinate","title":"is_valid_coordinate method","text":"
def is_valid_coordinate(self, coordinate):\n

Return a boolean indicating whether the given coordinate is valid.

Parameters Name Type Description Default coordinate Coordinate

The coordinate to validate.

required Returns Type Description bool

True if the coordinate is within the bounds of the table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.is_valid_row_index","title":"is_valid_row_index method","text":"
def is_valid_row_index(self, row_index):\n

Return a boolean indicating whether the row_index is within table bounds.

Parameters Name Type Description Default row_index int

The row index to check.

required Returns Type Description bool

True if the row index is within the bounds of the table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.move_cursor","title":"move_cursor method","text":"
def move_cursor(self, *, row=None, column=None, animate=False):\n

Move the cursor to the given position.

Example
datatable = app.query_one(DataTable)\ndatatable.move_cursor(row=4, column=6)\n# datatable.cursor_coordinate == Coordinate(4, 6)\ndatatable.move_cursor(row=3)\n# datatable.cursor_coordinate == Coordinate(3, 6)\n
Parameters Name Type Description Default row int | None

The new row to move the cursor to.

None column int | None

The new column to move the cursor to.

None animate bool

Whether to animate the change of coordinates.

False"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.refresh_column","title":"refresh_column method","text":"
def refresh_column(self, column_index):\n

Refresh the column at the given index.

Parameters Name Type Description Default column_index int

The index of the column to refresh.

required Returns Type Description Self

The DataTable instance.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.refresh_coordinate","title":"refresh_coordinate method","text":"
def refresh_coordinate(self, coordinate):\n

Refresh the cell at a coordinate.

Parameters Name Type Description Default coordinate Coordinate

The coordinate to refresh.

required Returns Type Description Self

The DataTable instance.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.refresh_row","title":"refresh_row method","text":"
def refresh_row(self, row_index):\n

Refresh the row at the given index.

Parameters Name Type Description Default row_index int

The index of the row to refresh.

required Returns Type Description Self

The DataTable instance.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.remove_column","title":"remove_column method","text":"
def remove_column(self, column_key):\n

Remove a column (identified by a key) from the DataTable.

Parameters Name Type Description Default column_key ColumnKey | str

The key identifying the column to remove.

required Raises Type Description ColumnDoesNotExist

If the column key does not exist.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.remove_row","title":"remove_row method","text":"
def remove_row(self, row_key):\n

Remove a row (identified by a key) from the DataTable.

Parameters Name Type Description Default row_key RowKey | str

The key identifying the row to remove.

required Raises Type Description RowDoesNotExist

If the row key does not exist.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.sort","title":"sort method","text":"
def sort(self, *columns, reverse=False):\n

Sort the rows in the DataTable by one or more column keys.

Parameters Name Type Description Default columns ColumnKey | str

One or more columns to sort by the values in.

() reverse bool

If True, the sort order will be reversed.

False Returns Type Description Self

The DataTable instance.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.update_cell","title":"update_cell method","text":"
def update_cell(\nself, row_key, column_key, value, *, update_width=False\n):\n

Update the cell identified by the specified row key and column key.

Parameters Name Type Description Default row_key RowKey | str

The key identifying the row.

required column_key ColumnKey | str

The key identifying the column.

required value CellType

The new value to put inside the cell.

required update_width bool

Whether to resize the column width to accommodate for the new cell content.

False Raises Type Description CellDoesNotExist

When the supplied row_key and column_key cannot be found in the table.

"},{"location":"widgets/data_table/#textual.widgets._data_table.DataTable.update_cell_at","title":"update_cell_at method","text":"
def update_cell_at(\nself, coordinate, value, *, update_width=False\n):\n

Update the content inside the cell currently occupying the given coordinate.

Parameters Name Type Description Default coordinate Coordinate

The coordinate to update the cell at.

required value CellType

The new value to place inside the cell.

required update_width bool

Whether to resize the column width to accommodate for the new cell content.

False"},{"location":"widgets/data_table/#textual.widgets.data_table","title":"textual.widgets.data_table","text":""},{"location":"widgets/data_table/#textual.widgets.data_table.CursorType","title":"CursorType module-attribute","text":"
CursorType = Literal['cell', 'row', 'column', 'none']\n

The valid types of cursors for DataTable.cursor_type.

"},{"location":"widgets/data_table/#textual.widgets.data_table.CellDoesNotExist","title":"CellDoesNotExist class","text":"

Bases: Exception

The cell key/index was invalid.

Raised when the coordinates or cell key provided does not exist in the DataTable (e.g. out of bounds index, invalid key)

"},{"location":"widgets/data_table/#textual.widgets.data_table.CellKey","title":"CellKey class","text":"

Bases: NamedTuple

A unique identifier for a cell in the DataTable.

Even if the cell changes visual location (i.e. moves to a different coordinate in the table), this key can still be used to retrieve it, regardless of where it currently is.

"},{"location":"widgets/data_table/#textual.widgets.data_table.Column","title":"Column class","text":"

Metadata for a column in the DataTable.

"},{"location":"widgets/data_table/#textual.widgets._data_table.Column.render_width","title":"render_width property","text":"
render_width: int\n

Width in cells, required to render a column.

"},{"location":"widgets/data_table/#textual.widgets.data_table.ColumnDoesNotExist","title":"ColumnDoesNotExist class","text":"

Bases: Exception

Raised when the column index or column key provided does not exist in the DataTable (e.g. out of bounds index, invalid key)

"},{"location":"widgets/data_table/#textual.widgets.data_table.ColumnKey","title":"ColumnKey class","text":"

Bases: StringKey

Uniquely identifies a column in the DataTable.

Even if the visual location of the column changes due to sorting or other modifications, a key will always refer to the same column.

"},{"location":"widgets/data_table/#textual.widgets.data_table.DuplicateKey","title":"DuplicateKey class","text":"

Bases: Exception

The key supplied already exists.

Raised when the RowKey or ColumnKey provided already refers to an existing row or column in the DataTable. Keys must be unique.

"},{"location":"widgets/data_table/#textual.widgets.data_table.Row","title":"Row class","text":"

Metadata for a row in the DataTable.

"},{"location":"widgets/data_table/#textual.widgets.data_table.RowDoesNotExist","title":"RowDoesNotExist class","text":"

Bases: Exception

Raised when the row index or row key provided does not exist in the DataTable (e.g. out of bounds index, invalid key)

"},{"location":"widgets/data_table/#textual.widgets.data_table.RowKey","title":"RowKey class","text":"

Bases: StringKey

Uniquely identifies a row in the DataTable.

Even if the visual location of the row changes due to sorting or other modifications, a key will always refer to the same row.

"},{"location":"widgets/digits/","title":"Digits","text":"

Added in version 0.33.0

A widget to display numerical values in tall multi-line characters.

The digits 0-9 are supported, in addition to the following characters +, -, ^, :, and \u00d7. Other characters will be displayed in a regular size font.

You can set the text to be displayed in the constructor, or call update() to change the text after the widget has been mounted.

This widget will respect the text-align rule.

  • Focusable
  • Container
"},{"location":"widgets/digits/#example","title":"Example","text":"

The following example displays a few digits of Pi:

Outputdigits.py

DigitApp \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557 \u2551\u257a\u2501\u2513\u00a0\u00a0\u2513\u00a0\u257b\u00a0\u257b\u00a0\u2513\u00a0\u00a0\u250f\u2501\u2578\u250f\u2501\u2513\u257a\u2501\u2513\u00a0\u250f\u2501\u2578\u250f\u2501\u2578\u257a\u2501\u2513\u00a0\u250f\u2501\u2578\u250f\u2501\u2513\u250f\u2501\u2513\u257a\u2501\u2513\u2551 \u2551\u00a0\u2501\u252b\u00a0\u00a0\u2503\u00a0\u2517\u2501\u252b\u00a0\u2503\u00a0\u00a0\u2517\u2501\u2513\u2517\u2501\u252b\u250f\u2501\u251b\u00a0\u2523\u2501\u2513\u2517\u2501\u2513\u00a0\u2501\u252b\u00a0\u2517\u2501\u2513\u2523\u2501\u252b\u2517\u2501\u252b\u00a0\u00a0\u2503\u2551 \u2551\u257a\u2501\u251b.\u257a\u253b\u2578\u00a0\u00a0\u2579\u257a\u253b\u2578,\u257a\u2501\u251b\u257a\u2501\u251b\u2517\u2501\u2578,\u2517\u2501\u251b\u257a\u2501\u251b\u257a\u2501\u251b,\u257a\u2501\u251b\u2517\u2501\u251b\u257a\u2501\u251b\u00a0\u00a0\u2579\u2551 \u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d

from textual.app import App, ComposeResult\nfrom textual.widgets import Digits\nclass DigitApp(App):\nCSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    #pi {\n        border: double green;\n        width: auto;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Digits(\"3.141,592,653,5897\", id=\"pi\")\nif __name__ == \"__main__\":\napp = DigitApp()\napp.run()\n

Here's another example which uses Digits to display the current time:

Outputclock.py

ClockApp \u00a0\u2513\u00a0\u250f\u2501\u2578\u00a0\u00a0\u00a0\u250f\u2501\u2513\u250f\u2501\u2513\u00a0\u00a0\u00a0\u257a\u2501\u2513\u00a0\u2513\u00a0 \u00a0\u2503\u00a0\u2517\u2501\u2513\u00a0:\u00a0\u2503\u00a0\u2503\u2503\u00a0\u2503\u00a0:\u00a0\u00a0\u2501\u252b\u00a0\u2503\u00a0 \u257a\u253b\u2578\u257a\u2501\u251b\u00a0\u00a0\u00a0\u2517\u2501\u251b\u2517\u2501\u251b\u00a0\u00a0\u00a0\u257a\u2501\u251b\u257a\u253b\u2578

from datetime import datetime\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Digits\nclass ClockApp(App):\nCSS = \"\"\"\n    Screen {\n        align: center middle;\n    }\n    #clock {\n        width: auto;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Digits(\"\", id=\"clock\")\ndef on_ready(self) -> None:\nself.update_clock()\nself.set_interval(1, self.update_clock)\ndef update_clock(self) -> None:\nclock = datetime.now().time()\nself.query_one(Digits).update(f\"{clock:%T}\")\nif __name__ == \"__main__\":\napp = ClockApp()\napp.run()\n
"},{"location":"widgets/digits/#reactive-attributes","title":"Reactive Attributes","text":"

This widget has no reactive attributes.

"},{"location":"widgets/digits/#messages","title":"Messages","text":"

This widget posts no messages.

"},{"location":"widgets/digits/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/digits/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/digits/#textual.widgets.Digits","title":"textual.widgets.Digits class","text":"
def __init__(\nself,\nvalue=\"\",\n*,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: Widget

A widget to display numerical values using a 3x3 grid of unicode characters.

name: The name of the widget.\nid: The ID of the widget in the DOM.\nclasses: The CSS classes of the widget.\ndisabled: Whether the widget is disabled or not.\n
"},{"location":"widgets/digits/#textual.widgets._digits.Digits.value","title":"value property","text":"
value: str\n

The current value displayed in the Digits.

"},{"location":"widgets/digits/#textual.widgets._digits.Digits.update","title":"update method","text":"
def update(self, value):\n

Update the Digits with a new value.

Parameters Name Type Description Default value str

New value to display.

required Raises Type Description ValueError

If the value isn't a str.

"},{"location":"widgets/directory_tree/","title":"DirectoryTree","text":"

A tree control to navigate the contents of your filesystem.

  • Focusable
  • Container
"},{"location":"widgets/directory_tree/#example","title":"Example","text":"

The example below creates a simple tree to navigate the current working directory.

from textual.app import App, ComposeResult\nfrom textual.widgets import DirectoryTree\nclass DirectoryTreeApp(App):\ndef compose(self) -> ComposeResult:\nyield DirectoryTree(\"./\")\nif __name__ == \"__main__\":\napp = DirectoryTreeApp()\napp.run()\n
"},{"location":"widgets/directory_tree/#filtering","title":"Filtering","text":"

There may be times where you want to filter what appears in the DirectoryTree. To do this inherit from DirectoryTree and implement your own version of the filter_paths method. It should take an iterable of Python Path objects, and return those that pass the filter. For example, if you wanted to take the above code an filter out all of the \"hidden\" files and directories:

Outputdirectory_tree_filtered.py

DirectoryTreeApp \ud83d\udcc2\u00a0. \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0__pycache__ \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0dist \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0docs \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0examples \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0imgs \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0notes \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0questions \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0reference \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0sandbox \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0site \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0src \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0tests \u2523\u2501\u2501\u00a0\ud83d\udcc1\u00a0tools \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0CHANGELOG.md \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0CODE_OF_CONDUCT.md \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0CONTRIBUTING.md \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0docs.md \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0faq.yml \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0keys.log \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0LICENSE \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0Makefile \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0mkdocs-common.yml \u2523\u2501\u2501\u00a0\ud83d\udcc4\u00a0mkdocs-nav-offline.yml

from pathlib import Path\nfrom typing import Iterable\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import DirectoryTree\nclass FilteredDirectoryTree(DirectoryTree):\ndef filter_paths(self, paths: Iterable[Path]) -> Iterable[Path]:\nreturn [path for path in paths if not path.name.startswith(\".\")]\nclass DirectoryTreeApp(App):\ndef compose(self) -> ComposeResult:\nyield FilteredDirectoryTree(\"./\")\nif __name__ == \"__main__\":\napp = DirectoryTreeApp()\napp.run()\n
"},{"location":"widgets/directory_tree/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description show_root bool True Show the root node. show_guides bool True Show guide lines between levels. guide_depth int 4 Amount of indentation between parent and child."},{"location":"widgets/directory_tree/#messages","title":"Messages","text":"
  • DirectoryTree.FileSelected
"},{"location":"widgets/directory_tree/#bindings","title":"Bindings","text":"

The directory tree widget inherits the bindings from the tree widget.

"},{"location":"widgets/directory_tree/#component-classes","title":"Component Classes","text":"

The directory tree widget provides the following component classes:

Class Description directory-tree--extension Target the extension of a file name. directory-tree--file Target files in the directory structure. directory-tree--folder Target folders in the directory structure. directory-tree--hidden Target hidden items in the directory structure.

See also the component classes for Tree.

"},{"location":"widgets/directory_tree/#see-also","title":"See Also","text":"
  • Tree code reference
"},{"location":"widgets/directory_tree/#textual.widgets.DirectoryTree","title":"textual.widgets.DirectoryTree class","text":"
def __init__(\nself,\npath,\n*,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: Tree[DirEntry]

A Tree widget that presents files and directories.

Parameters Name Type Description Default path str | Path

Path to directory.

required name str | None

The name of the widget, or None for no name.

None id str | None

The ID of the widget in the DOM, or None for no ID.

None classes str | None

A space-separated list of classes, or None for no classes.

None disabled bool

Whether the directory tree is disabled or not.

False"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
COMPONENT_CLASSES: set[str] = {\n\"directory-tree--extension\",\n\"directory-tree--file\",\n\"directory-tree--folder\",\n\"directory-tree--hidden\",\n}\n
Class Description directory-tree--extension Target the extension of a file name. directory-tree--file Target files in the directory structure. directory-tree--folder Target folders in the directory structure. directory-tree--hidden Target hidden items in the directory structure.

See also the component classes for Tree.

"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.PATH","title":"PATH instance-attribute class-attribute","text":"
PATH: Callable[[str | Path], Path] = Path\n

Callable that returns a fresh path object.

"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.path","title":"path instance-attribute class-attribute","text":"
path: var[str | Path] = path\n

The path that is the root of the directory tree.

Note

This can be set to either a str or a pathlib.Path object, but the value will always be a pathlib.Path object.

"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.DirectorySelected","title":"DirectorySelected class","text":"
def __init__(self, node, path):\n

Bases: Message

Posted when a directory is selected.

Can be handled using on_directory_tree_directory_selected in a subclass of DirectoryTree or in a parent widget in the DOM.

Parameters Name Type Description Default node TreeNode[DirEntry]

The tree node for the directory that was selected.

required path Path

The path of the directory that was selected.

required"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.DirectorySelected.control","title":"control property","text":"
control: Tree[DirEntry]\n

The Tree that had a directory selected.

"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.DirectorySelected.node","title":"node instance-attribute","text":"
node: TreeNode[DirEntry] = node\n

The tree node of the directory that was selected.

"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.DirectorySelected.path","title":"path instance-attribute","text":"
path: Path = path\n

The path of the directory that was selected.

"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.FileSelected","title":"FileSelected class","text":"
def __init__(self, node, path):\n

Bases: Message

Posted when a file is selected.

Can be handled using on_directory_tree_file_selected in a subclass of DirectoryTree or in a parent widget in the DOM.

Parameters Name Type Description Default node TreeNode[DirEntry]

The tree node for the file that was selected.

required path Path

The path of the file that was selected.

required"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.FileSelected.control","title":"control property","text":"
control: Tree[DirEntry]\n

The Tree that had a file selected.

"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.FileSelected.node","title":"node instance-attribute","text":"
node: TreeNode[DirEntry] = node\n

The tree node of the file that was selected.

"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.FileSelected.path","title":"path instance-attribute","text":"
path: Path = path\n

The path of the file that was selected.

"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.clear_node","title":"clear_node method","text":"
def clear_node(self, node):\n

Clear all nodes under the given node.

Returns Type Description Self

The Tree instance.

"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.filter_paths","title":"filter_paths method","text":"
def filter_paths(self, paths):\n

Filter the paths before adding them to the tree.

Parameters Name Type Description Default paths Iterable[Path]

The paths to be filtered.

required Returns Type Description Iterable[Path]

The filtered paths.

By default this method returns all of the paths provided. To create a filtered DirectoryTree inherit from it and implement your own version of this method.

"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.process_label","title":"process_label method","text":"
def process_label(self, label):\n

Process a str or Text into a label. Maybe overridden in a subclass to modify how labels are rendered.

Parameters Name Type Description Default label TextType

Label.

required Returns Type Description Text

A Rich Text object.

"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.reload","title":"reload method","text":"
def reload(self):\n

Reload the DirectoryTree contents.

"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.reload_node","title":"reload_node method","text":"
def reload_node(self, node):\n

Reload the given node's contents.

Parameters Name Type Description Default node TreeNode[DirEntry]

The node to reload.

required"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.render_label","title":"render_label method","text":"
def render_label(self, node, base_style, style):\n

Render a label for the given node.

Parameters Name Type Description Default node TreeNode[DirEntry]

A tree node.

required base_style Style

The base style of the widget.

required style Style

The additional style for the label.

required Returns Type Description Text

A Rich Text object containing the label.

"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.reset_node","title":"reset_node method","text":"
def reset_node(self, node, label, data=None):\n

Clear the subtree and reset the given node.

Parameters Name Type Description Default label TextType

The label for the node.

required data DirEntry | None

Optional data for the node.

None Returns Type Description Self

The Tree instance.

"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.validate_path","title":"validate_path method","text":"
def validate_path(self, path):\n

Ensure that the path is of the Path type.

Parameters Name Type Description Default path str | Path

The path to validate.

required Returns Type Description Path

The validated Path value.

Note

The result will always be a Python Path object, regardless of the value given.

"},{"location":"widgets/directory_tree/#textual.widgets._directory_tree.DirectoryTree.watch_path","title":"watch_path method","text":"
def watch_path(self):\n

Watch for changes to the path of the directory tree.

If the path is changed the directory tree will be repopulated using the new value as the root.

"},{"location":"widgets/footer/","title":"Footer","text":"

A simple footer widget which is docked to the bottom of its parent container. Displays available keybindings for the currently focused widget.

  • Focusable
  • Container
"},{"location":"widgets/footer/#example","title":"Example","text":"

The example below shows an app with a single keybinding that contains only a Footer widget. Notice how the Footer automatically displays the keybinding.

Outputfooter.py

FooterApp \u00a0Q\u00a0\u00a0Quit\u00a0the\u00a0app\u00a0\u00a0?\u00a0\u00a0Show\u00a0help\u00a0screen\u00a0\u00a0DELETE\u00a0\u00a0Delete\u00a0the\u00a0thing\u00a0

from textual.app import App, ComposeResult\nfrom textual.binding import Binding\nfrom textual.widgets import Footer\nclass FooterApp(App):\nBINDINGS = [\nBinding(key=\"q\", action=\"quit\", description=\"Quit the app\"),\nBinding(\nkey=\"question_mark\",\naction=\"help\",\ndescription=\"Show help screen\",\nkey_display=\"?\",\n),\nBinding(key=\"delete\", action=\"delete\", description=\"Delete the thing\"),\nBinding(key=\"j\", action=\"down\", description=\"Scroll down\", show=False),\n]\ndef compose(self) -> ComposeResult:\nyield Footer()\nif __name__ == \"__main__\":\napp = FooterApp()\napp.run()\n
"},{"location":"widgets/footer/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description highlight_key str None Stores the currently highlighted key. This is typically the key the cursor is hovered over in the footer."},{"location":"widgets/footer/#messages","title":"Messages","text":"

This widget posts no messages.

"},{"location":"widgets/footer/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/footer/#component-classes","title":"Component Classes","text":"

The footer widget provides the following component classes:

Class Description footer--description Targets the descriptions of the key bindings. footer--highlight Targets the highlighted key binding. footer--highlight-key Targets the key portion of the highlighted key binding. footer--key Targets the key portions of the key bindings."},{"location":"widgets/footer/#additional-notes","title":"Additional Notes","text":"
  • You can prevent keybindings from appearing in the footer by setting the show argument of the Binding to False.
  • You can customize the text that appears for the key itself in the footer using the key_display argument of Binding.
"},{"location":"widgets/footer/#textual.widgets.Footer","title":"textual.widgets.Footer class","text":"
def __init__(self):\n

Bases: Widget

A simple footer widget which docks itself to the bottom of the parent container.

"},{"location":"widgets/footer/#textual.widgets._footer.Footer.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
COMPONENT_CLASSES: set[str] = {\n\"footer--description\",\n\"footer--key\",\n\"footer--highlight\",\n\"footer--highlight-key\",\n}\n
Class Description footer--description Targets the descriptions of the key bindings. footer--highlight Targets the highlighted key binding. footer--highlight-key Targets the key portion of the highlighted key binding. footer--key Targets the key portions of the key bindings."},{"location":"widgets/footer/#textual.widgets._footer.Footer.watch_highlight_key","title":"watch_highlight_key async","text":"
def watch_highlight_key(self):\n

If highlight key changes we need to regenerate the text.

"},{"location":"widgets/header/","title":"Header","text":"

A simple header widget which docks itself to the top of the parent container.

Note

The application title which is shown in the header is taken from the title and sub_title of the application.

  • Focusable
  • Container
"},{"location":"widgets/header/#example","title":"Example","text":"

The example below shows an app with a Header.

Outputheader.py

HeaderApp \u2b58HeaderApp

from textual.app import App, ComposeResult\nfrom textual.widgets import Header\nclass HeaderApp(App):\ndef compose(self) -> ComposeResult:\nyield Header()\nif __name__ == \"__main__\":\napp = HeaderApp()\napp.run()\n

This example shows how to set the text in the Header using App.title and App.sub_title:

Outputheader_app_title.py

HeaderApp \u2b58Header\u00a0Application\u00a0\u2014\u00a0With\u00a0title\u00a0and\u00a0sub-title

from textual.app import App, ComposeResult\nfrom textual.widgets import Header\nclass HeaderApp(App):\ndef compose(self) -> ComposeResult:\nyield Header()\ndef on_mount(self) -> None:\nself.title = \"Header Application\"\nself.sub_title = \"With title and sub-title\"\nif __name__ == \"__main__\":\napp = HeaderApp()\napp.run()\n
"},{"location":"widgets/header/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description tall bool True Whether the Header widget is displayed as tall or not. The tall variant is 3 cells tall by default. The non-tall variant is a single cell tall. This can be toggled by clicking on the header."},{"location":"widgets/header/#messages","title":"Messages","text":"

This widget posts no messages.

"},{"location":"widgets/header/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/header/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/header/#textual.widgets.Header","title":"textual.widgets.Header class","text":"
def __init__(\nself,\nshow_clock=False,\n*,\nname=None,\nid=None,\nclasses=None\n):\n

Bases: Widget

A header widget with icon and clock.

Parameters Name Type Description Default show_clock bool

True if the clock should be shown on the right of the header.

False name str | None

The name of the header widget.

None id str | None

The ID of the header widget in the DOM.

None classes str | None

The CSS classes of the header widget.

None"},{"location":"widgets/header/#textual.widgets._header.Header.screen_sub_title","title":"screen_sub_title property","text":"
screen_sub_title: str\n

The sub-title that this header will display.

This depends on Screen.sub_title and App.sub_title.

"},{"location":"widgets/header/#textual.widgets._header.Header.screen_title","title":"screen_title property","text":"
screen_title: str\n

The title that this header will display.

This depends on Screen.title and App.title.

"},{"location":"widgets/header/#textual.widgets._header.Header.tall","title":"tall instance-attribute class-attribute","text":"
tall: Reactive[bool] = Reactive(False)\n

Set to True for a taller header or False for a single line header.

"},{"location":"widgets/input/","title":"Input","text":"

A single-line text input widget.

  • Focusable
  • Container
"},{"location":"widgets/input/#examples","title":"Examples","text":""},{"location":"widgets/input/#a-simple-example","title":"A Simple Example","text":"

The example below shows how you might create a simple form using two Input widgets.

Outputinput.py

InputApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aDarren\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aLast\u00a0Name\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

from textual.app import App, ComposeResult\nfrom textual.widgets import Input\nclass InputApp(App):\ndef compose(self) -> ComposeResult:\nyield Input(placeholder=\"First Name\")\nyield Input(placeholder=\"Last Name\")\nif __name__ == \"__main__\":\napp = InputApp()\napp.run()\n
"},{"location":"widgets/input/#validating-input","title":"Validating Input","text":"

You can supply one or more validators to the Input widget to validate the value.

All the supplied validators will run when the value changes, the Input is submitted, or focus moves out of the Input. The values \"changed\", \"submitted\", and \"blur\", can be passed as an iterable to the Input parameter validate_on to request that validation occur only on the respective mesages. (See InputValidationOn and Input.validate_on.) For example, the code below creates an Input widget that only gets validated when the value is submitted explicitly:

input = Input(validate_on=[\"submitted\"])\n

Validation is considered to have failed if any of the validators fail.

You can check whether the validation succeeded or failed inside an Input.Changed or Input.Submitted handler by looking at the validation_result attribute on these events.

In the example below, we show how to combine multiple validators and update the UI to tell the user why validation failed. Click the tabs to see the output for validation failures and successes.

input_validation.pyValidation FailureValidation Success
from textual import on\nfrom textual.app import App, ComposeResult\nfrom textual.validation import Function, Number, ValidationResult, Validator\nfrom textual.widgets import Input, Label, Pretty\nclass InputApp(App):\n# (6)!\nCSS = \"\"\"\n    Input.-valid {\n        border: tall $success 60%;\n    }\n    Input.-valid:focus {\n        border: tall $success;\n    }\n    Input {\n        margin: 1 1;\n    }\n    Label {\n        margin: 1 2;\n    }\n    Pretty {\n        margin: 1 2;\n    }\n    \"\"\"\ndef compose(self) -> ComposeResult:\nyield Label(\"Enter an even number between 1 and 100 that is also a palindrome.\")\nyield Input(\nplaceholder=\"Enter a number...\",\nvalidators=[\nNumber(minimum=1, maximum=100),  # (1)!\nFunction(is_even, \"Value is not even.\"),  # (2)!\nPalindrome(),  # (3)!\n],\n)\nyield Pretty([])\n@on(Input.Changed)\ndef show_invalid_reasons(self, event: Input.Changed) -> None:\n# Updating the UI to show the reasons why validation failed\nif not event.validation_result.is_valid:  # (4)!\nself.query_one(Pretty).update(event.validation_result.failure_descriptions)\nelse:\nself.query_one(Pretty).update([])\ndef is_even(value: str) -> bool:\ntry:\nreturn int(value) % 2 == 0\nexcept ValueError:\nreturn False\n# A custom validator\nclass Palindrome(Validator):  # (5)!\ndef validate(self, value: str) -> ValidationResult:\n\"\"\"Check a string is equal to its reverse.\"\"\"\nif self.is_palindrome(value):\nreturn self.success()\nelse:\nreturn self.failure(\"That's not a palindrome :/\")\n@staticmethod\ndef is_palindrome(value: str) -> bool:\nreturn value == value[::-1]\napp = InputApp()\nif __name__ == \"__main__\":\napp.run()\n
  1. Number is a built-in Validator. It checks that the value in the Input is a valid number, and optionally can check that it falls within a range.
  2. Function lets you quickly define custom validation constraints. In this case, we check the value in the Input is even.
  3. Palindrome is a custom Validator defined below.
  4. The Input.Changed event has a validation_result attribute which contains information about the validation that occurred when the value changed.
  5. Here's how we can implement a custom validator which checks if a string is a palindrome. Note how the description passed into self.failure corresponds to the message seen on UI.
  6. Textual offers default styling for the -invalid CSS class (a red border), which is automatically applied to Input when validation fails. We can also provide custom styling for the -valid class, as seen here. In this case, we add a green border around the Input to indicate successful validation.

InputApp Enter\u00a0an\u00a0even\u00a0number\u00a0between\u00a01\u00a0and\u00a0100\u00a0that\u00a0is\u00a0also\u00a0a\u00a0palindrome. \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a-23\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e [ 'Must\u00a0be\u00a0between\u00a01\u00a0and\u00a0100.', 'Value\u00a0is\u00a0not\u00a0even.', \"That's\u00a0not\u00a0a\u00a0palindrome\u00a0:/\" ]

InputApp Enter\u00a0an\u00a0even\u00a0number\u00a0between\u00a01\u00a0and\u00a0100\u00a0that\u00a0is\u00a0also\u00a0a\u00a0palindrome. \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a44\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e []

Textual offers several built-in validators for common requirements, but you can easily roll your own by extending Validator, as seen for Palindrome in the example above.

"},{"location":"widgets/input/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description cursor_blink bool True True if cursor blinking is enabled. value str \"\" The value currently in the text input. cursor_position int 0 The index of the cursor in the value string. placeholder str str The dimmed placeholder text to display when the input is empty. password bool False True if the input should be masked."},{"location":"widgets/input/#messages","title":"Messages","text":"
  • Input.Changed
  • Input.Submitted
"},{"location":"widgets/input/#bindings","title":"Bindings","text":"

The input widget defines the following bindings:

Key(s) Description left Move the cursor left. ctrl+left Move the cursor one word to the left. right Move the cursor right or accept the completion suggestion. ctrl+right Move the cursor one word to the right. backspace Delete the character to the left of the cursor. home,ctrl+a Go to the beginning of the input. end,ctrl+e Go to the end of the input. delete,ctrl+d Delete the character to the right of the cursor. enter Submit the current value of the input. ctrl+w Delete the word to the left of the cursor. ctrl+u Delete everything to the left of the cursor. ctrl+f Delete the word to the right of the cursor. ctrl+k Delete everything to the right of the cursor."},{"location":"widgets/input/#component-classes","title":"Component Classes","text":"

The input widget provides the following component classes:

Class Description input--cursor Target the cursor. input--placeholder Target the placeholder text (when it exists). input--suggestion Target the auto-completion suggestion (when it exists)."},{"location":"widgets/input/#additional-notes","title":"Additional Notes","text":"
  • The spacing around the text content is due to border. To remove it, set border: none; in your CSS.
"},{"location":"widgets/input/#textual.widgets.Input","title":"textual.widgets.Input class","text":"
def __init__(\nself,\nvalue=None,\nplaceholder=\"\",\nhighlighter=None,\npassword=False,\n*,\nsuggester=None,\nvalidators=None,\nvalidate_on=None,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: Widget

A text input widget.

Parameters Name Type Description Default value str | None

An optional default value for the input.

None placeholder str

Optional placeholder text for the input.

'' highlighter Highlighter | None

An optional highlighter for the input.

None password bool

Flag to say if the field should obfuscate its content.

False suggester Suggester | None

Suggester associated with this input instance.

None validators Validator | Iterable[Validator] | None

An iterable of validators that the Input value will be checked against.

None validate_on Iterable[InputValidationOn] | None

Zero or more of the values \"blur\", \"changed\", and \"submitted\", which determine when to do input validation. The default is to do validation for all messages.

None name str | None

Optional name for the input widget.

None id str | None

Optional ID for the widget.

None classes str | None

Optional initial classes for the widget.

None disabled bool

Whether the input is disabled or not.

False"},{"location":"widgets/input/#textual.widgets._input.Input.BINDINGS","title":"BINDINGS class-attribute","text":"
BINDINGS: list[BindingType] = [\nBinding(\n\"left\", \"cursor_left\", \"cursor left\", show=False\n),\nBinding(\n\"ctrl+left\",\n\"cursor_left_word\",\n\"cursor left word\",\nshow=False,\n),\nBinding(\n\"right\", \"cursor_right\", \"cursor right\", show=False\n),\nBinding(\n\"ctrl+right\",\n\"cursor_right_word\",\n\"cursor right word\",\nshow=False,\n),\nBinding(\n\"backspace\",\n\"delete_left\",\n\"delete left\",\nshow=False,\n),\nBinding(\"home,ctrl+a\", \"home\", \"home\", show=False),\nBinding(\"end,ctrl+e\", \"end\", \"end\", show=False),\nBinding(\n\"delete,ctrl+d\",\n\"delete_right\",\n\"delete right\",\nshow=False,\n),\nBinding(\"enter\", \"submit\", \"submit\", show=False),\nBinding(\n\"ctrl+w\",\n\"delete_left_word\",\n\"delete left to start of word\",\nshow=False,\n),\nBinding(\n\"ctrl+u\",\n\"delete_left_all\",\n\"delete all to the left\",\nshow=False,\n),\nBinding(\n\"ctrl+f\",\n\"delete_right_word\",\n\"delete right to start of word\",\nshow=False,\n),\nBinding(\n\"ctrl+k\",\n\"delete_right_all\",\n\"delete all to the right\",\nshow=False,\n),\n]\n
Key(s) Description left Move the cursor left. ctrl+left Move the cursor one word to the left. right Move the cursor right or accept the completion suggestion. ctrl+right Move the cursor one word to the right. backspace Delete the character to the left of the cursor. home,ctrl+a Go to the beginning of the input. end,ctrl+e Go to the end of the input. delete,ctrl+d Delete the character to the right of the cursor. enter Submit the current value of the input. ctrl+w Delete the word to the left of the cursor. ctrl+u Delete everything to the left of the cursor. ctrl+f Delete the word to the right of the cursor. ctrl+k Delete everything to the right of the cursor."},{"location":"widgets/input/#textual.widgets._input.Input.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
COMPONENT_CLASSES: set[str] = {\n\"input--cursor\",\n\"input--placeholder\",\n\"input--suggestion\",\n}\n
Class Description input--cursor Target the cursor. input--placeholder Target the placeholder text (when it exists). input--suggestion Target the auto-completion suggestion (when it exists)."},{"location":"widgets/input/#textual.widgets._input.Input.cursor_width","title":"cursor_width property","text":"
cursor_width: int\n

The width of the input (with extra space for cursor at the end).

"},{"location":"widgets/input/#textual.widgets._input.Input.suggester","title":"suggester instance-attribute","text":"
suggester: Suggester | None = suggester\n

The suggester used to provide completions as the user types.

"},{"location":"widgets/input/#textual.widgets._input.Input.validate_on","title":"validate_on instance-attribute","text":"
validate_on = (\nset(validate_on) & _POSSIBLE_VALIDATE_ON_VALUES\nif validate_on is not None\nelse _POSSIBLE_VALIDATE_ON_VALUES\n)\n

Set with event names to do input validation on.

Validation can only be performed on blur, on input changes and on input submission.

Example

This creates an Input widget that only gets validated when the value is submitted explicitly:

input = Input(validate_on=[\"submitted\"])\n
"},{"location":"widgets/input/#textual.widgets._input.Input.Changed","title":"Changed class","text":"

Bases: Message

Posted when the value changes.

Can be handled using on_input_changed in a subclass of Input or in a parent widget in the DOM.

"},{"location":"widgets/input/#textual.widgets._input.Input.Changed.control","title":"control property","text":"
control: Input\n

Alias for self.input.

"},{"location":"widgets/input/#textual.widgets._input.Input.Changed.input","title":"input instance-attribute","text":"
input: Input\n

The Input widget that was changed.

"},{"location":"widgets/input/#textual.widgets._input.Input.Changed.validation_result","title":"validation_result instance-attribute class-attribute","text":"
validation_result: ValidationResult | None = None\n

The result of validating the value (formed by combining the results from each validator), or None if validation was not performed (for example when no validators are specified in the Inputs init)

"},{"location":"widgets/input/#textual.widgets._input.Input.Changed.value","title":"value instance-attribute","text":"
value: str\n

The value that the input was changed to.

"},{"location":"widgets/input/#textual.widgets._input.Input.Submitted","title":"Submitted class","text":"

Bases: Message

Posted when the enter key is pressed within an Input.

Can be handled using on_input_submitted in a subclass of Input or in a parent widget in the DOM.

"},{"location":"widgets/input/#textual.widgets._input.Input.Submitted.control","title":"control property","text":"
control: Input\n

Alias for self.input.

"},{"location":"widgets/input/#textual.widgets._input.Input.Submitted.input","title":"input instance-attribute","text":"
input: Input\n

The Input widget that is being submitted.

"},{"location":"widgets/input/#textual.widgets._input.Input.Submitted.validation_result","title":"validation_result instance-attribute class-attribute","text":"
validation_result: ValidationResult | None = None\n

The result of validating the value on submission, formed by combining the results for each validator. This value will be None if no validation was performed, which will be the case if no validators are supplied to the corresponding Input widget.

"},{"location":"widgets/input/#textual.widgets._input.Input.Submitted.value","title":"value instance-attribute","text":"
value: str\n

The value of the Input being submitted.

"},{"location":"widgets/input/#textual.widgets._input.Input.action_cursor_left","title":"action_cursor_left method","text":"
def action_cursor_left(self):\n

Move the cursor one position to the left.

"},{"location":"widgets/input/#textual.widgets._input.Input.action_cursor_left_word","title":"action_cursor_left_word method","text":"
def action_cursor_left_word(self):\n

Move the cursor left to the start of a word.

"},{"location":"widgets/input/#textual.widgets._input.Input.action_cursor_right","title":"action_cursor_right method","text":"
def action_cursor_right(self):\n

Accept an auto-completion or move the cursor one position to the right.

"},{"location":"widgets/input/#textual.widgets._input.Input.action_cursor_right_word","title":"action_cursor_right_word method","text":"
def action_cursor_right_word(self):\n

Move the cursor right to the start of a word.

"},{"location":"widgets/input/#textual.widgets._input.Input.action_delete_left","title":"action_delete_left method","text":"
def action_delete_left(self):\n

Delete one character to the left of the current cursor position.

"},{"location":"widgets/input/#textual.widgets._input.Input.action_delete_left_all","title":"action_delete_left_all method","text":"
def action_delete_left_all(self):\n

Delete all characters to the left of the cursor position.

"},{"location":"widgets/input/#textual.widgets._input.Input.action_delete_left_word","title":"action_delete_left_word method","text":"
def action_delete_left_word(self):\n

Delete leftward of the cursor position to the start of a word.

"},{"location":"widgets/input/#textual.widgets._input.Input.action_delete_right","title":"action_delete_right method","text":"
def action_delete_right(self):\n

Delete one character at the current cursor position.

"},{"location":"widgets/input/#textual.widgets._input.Input.action_delete_right_all","title":"action_delete_right_all method","text":"
def action_delete_right_all(self):\n

Delete the current character and all characters to the right of the cursor position.

"},{"location":"widgets/input/#textual.widgets._input.Input.action_delete_right_word","title":"action_delete_right_word method","text":"
def action_delete_right_word(self):\n

Delete the current character and all rightward to the start of the next word.

"},{"location":"widgets/input/#textual.widgets._input.Input.action_end","title":"action_end method","text":"
def action_end(self):\n

Move the cursor to the end of the input.

"},{"location":"widgets/input/#textual.widgets._input.Input.action_home","title":"action_home method","text":"
def action_home(self):\n

Move the cursor to the start of the input.

"},{"location":"widgets/input/#textual.widgets._input.Input.action_submit","title":"action_submit async","text":"
def action_submit(self):\n

Handle a submit action.

Normally triggered by the user pressing Enter. This may also run any validators.

"},{"location":"widgets/input/#textual.widgets._input.Input.insert_text_at_cursor","title":"insert_text_at_cursor method","text":"
def insert_text_at_cursor(self, text):\n

Insert new text at the cursor, move the cursor to the end of the new text.

Parameters Name Type Description Default text str

New text to insert.

required"},{"location":"widgets/input/#textual.widgets._input.Input.validate","title":"validate method","text":"
def validate(self, value):\n

Run all the validators associated with this Input on the supplied value.

Runs all validators, combines the result into one. If any of the validators failed, the combined result will be a failure. If no validators are present, None will be returned. This also sets the -invalid CSS class on the Input if the validation fails, and sets the -valid CSS class on the Input if the validation succeeds.

Returns Type Description ValidationResult | None

A ValidationResult indicating whether all validators succeeded or not. That is, if any validator fails, the result will be an unsuccessful validation.

"},{"location":"widgets/label/","title":"Label","text":"

Added in version 0.5.0

A widget which displays static text, but which can also contain more complex Rich renderables.

  • Focusable
  • Container
"},{"location":"widgets/label/#example","title":"Example","text":"

The example below shows how you can use a Label widget to display some text.

Outputlabel.py

LabelApp Hello,\u00a0world!

from textual.app import App, ComposeResult\nfrom textual.widgets import Label\nclass LabelApp(App):\ndef compose(self) -> ComposeResult:\nyield Label(\"Hello, world!\")\nif __name__ == \"__main__\":\napp = LabelApp()\napp.run()\n
"},{"location":"widgets/label/#reactive-attributes","title":"Reactive Attributes","text":"

This widget has no reactive attributes.

"},{"location":"widgets/label/#messages","title":"Messages","text":"

This widget posts no messages.

"},{"location":"widgets/label/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/label/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/label/#textual.widgets.Label","title":"textual.widgets.Label class","text":"

Bases: Static

A simple label widget for displaying text-oriented renderables.

"},{"location":"widgets/list_item/","title":"ListItem","text":"

Added in version 0.6.0

ListItem is the type of the elements in a ListView.

  • Focusable
  • Container
"},{"location":"widgets/list_item/#example","title":"Example","text":"

The example below shows an app with a simple ListView, consisting of multiple ListItems. The arrow keys can be used to navigate the list.

Outputlist_view.py

ListViewExample One Two Three

from textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Label, ListItem, ListView\nclass ListViewExample(App):\nCSS_PATH = \"list_view.tcss\"\ndef compose(self) -> ComposeResult:\nyield ListView(\nListItem(Label(\"One\")),\nListItem(Label(\"Two\")),\nListItem(Label(\"Three\")),\n)\nyield Footer()\nif __name__ == \"__main__\":\napp = ListViewExample()\napp.run()\n
"},{"location":"widgets/list_item/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description highlighted bool False True if this ListItem is highlighted"},{"location":"widgets/list_item/#messages","title":"Messages","text":"

This widget posts no messages.

"},{"location":"widgets/list_item/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/list_item/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/list_item/#textual.widgets.ListItem","title":"textual.widgets.ListItem class","text":"

Bases: Widget

A widget that is an item within a ListView.

A ListItem is designed for use within a ListView, please see ListView's documentation for more details on use.

"},{"location":"widgets/list_item/#textual.widgets._list_item.ListItem.highlighted","title":"highlighted instance-attribute class-attribute","text":"
highlighted = reactive(False)\n

Is this item highlighted?

"},{"location":"widgets/list_view/","title":"ListView","text":"

Added in version 0.6.0

Displays a vertical list of ListItems which can be highlighted and selected. Supports keyboard navigation.

  • Focusable
  • Container
"},{"location":"widgets/list_view/#example","title":"Example","text":"

The example below shows an app with a simple ListView.

Outputlist_view.pylist_view.tcss

ListViewExample One Two Three

from textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Label, ListItem, ListView\nclass ListViewExample(App):\nCSS_PATH = \"list_view.tcss\"\ndef compose(self) -> ComposeResult:\nyield ListView(\nListItem(Label(\"One\")),\nListItem(Label(\"Two\")),\nListItem(Label(\"Three\")),\n)\nyield Footer()\nif __name__ == \"__main__\":\napp = ListViewExample()\napp.run()\n
Screen {\nalign: center middle;\n}\nListView {\nwidth: 30;\nheight: auto;\nmargin: 2 2;\n}\nLabel {\npadding: 1 2;\n}\n
"},{"location":"widgets/list_view/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description index int 0 The currently highlighted index."},{"location":"widgets/list_view/#messages","title":"Messages","text":"
  • ListView.Highlighted
  • ListView.Selected
"},{"location":"widgets/list_view/#bindings","title":"Bindings","text":"

The list view widget defines the following bindings:

Key(s) Description enter Select the current item. up Move the cursor up. down Move the cursor down."},{"location":"widgets/list_view/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/list_view/#textual.widgets.ListView","title":"textual.widgets.ListView class","text":"
def __init__(\nself,\n*children,\ninitial_index=0,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: VerticalScroll

A vertical list view widget.

Displays a vertical list of ListItems which can be highlighted and selected using the mouse or keyboard.

Attributes Name Type Description index

The index in the list that's currently highlighted.

Parameters Name Type Description Default *children ListItem

The ListItems to display in the list.

() initial_index int | None

The index that should be highlighted when the list is first mounted.

0 name str | None

The name of the widget.

None id str | None

The unique ID of the widget used in CSS/query selection.

None classes str | None

The CSS classes of the widget.

None disabled bool

Whether the ListView is disabled or not.

False"},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.BINDINGS","title":"BINDINGS class-attribute","text":"
BINDINGS: list[BindingType] = [\nBinding(\"enter\", \"select_cursor\", \"Select\", show=False),\nBinding(\"up\", \"cursor_up\", \"Cursor Up\", show=False),\nBinding(\n\"down\", \"cursor_down\", \"Cursor Down\", show=False\n),\n]\n
Key(s) Description enter Select the current item. up Move the cursor up. down Move the cursor down."},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.highlighted_child","title":"highlighted_child property","text":"
highlighted_child: ListItem | None\n

The currently highlighted ListItem, or None if nothing is highlighted.

"},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Highlighted","title":"Highlighted class","text":"
def __init__(self, list_view, item):\n

Bases: Message

Posted when the highlighted item changes.

Highlighted item is controlled using up/down keys. Can be handled using on_list_view_highlighted in a subclass of ListView or in a parent widget in the DOM.

"},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Highlighted.ALLOW_SELECTOR_MATCH","title":"ALLOW_SELECTOR_MATCH instance-attribute class-attribute","text":"
ALLOW_SELECTOR_MATCH = {'item'}\n

Additional message attributes that can be used with the on decorator.

"},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Highlighted.control","title":"control property","text":"
control: ListView\n

The view that contains the item highlighted.

This is an alias for Highlighted.list_view and is used by the on decorator.

"},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Highlighted.item","title":"item instance-attribute","text":"
item: ListItem | None = item\n

The highlighted item, if there is one highlighted.

"},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Highlighted.list_view","title":"list_view instance-attribute","text":"
list_view: ListView = list_view\n

The view that contains the item highlighted.

"},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Selected","title":"Selected class","text":"
def __init__(self, list_view, item):\n

Bases: Message

Posted when a list item is selected, e.g. when you press the enter key on it.

Can be handled using on_list_view_selected in a subclass of ListView or in a parent widget in the DOM.

"},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Selected.ALLOW_SELECTOR_MATCH","title":"ALLOW_SELECTOR_MATCH instance-attribute class-attribute","text":"
ALLOW_SELECTOR_MATCH = {'item'}\n

Additional message attributes that can be used with the on decorator.

"},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Selected.control","title":"control property","text":"
control: ListView\n

The view that contains the item selected.

This is an alias for Selected.list_view and is used by the on decorator.

"},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Selected.item","title":"item instance-attribute","text":"
item: ListItem = item\n

The selected item.

"},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.Selected.list_view","title":"list_view instance-attribute","text":"
list_view: ListView = list_view\n

The view that contains the item selected.

"},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.action_cursor_down","title":"action_cursor_down method","text":"
def action_cursor_down(self):\n

Highlight the next item in the list.

"},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.action_cursor_up","title":"action_cursor_up method","text":"
def action_cursor_up(self):\n

Highlight the previous item in the list.

"},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.action_select_cursor","title":"action_select_cursor method","text":"
def action_select_cursor(self):\n

Select the current item in the list.

"},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.append","title":"append method","text":"
def append(self, item):\n

Append a new ListItem to the end of the ListView.

Parameters Name Type Description Default item ListItem

The ListItem to append.

required Returns Type Description AwaitMount

An awaitable that yields control to the event loop until the DOM has been updated with the new child item.

"},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.clear","title":"clear method","text":"
def clear(self):\n

Clear all items from the ListView.

Returns Type Description AwaitRemove

An awaitable that yields control to the event loop until the DOM has been updated to reflect all children being removed.

"},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.extend","title":"extend method","text":"
def extend(self, items):\n

Append multiple new ListItems to the end of the ListView.

Parameters Name Type Description Default items Iterable[ListItem]

The ListItems to append.

required Returns Type Description AwaitMount

An awaitable that yields control to the event loop until the DOM has been updated with the new child items.

"},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.validate_index","title":"validate_index method","text":"
def validate_index(self, index):\n

Clamp the index to the valid range, or set to None if there's nothing to highlight.

Parameters Name Type Description Default index int | None

The index to clamp.

required Returns Type Description int | None

The clamped index.

"},{"location":"widgets/list_view/#textual.widgets._list_view.ListView.watch_index","title":"watch_index method","text":"
def watch_index(self, old_index, new_index):\n

Updates the highlighting when the index changes.

"},{"location":"widgets/loading_indicator/","title":"LoadingIndicator","text":"

Added in version 0.15.0

Displays pulsating dots to indicate when data is being loaded.

  • Focusable
  • Container
"},{"location":"widgets/loading_indicator/#example","title":"Example","text":"

Simple usage example:

Outputloading_indicator.py

LoadingApp \u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf\u00a0\u25cf

from textual.app import App, ComposeResult\nfrom textual.widgets import LoadingIndicator\nclass LoadingApp(App):\ndef compose(self) -> ComposeResult:\nyield LoadingIndicator()\nif __name__ == \"__main__\":\napp = LoadingApp()\napp.run()\n
"},{"location":"widgets/loading_indicator/#changing-indicator-color","title":"Changing Indicator Color","text":"

You can set the color of the loading indicator by setting its color style.

Here's how you would do that with CSS:

LoadingIndicator {\ncolor: red;\n}\n
"},{"location":"widgets/loading_indicator/#reactive-attributes","title":"Reactive Attributes","text":"

This widget has no reactive attributes.

"},{"location":"widgets/loading_indicator/#messages","title":"Messages","text":"

This widget posts no messages.

"},{"location":"widgets/loading_indicator/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/loading_indicator/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/loading_indicator/#textual.widgets.LoadingIndicator","title":"textual.widgets.LoadingIndicator class","text":"

Bases: Widget

Display an animated loading indicator.

"},{"location":"widgets/log/","title":"Log","text":"

Added in version 0.32.0

A Log widget displays lines of text which may be appended to in realtime.

Call Log.write_line to write a line at a time, or Log.write_lines to write multiple lines at once. Call Log.clear to clear the Log widget.

Tip

See also RichLog which can write more than just text, and supports a number of advanced features.

  • Focusable
  • Container
"},{"location":"widgets/log/#example","title":"Example","text":"

The example below shows how to write text to a Log widget:

Outputlog.py

LogApp And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain. I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain. I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.\u2584\u2584 I\u00a0must\u00a0not\u00a0fear. Fear\u00a0is\u00a0the\u00a0mind-killer. Fear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0obliteration. I\u00a0will\u00a0face\u00a0my\u00a0fear. I\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me. And\u00a0when\u00a0it\u00a0has\u00a0gone\u00a0past,\u00a0I\u00a0will\u00a0turn\u00a0the\u00a0inner\u00a0eye\u00a0to\u00a0see\u00a0its\u00a0path. Where\u00a0the\u00a0fear\u00a0has\u00a0gone\u00a0there\u00a0will\u00a0be\u00a0nothing.\u00a0Only\u00a0I\u00a0will\u00a0remain.

from textual.app import App, ComposeResult\nfrom textual.widgets import Log\nTEXT = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\nAnd when it has gone past, I will turn the inner eye to see its path.\nWhere the fear has gone there will be nothing. Only I will remain.\"\"\"\nclass LogApp(App):\n\"\"\"An app with a simple log.\"\"\"\ndef compose(self) -> ComposeResult:\nyield Log()\ndef on_ready(self) -> None:\nlog = self.query_one(Log)\nlog.write_line(\"Hello, World!\")\nfor _ in range(10):\nlog.write_line(TEXT)\nif __name__ == \"__main__\":\napp = LogApp()\napp.run()\n
"},{"location":"widgets/log/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description max_lines int None Maximum number of lines in the log or None for no maximum. auto_scroll bool False Scroll to end of log when new lines are added."},{"location":"widgets/log/#messages","title":"Messages","text":"

This widget posts no messages.

"},{"location":"widgets/log/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/log/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/log/#textual.widgets.Log","title":"textual.widgets.Log class","text":"
def __init__(\nself,\nhighlight=False,\nmax_lines=None,\nauto_scroll=True,\nname=None,\nid=None,\nclasses=None,\ndisabled=False,\n):\n

Bases: ScrollView

A widget to log text.

Parameters Name Type Description Default highlight bool

Enable highlighting.

False max_lines int | None

Maximum number of lines to display.

None auto_scroll bool

Scroll to end on new lines.

True name str | None

The name of the text log.

None id str | None

The ID of the text log in the DOM.

None classes str | None

The CSS classes of the text log.

None disabled bool

Whether the text log is disabled or not.

False"},{"location":"widgets/log/#textual.widgets._log.Log.auto_scroll","title":"auto_scroll instance-attribute class-attribute","text":"
auto_scroll: var[bool] = auto_scroll\n

Automatically scroll to new lines.

"},{"location":"widgets/log/#textual.widgets._log.Log.highlight","title":"highlight instance-attribute","text":"
highlight = highlight\n

Enable highlighting.

"},{"location":"widgets/log/#textual.widgets._log.Log.highlighter","title":"highlighter instance-attribute","text":"
highlighter = ReprHighlighter()\n

The Rich Highlighter object to use, if highlight=True

"},{"location":"widgets/log/#textual.widgets._log.Log.line_count","title":"line_count property","text":"
line_count: int\n

Number of lines of content.

"},{"location":"widgets/log/#textual.widgets._log.Log.lines","title":"lines property","text":"
lines: Sequence[str]\n

The raw lines in the TextLog.

Note that this attribute is read only. Changing the lines will not update the Log's contents.

"},{"location":"widgets/log/#textual.widgets._log.Log.max_lines","title":"max_lines instance-attribute class-attribute","text":"
max_lines: var[int | None] = max_lines\n

Maximum number of lines to show

"},{"location":"widgets/log/#textual.widgets._log.Log.clear","title":"clear method","text":"
def clear(self):\n

Clear the Log.

Returns Type Description Self

The Log instance.

"},{"location":"widgets/log/#textual.widgets._log.Log.notify_style_update","title":"notify_style_update method","text":"
def notify_style_update(self):\n

Called by Textual when styles update.

"},{"location":"widgets/log/#textual.widgets._log.Log.refresh_lines","title":"refresh_lines method","text":"
def refresh_lines(self, y_start, line_count=1):\n

Refresh one or more lines.

Parameters Name Type Description Default y_start int

First line to refresh.

required line_count int

Total number of lines to refresh.

1"},{"location":"widgets/log/#textual.widgets._log.Log.write","title":"write method","text":"
def write(self, data, scroll_end=None):\n

Write to the log.

Parameters Name Type Description Default data str

Data to write.

required scroll_end bool | None

Scroll to the end after writing, or None to use self.auto_scroll.

None Returns Type Description Self

The Log instance.

"},{"location":"widgets/log/#textual.widgets._log.Log.write_line","title":"write_line method","text":"
def write_line(self, line):\n

Write content on a new line.

Parameters Name Type Description Default line str

String to write to the log.

required Returns Type Description Self

The Log instance.

"},{"location":"widgets/log/#textual.widgets._log.Log.write_lines","title":"write_lines method","text":"
def write_lines(self, lines, scroll_end=None):\n

Write an iterable of lines.

Parameters Name Type Description Default lines Iterable[str]

An iterable of strings to write.

required scroll_end bool | None

Scroll to the end after writing, or None to use self.auto_scroll.

None Returns Type Description Self

The Log instance.

"},{"location":"widgets/markdown/","title":"Markdown","text":"

Added in version 0.11.0

A widget to display a Markdown document.

  • Focusable
  • Container

Tip

See MarkdownViewer for a widget that adds additional features such as a Table of Contents.

"},{"location":"widgets/markdown/#example","title":"Example","text":"

The following example displays Markdown from a string.

Outputmarkdown.py

MarkdownExampleApp \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eMarkdown\u00a0Document\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 This\u00a0is\u00a0an\u00a0example\u00a0of\u00a0Textual's\u00a0Markdown\u00a0widget. \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Features\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Markdown\u00a0syntax\u00a0and\u00a0extensions\u00a0are\u00a0supported. \u25cf\u00a0Typography\u00a0emphasis,\u00a0strong,\u00a0inline\u00a0code\u00a0etc. \u25cf\u00a0Headers \u25cf\u00a0Lists\u00a0(bullet\u00a0and\u00a0ordered) \u25cf\u00a0Syntax\u00a0highlighted\u00a0code\u00a0blocks \u25cf\u00a0Tables!

from textual.app import App, ComposeResult\nfrom textual.widgets import Markdown\nEXAMPLE_MARKDOWN = \"\"\"\\\n# Markdown Document\nThis is an example of Textual's `Markdown` widget.\n## Features\nMarkdown syntax and extensions are supported.\n- Typography *emphasis*, **strong**, `inline code` etc.\n- Headers\n- Lists (bullet and ordered)\n- Syntax highlighted code blocks\n- Tables!\n\"\"\"\nclass MarkdownExampleApp(App):\ndef compose(self) -> ComposeResult:\nyield Markdown(EXAMPLE_MARKDOWN)\nif __name__ == \"__main__\":\napp = MarkdownExampleApp()\napp.run()\n
"},{"location":"widgets/markdown/#reactive-attributes","title":"Reactive Attributes","text":"

This widget has no reactive attributes.

"},{"location":"widgets/markdown/#messages","title":"Messages","text":"
  • Markdown.TableOfContentsUpdated
  • Markdown.TableOfContentsSelected
  • Markdown.LinkClicked
"},{"location":"widgets/markdown/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/markdown/#component-classes","title":"Component Classes","text":"

The markdown widget provides the following component classes:

These component classes target standard inline markdown styles. Changing these will potentially break the standard markdown formatting.

Class Description code_inline Target text that is styled as inline code. em Target text that is emphasized inline. s Target text that is styled inline with strykethrough. strong Target text that is styled inline with strong."},{"location":"widgets/markdown/#see-also","title":"See Also","text":"
  • MarkdownViewer code reference
"},{"location":"widgets/markdown/#textual.widgets.Markdown","title":"textual.widgets.Markdown class","text":"
def __init__(\nself,\nmarkdown=None,\n*,\nname=None,\nid=None,\nclasses=None,\nparser_factory=None\n):\n

Bases: Widget

Parameters Name Type Description Default markdown str | None

String containing Markdown or None to leave blank for now.

None name str | None

The name of the widget.

None id str | None

The ID of the widget in the DOM.

None classes str | None

The CSS classes of the widget.

None parser_factory Callable[[], MarkdownIt] | None

A factory function to return a configured MarkdownIt instance. If None, a \"gfm-like\" parser is used.

None"},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.COMPONENT_CLASSES","title":"COMPONENT_CLASSES instance-attribute class-attribute","text":"
COMPONENT_CLASSES = {'em', 'strong', 's', 'code_inline'}\n

These component classes target standard inline markdown styles. Changing these will potentially break the standard markdown formatting.

Class Description code_inline Target text that is styled as inline code. em Target text that is emphasized inline. s Target text that is styled inline with strykethrough. strong Target text that is styled inline with strong."},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.LinkClicked","title":"LinkClicked class","text":"
def __init__(self, markdown, href):\n

Bases: Message

A link in the document was clicked.

"},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.LinkClicked.control","title":"control property","text":"
control: Markdown\n

The Markdown widget containing the link clicked.

This is an alias for LinkClicked.markdown and is used by the on decorator.

"},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.LinkClicked.href","title":"href instance-attribute","text":"
href: str = href\n

The link that was selected.

"},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.LinkClicked.markdown","title":"markdown instance-attribute","text":"
markdown: Markdown = markdown\n

The Markdown widget containing the link clicked.

"},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.TableOfContentsSelected","title":"TableOfContentsSelected class","text":"
def __init__(self, markdown, block_id):\n

Bases: Message

An item in the TOC was selected.

"},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.TableOfContentsSelected.block_id","title":"block_id instance-attribute","text":"
block_id: str = block_id\n

ID of the block that was selected.

"},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.TableOfContentsSelected.control","title":"control property","text":"
control: Markdown\n

The Markdown widget where the selected item is.

This is an alias for TableOfContentsSelected.markdown and is used by the on decorator.

"},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.TableOfContentsSelected.markdown","title":"markdown instance-attribute","text":"
markdown: Markdown = markdown\n

The Markdown widget where the selected item is.

"},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.TableOfContentsUpdated","title":"TableOfContentsUpdated class","text":"
def __init__(self, markdown, table_of_contents):\n

Bases: Message

The table of contents was updated.

"},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.TableOfContentsUpdated.control","title":"control property","text":"
control: Markdown\n

The Markdown widget associated with the table of contents.

This is an alias for TableOfContentsUpdated.markdown and is used by the on decorator.

"},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.TableOfContentsUpdated.markdown","title":"markdown instance-attribute","text":"
markdown: Markdown = markdown\n

The Markdown widget associated with the table of contents.

"},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.TableOfContentsUpdated.table_of_contents","title":"table_of_contents instance-attribute","text":"
table_of_contents: TableOfContentsType = table_of_contents\n

Table of contents.

"},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.goto_anchor","title":"goto_anchor method","text":"
def goto_anchor(self, anchor):\n

Try and find the given anchor in the current document.

Parameters Name Type Description Default anchor str

The anchor to try and find.

required Note

The anchor is found by looking at all of the headings in the document and finding the first one whose slug matches the anchor.

Note that the slugging method used is similar to that found on GitHub.

Returns Type Description bool

True when the anchor was found in the current document, False otherwise.

"},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.load","title":"load async","text":"
def load(self, path):\n

Load a new Markdown document.

Parameters Name Type Description Default path Path

Path to the document.

required Raises Type Description OSError

If there was some form of error loading the document.

Note

The exceptions that can be raised by this method are all of those that can be raised by calling Path.read_text.

"},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.sanitize_location","title":"sanitize_location staticmethod","text":"
def sanitize_location(location):\n

Given a location, break out the path and any anchor.

Parameters Name Type Description Default location str

The location to sanitize.

required Returns Type Description Path

A tuple of the path to the location cleaned of any anchor, plus

str

the anchor (or an empty string if none was found).

"},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.unhandled_token","title":"unhandled_token method","text":"
def unhandled_token(self, token):\n

Process an unhandled token.

Parameters Name Type Description Default token Token

The token to handle.

required Returns Type Description MarkdownBlock | None

Either a widget to be added to the output, or None.

"},{"location":"widgets/markdown/#textual.widgets._markdown.Markdown.update","title":"update method","text":"
def update(self, markdown):\n

Update the document with new Markdown.

Parameters Name Type Description Default markdown str

A string containing Markdown.

required Returns Type Description AwaitMount

An optionally awaitable object. Await this to ensure that all children have been mounted.

"},{"location":"widgets/markdown_viewer/","title":"MarkdownViewer","text":"

Added in version 0.11.0

A Widget to display Markdown content with an optional Table of Contents.

  • Focusable
  • Container

Note

This Widget adds browser-like functionality on top of the Markdown widget.

"},{"location":"widgets/markdown_viewer/#example","title":"Example","text":"

The following example displays Markdown from a string and a Table of Contents.

Outputmarkdown.py

MarkdownExampleApp \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u25bc\u00a0\u2160\u00a0Markdown\u00a0Viewer\u258a\u258e\u258a \u251c\u2500\u2500\u00a0\u2161\u00a0Features\u258a\u258eMarkdown\u00a0Viewer\u258a \u251c\u2500\u2500\u00a0\u2161\u00a0Tables\u258a\u258e\u258a \u2514\u2500\u2500\u00a0\u2161\u00a0Code\u00a0Blocks\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258aThis\u00a0is\u00a0an\u00a0example\u00a0of\u00a0Textual's\u00a0MarkdownViewer\u00a0widget. \u258a \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258a\u258e\u258a \u258a\u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Features\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258e\u258a \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258aMarkdown\u00a0syntax\u00a0and\u00a0extensions\u00a0are\u00a0supported. \u258a \u258a\u25cf\u00a0Typography\u00a0emphasis,\u00a0strong,\u00a0inline\u00a0code\u00a0etc. \u258a\u25cf\u00a0Headers \u258a\u25cf\u00a0Lists\u00a0(bullet\u00a0and\u00a0ordered) \u258a\u25cf\u00a0Syntax\u00a0highlighted\u00a0code\u00a0blocks \u258a\u25cf\u00a0Tables! \u258a \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258a\u258e\u258a \u258a\u258e\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Tables\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258e\u258a \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258aTables\u00a0are\u00a0displayed\u00a0in\u00a0a\u00a0DataTable\u00a0widget. \u258a \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258a\u258e\u258a \u258a\u258eName\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0TypeDefaultDescription\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258e\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u00a0\u258a \u258a\u258eshow_headerboolTrueShow\u00a0the\u00a0table\u00a0header\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258efixed_rowsint0Number\u00a0of\u00a0fixed\u00a0rows\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258efixed_columnsint0Number\u00a0of\u00a0fixed\u00a0columns\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258ezebra_stripesboolFalseDisplay\u00a0alternating\u00a0colors\u00a0on\u00a0rows\u258a \u258a\u258eheader_heightint1Height\u00a0of\u00a0header\u00a0row\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258eshow_cursorboolTrueShow\u00a0a\u00a0cell\u00a0cursor\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a \u258a\u258e\u258a \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258a \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258a\u258e\u258a

from textual.app import App, ComposeResult\nfrom textual.widgets import MarkdownViewer\nEXAMPLE_MARKDOWN = \"\"\"\\\n# Markdown Viewer\nThis is an example of Textual's `MarkdownViewer` widget.\n## Features\nMarkdown syntax and extensions are supported.\n- Typography *emphasis*, **strong**, `inline code` etc.\n- Headers\n- Lists (bullet and ordered)\n- Syntax highlighted code blocks\n- Tables!\n## Tables\nTables are displayed in a DataTable widget.\n| Name            | Type   | Default | Description                        |\n| --------------- | ------ | ------- | ---------------------------------- |\n| `show_header`   | `bool` | `True`  | Show the table header              |\n| `fixed_rows`    | `int`  | `0`     | Number of fixed rows               |\n| `fixed_columns` | `int`  | `0`     | Number of fixed columns            |\n| `zebra_stripes` | `bool` | `False` | Display alternating colors on rows |\n| `header_height` | `int`  | `1`     | Height of header row               |\n| `show_cursor`   | `bool` | `True`  | Show a cell cursor                 |\n## Code Blocks\nCode blocks are syntax highlighted, with guidelines.\n```python\nclass ListViewExample(App):\n    def compose(self) -> ComposeResult:\n        yield ListView(\n            ListItem(Label(\"One\")),\n            ListItem(Label(\"Two\")),\n            ListItem(Label(\"Three\")),\n        )\n        yield Footer()\n```\n\"\"\"\nclass MarkdownExampleApp(App):\ndef compose(self) -> ComposeResult:\nyield MarkdownViewer(EXAMPLE_MARKDOWN, show_table_of_contents=True)\nif __name__ == \"__main__\":\napp = MarkdownExampleApp()\napp.run()\n
"},{"location":"widgets/markdown_viewer/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description show_table_of_contents bool True Wether a Table of Contents should be displayed with the Markdown."},{"location":"widgets/markdown_viewer/#messages","title":"Messages","text":"

This widget posts no messages.

"},{"location":"widgets/markdown_viewer/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/markdown_viewer/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/markdown_viewer/#see-also","title":"See Also","text":"
  • Markdown code reference
"},{"location":"widgets/markdown_viewer/#textual.widgets.MarkdownViewer","title":"textual.widgets.MarkdownViewer class","text":"
def __init__(\nself,\nmarkdown=None,\n*,\nshow_table_of_contents=True,\nname=None,\nid=None,\nclasses=None,\nparser_factory=None\n):\n

Bases: VerticalScroll

A Markdown viewer widget.

Parameters Name Type Description Default markdown str | None

String containing Markdown, or None to leave blank.

None show_table_of_contents bool

Show a table of contents in a sidebar.

True name str | None

The name of the widget.

None id str | None

The ID of the widget in the DOM.

None classes str | None

The CSS classes of the widget.

None parser_factory Callable[[], MarkdownIt] | None

A factory function to return a configured MarkdownIt instance. If None, a \"gfm-like\" parser is used.

None"},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.MarkdownViewer.document","title":"document property","text":"
document: Markdown\n

The Markdown document object.

"},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.MarkdownViewer.table_of_contents","title":"table_of_contents property","text":"
table_of_contents: MarkdownTableOfContents\n

The table of contents widget

"},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.MarkdownViewer.back","title":"back async","text":"
def back(self):\n

Go back one level in the history.

"},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.MarkdownViewer.forward","title":"forward async","text":"
def forward(self):\n

Go forward one level in the history.

"},{"location":"widgets/markdown_viewer/#textual.widgets._markdown.MarkdownViewer.go","title":"go async","text":"
def go(self, location):\n

Navigate to a new document path.

"},{"location":"widgets/option_list/","title":"OptionList","text":"

Added in version 0.17.0

A widget for showing a vertical list of Rich renderable options.

  • Focusable
  • Container
"},{"location":"widgets/option_list/#examples","title":"Examples","text":""},{"location":"widgets/option_list/#options-as-simple-strings","title":"Options as simple strings","text":"

An OptionList can be constructed with a simple collection of string options:

Outputoption_list_strings.pyoption_list.tcss

OptionListApp \u2b58OptionListApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aAerilon\u258e \u258aAquaria\u258e \u258aCanceron\u258e \u258aCaprica\u258e \u258aGemenon\u258e \u258aLeonis\u258e \u258aLibran\u258e \u258aPicon\u258e \u258aSagittaron\u258e \u258aScorpia\u258e \u258aTauron\u258e \u258aVirgon\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

from textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Header, OptionList\nclass OptionListApp(App[None]):\nCSS_PATH = \"option_list.tcss\"\ndef compose(self) -> ComposeResult:\nyield Header()\nyield OptionList(\n\"Aerilon\",\n\"Aquaria\",\n\"Canceron\",\n\"Caprica\",\n\"Gemenon\",\n\"Leonis\",\n\"Libran\",\n\"Picon\",\n\"Sagittaron\",\n\"Scorpia\",\n\"Tauron\",\n\"Virgon\",\n)\nyield Footer()\nif __name__ == \"__main__\":\nOptionListApp().run()\n
Screen {\nalign: center middle;\n}\nOptionList {\nwidth: 70%;\nheight: 80%;\n}\n
"},{"location":"widgets/option_list/#options-as-option-instances","title":"Options as Option instances","text":"

For finer control over the options, the Option class can be used; this allows for setting IDs, setting initial disabled state, etc. The Separator class can be used to add separator lines between options.

Outputoption_list_options.pyoption_list.tcss

OptionListApp \u2b58OptionListApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aAerilon\u258e \u258aAquaria\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aCanceron\u258e \u258aCaprica\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aGemenon\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aLeonis\u258e \u258aLibran\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aPicon\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258aSagittaron\u2584\u2584\u258e \u258aScorpia\u258e \u258a\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

from textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Header, OptionList\nfrom textual.widgets.option_list import Option, Separator\nclass OptionListApp(App[None]):\nCSS_PATH = \"option_list.tcss\"\ndef compose(self) -> ComposeResult:\nyield Header()\nyield OptionList(\nOption(\"Aerilon\", id=\"aer\"),\nOption(\"Aquaria\", id=\"aqu\"),\nSeparator(),\nOption(\"Canceron\", id=\"can\"),\nOption(\"Caprica\", id=\"cap\", disabled=True),\nSeparator(),\nOption(\"Gemenon\", id=\"gem\"),\nSeparator(),\nOption(\"Leonis\", id=\"leo\"),\nOption(\"Libran\", id=\"lib\"),\nSeparator(),\nOption(\"Picon\", id=\"pic\"),\nSeparator(),\nOption(\"Sagittaron\", id=\"sag\"),\nOption(\"Scorpia\", id=\"sco\"),\nSeparator(),\nOption(\"Tauron\", id=\"tau\"),\nSeparator(),\nOption(\"Virgon\", id=\"vir\"),\n)\nyield Footer()\nif __name__ == \"__main__\":\nOptionListApp().run()\n
Screen {\nalign: center middle;\n}\nOptionList {\nwidth: 70%;\nheight: 80%;\n}\n
"},{"location":"widgets/option_list/#options-as-rich-renderables","title":"Options as Rich renderables","text":"

Because the prompts for the options can be Rich renderables, this means they can be any height you wish. As an example, here is an option list comprised of Rich tables:

Outputoption_list_tables.pyoption_list.tcss

OptionListApp \u2b58OptionListApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Data\u00a0for\u00a0Aerilon\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258e \u258a\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u258e \u258a\u2503Patron\u00a0God\u00a0\u00a0\u00a0\u2503Population\u00a0\u00a0\u00a0\u00a0\u2503Capital\u00a0City\u00a0\u00a0\u2503\u258e \u258a\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\u2583\u2583\u258e \u258a\u2502Demeter\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u25021.2\u00a0Billion\u00a0\u00a0\u00a0\u2502Gaoth\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u258e \u258a\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u258e \u258a\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Data\u00a0for\u00a0Aquaria\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258e \u258a\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u258e \u258a\u2503Patron\u00a0God\u00a0\u00a0\u00a0\u2503Population\u00a0\u00a0\u00a0\u2503Capital\u00a0City\u00a0\u00a0\u00a0\u2503\u258e \u258a\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\u258e \u258a\u2502Hermes\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250275,000\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502None\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502\u258e \u258a\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u258e \u258a\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Data\u00a0for\u00a0Canceron\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258e \u258a\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u258e \u258a\u2503Patron\u00a0God\u00a0\u00a0\u00a0\u2503Population\u00a0\u00a0\u00a0\u00a0\u2503Capital\u00a0City\u00a0\u00a0\u2503\u258e \u258a\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

from __future__ import annotations\nfrom rich.table import Table\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Header, OptionList\nCOLONIES: tuple[tuple[str, str, str, str], ...] = (\n(\"Aerilon\", \"Demeter\", \"1.2 Billion\", \"Gaoth\"),\n(\"Aquaria\", \"Hermes\", \"75,000\", \"None\"),\n(\"Canceron\", \"Hephaestus\", \"6.7 Billion\", \"Hades\"),\n(\"Caprica\", \"Apollo\", \"4.9 Billion\", \"Caprica City\"),\n(\"Gemenon\", \"Hera\", \"2.8 Billion\", \"Oranu\"),\n(\"Leonis\", \"Artemis\", \"2.6 Billion\", \"Luminere\"),\n(\"Libran\", \"Athena\", \"2.1 Billion\", \"None\"),\n(\"Picon\", \"Poseidon\", \"1.4 Billion\", \"Queenstown\"),\n(\"Sagittaron\", \"Zeus\", \"1.7 Billion\", \"Tawa\"),\n(\"Scorpia\", \"Dionysus\", \"450 Million\", \"Celeste\"),\n(\"Tauron\", \"Ares\", \"2.5 Billion\", \"Hypatia\"),\n(\"Virgon\", \"Hestia\", \"4.3 Billion\", \"Boskirk\"),\n)\nclass OptionListApp(App[None]):\nCSS_PATH = \"option_list.tcss\"\n@staticmethod\ndef colony(name: str, god: str, population: str, capital: str) -> Table:\ntable = Table(title=f\"Data for {name}\", expand=True)\ntable.add_column(\"Patron God\")\ntable.add_column(\"Population\")\ntable.add_column(\"Capital City\")\ntable.add_row(god, population, capital)\nreturn table\ndef compose(self) -> ComposeResult:\nyield Header()\nyield OptionList(*[self.colony(*row) for row in COLONIES])\nyield Footer()\nif __name__ == \"__main__\":\nOptionListApp().run()\n
Screen {\nalign: center middle;\n}\nOptionList {\nwidth: 70%;\nheight: 80%;\n}\n
"},{"location":"widgets/option_list/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description highlighted int | None None The index of the highlighted option. None means nothing is highlighted."},{"location":"widgets/option_list/#messages","title":"Messages","text":"
  • OptionList.OptionHighlighted
  • OptionList.OptionSelected

Both of the messages above inherit from the common base OptionList, so refer to its documentation to see what attributes are available.

"},{"location":"widgets/option_list/#bindings","title":"Bindings","text":"

The option list widget defines the following bindings:

Key(s) Description down Move the highlight down. end Move the highlight to the last option. enter Select the current option. home Move the highlight to the first option. pagedown Move the highlight down a page of options. pageup Move the highlight up a page of options. up Move the highlight up."},{"location":"widgets/option_list/#component-classes","title":"Component Classes","text":"

The option list provides the following component classes:

Class Description option-list--option-disabled Target disabled options. option-list--option-highlighted Target the highlighted option. option-list--option-highlighted-disabled Target a disabled option that is also highlighted. option-list--option-hover Target an option that has the mouse over it. option-list--option-hover-disabled Target a disabled option that has the mouse over it. option-list--option-hover-highlighted Target a highlighted option that has the mouse over it. option-list--option-hover-highlighted-disabled Target a disabled highlighted option that has the mouse over it. option-list--separator Target the separators."},{"location":"widgets/option_list/#textual.widgets.OptionList","title":"textual.widgets.OptionList class","text":"
def __init__(\nself,\n*content,\nname=None,\nid=None,\nclasses=None,\ndisabled=False,\nwrap=True\n):\n

Bases: ScrollView

A vertical option list with bounce-bar highlighting.

Parameters Name Type Description Default *content NewOptionListContent

The content for the option list.

() name str | None

The name of the option list.

None id str | None

The ID of the option list in the DOM.

None classes str | None

The CSS classes of the option list.

None disabled bool

Whether the option list is disabled or not.

False wrap bool

Should prompts be auto-wrapped?

True"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.BINDINGS","title":"BINDINGS class-attribute","text":"
BINDINGS: list[BindingType] = [\nBinding(\"down\", \"cursor_down\", \"Down\", show=False),\nBinding(\"end\", \"last\", \"Last\", show=False),\nBinding(\"enter\", \"select\", \"Select\", show=False),\nBinding(\"home\", \"first\", \"First\", show=False),\nBinding(\n\"pagedown\", \"page_down\", \"Page Down\", show=False\n),\nBinding(\"pageup\", \"page_up\", \"Page Up\", show=False),\nBinding(\"up\", \"cursor_up\", \"Up\", show=False),\n]\n
Key(s) Description down Move the highlight down. end Move the highlight to the last option. enter Select the current option. home Move the highlight to the first option. pagedown Move the highlight down a page of options. pageup Move the highlight up a page of options. up Move the highlight up."},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
COMPONENT_CLASSES: set[str] = {\n\"option-list--option\",\n\"option-list--option-disabled\",\n\"option-list--option-highlighted\",\n\"option-list--option-highlighted-disabled\",\n\"option-list--option-hover\",\n\"option-list--option-hover-disabled\",\n\"option-list--option-hover-highlighted\",\n\"option-list--option-hover-highlighted-disabled\",\n\"option-list--separator\",\n}\n
Class Description option-list--option-disabled Target disabled options. option-list--option-highlighted Target the highlighted option. option-list--option-highlighted-disabled Target a disabled option that is also highlighted. option-list--option-hover Target an option that has the mouse over it. option-list--option-hover-disabled Target a disabled option that has the mouse over it. option-list--option-hover-highlighted Target a highlighted option that has the mouse over it. option-list--option-hover-highlighted-disabled Target a disabled highlighted option that has the mouse over it. option-list--separator Target the separators."},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.highlighted","title":"highlighted instance-attribute class-attribute","text":"
highlighted: reactive[int | None] = None\n

The index of the currently-highlighted option, or None if no option is highlighted.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.option_count","title":"option_count property","text":"
option_count: int\n

The count of options.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.OptionHighlighted","title":"OptionHighlighted class","text":"

Bases: OptionMessage

Message sent when an option is highlighted.

Can be handled using on_option_list_option_highlighted in a subclass of OptionList or in a parent node in the DOM.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.OptionMessage","title":"OptionMessage class","text":"
def __init__(self, option_list, index):\n

Bases: Message

Base class for all option messages.

Parameters Name Type Description Default option_list OptionList

The option list that owns the option.

required index int

The index of the option that the message relates to.

required"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.OptionMessage.control","title":"control property","text":"
control: OptionList\n

The option list that sent the message.

This is an alias for OptionMessage.option_list and is used by the on decorator.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.OptionMessage.option","title":"option instance-attribute","text":"
option: Option = option_list.get_option_at_index(index)\n

The highlighted option.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.OptionMessage.option_id","title":"option_id instance-attribute","text":"
option_id: str | None = self.option.id\n

The ID of the option that the message relates to.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.OptionMessage.option_index","title":"option_index instance-attribute","text":"
option_index: int = index\n

The index of the option that the message relates to.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.OptionMessage.option_list","title":"option_list instance-attribute","text":"
option_list: OptionList = option_list\n

The option list that sent the message.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.OptionSelected","title":"OptionSelected class","text":"

Bases: OptionMessage

Message sent when an option is selected.

Can be handled using on_option_list_option_selected in a subclass of OptionList or in a parent node in the DOM.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.action_cursor_down","title":"action_cursor_down method","text":"
def action_cursor_down(self):\n

Move the highlight down by one option.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.action_cursor_up","title":"action_cursor_up method","text":"
def action_cursor_up(self):\n

Move the highlight up by one option.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.action_first","title":"action_first method","text":"
def action_first(self):\n

Move the highlight to the first option.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.action_last","title":"action_last method","text":"
def action_last(self):\n

Move the highlight to the last option.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.action_page_down","title":"action_page_down method","text":"
def action_page_down(self):\n

Move the highlight down one page.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.action_page_up","title":"action_page_up method","text":"
def action_page_up(self):\n

Move the highlight up one page.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.action_select","title":"action_select method","text":"
def action_select(self):\n

Select the currently-highlighted option.

If no option is selected, then nothing happens. If an option is selected, a OptionList.OptionSelected message will be posted.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.add_option","title":"add_option method","text":"
def add_option(self, item=None):\n

Add a new option to the end of the option list.

Parameters Name Type Description Default item NewOptionListContent

The new item to add.

None Returns Type Description Self

The OptionList instance.

Raises Type Description DuplicateID

If there is an attempt to use a duplicate ID.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.add_options","title":"add_options method","text":"
def add_options(self, items):\n

Add new options to the end of the option list.

Parameters Name Type Description Default items Iterable[NewOptionListContent]

The new items to add.

required Returns Type Description Self

The OptionList instance.

Raises Type Description DuplicateID

If there is an attempt to use a duplicate ID.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.clear_options","title":"clear_options method","text":"
def clear_options(self):\n

Clear the content of the option list.

Returns Type Description Self

The OptionList instance.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.disable_option","title":"disable_option method","text":"
def disable_option(self, option_id):\n

Disable the option with the given ID.

Parameters Name Type Description Default option_id str

The ID of the option to disable.

required Returns Type Description Self

The OptionList instance.

Raises Type Description OptionDoesNotExist

If no option has the given ID.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.disable_option_at_index","title":"disable_option_at_index method","text":"
def disable_option_at_index(self, index):\n

Disable the option at the given index.

Returns Type Description Self

The OptionList instance.

Raises Type Description OptionDoesNotExist

If there is no option with the given index.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.enable_option","title":"enable_option method","text":"
def enable_option(self, option_id):\n

Enable the option with the given ID.

Parameters Name Type Description Default option_id str

The ID of the option to enable.

required Returns Type Description Self

The OptionList instance.

Raises Type Description OptionDoesNotExist

If no option has the given ID.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.enable_option_at_index","title":"enable_option_at_index method","text":"
def enable_option_at_index(self, index):\n

Enable the option at the given index.

Returns Type Description Self

The OptionList instance.

Raises Type Description OptionDoesNotExist

If there is no option with the given index.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.get_option","title":"get_option method","text":"
def get_option(self, option_id):\n

Get the option with the given ID.

Parameters Name Type Description Default option_id str

The ID of the option to get.

required Returns Type Description Option

The option with the ID.

Raises Type Description OptionDoesNotExist

If no option has the given ID.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.get_option_at_index","title":"get_option_at_index method","text":"
def get_option_at_index(self, index):\n

Get the option at the given index.

Parameters Name Type Description Default index int

The index of the option to get.

required Returns Type Description Option

The option at that index.

Raises Type Description OptionDoesNotExist

If there is no option with the given index.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.get_option_index","title":"get_option_index method","text":"
def get_option_index(self, option_id):\n

Get the index of the option with the given ID.

Parameters Name Type Description Default option_id

The ID of the option to get the index of.

required Raises Type Description OptionDoesNotExist

If no option has the given ID.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.remove_option","title":"remove_option method","text":"
def remove_option(self, option_id):\n

Remove the option with the given ID.

Parameters Name Type Description Default option_id str

The ID of the option to remove.

required Returns Type Description Self

The OptionList instance.

Raises Type Description OptionDoesNotExist

If no option has the given ID.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.remove_option_at_index","title":"remove_option_at_index method","text":"
def remove_option_at_index(self, index):\n

Remove the option at the given index.

Parameters Name Type Description Default index int

The index of the option to remove.

required Returns Type Description Self

The OptionList instance.

Raises Type Description OptionDoesNotExist

If there is no option with the given index.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.replace_option_prompt","title":"replace_option_prompt method","text":"
def replace_option_prompt(self, option_id, prompt):\n

Replace the prompt of the option with the given ID.

Parameters Name Type Description Default option_id str

The ID of the option to replace the prompt of.

required prompt RenderableType

The new prompt for the option.

required Returns Type Description Self

The OptionList instance.

Raises Type Description OptionDoesNotExist

If no option has the given ID.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.replace_option_prompt_at_index","title":"replace_option_prompt_at_index method","text":"
def replace_option_prompt_at_index(self, index, prompt):\n

Replace the prompt of the option at the given index.

Parameters Name Type Description Default index int

The index of the option to replace the prompt of.

required prompt RenderableType

The new prompt for the option.

required Returns Type Description Self

The OptionList instance.

Raises Type Description OptionDoesNotExist

If there is no option with the given index.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.scroll_to_highlight","title":"scroll_to_highlight method","text":"
def scroll_to_highlight(self, top=False):\n

Ensure that the highlighted option is in view.

Parameters Name Type Description Default top bool

Scroll highlight to top of the list.

False"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.validate_highlighted","title":"validate_highlighted method","text":"
def validate_highlighted(self, highlighted):\n

Validate the highlighted property value on access.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.watch_highlighted","title":"watch_highlighted method","text":"
def watch_highlighted(self, highlighted):\n

React to the highlighted option having changed.

"},{"location":"widgets/option_list/#textual.widgets._option_list.OptionList.watch_show_vertical_scrollbar","title":"watch_show_vertical_scrollbar method","text":"
def watch_show_vertical_scrollbar(self):\n

Handle the vertical scrollbar visibility status changing.

show_vertical_scrollbar is watched because it has an impact on the available width in which to render the renderables that make up the options in the list. If a vertical scrollbar appears or disappears we need to recalculate all the lines that make up the list.

"},{"location":"widgets/option_list/#textual.widgets.option_list.DuplicateID","title":"DuplicateID class","text":"

Bases: Exception

Exception raised if a duplicate ID is used.

"},{"location":"widgets/option_list/#textual.widgets.option_list.Option","title":"Option class","text":"
def __init__(self, prompt, id=None, disabled=False):\n

Class that holds the details of an individual option.

Parameters Name Type Description Default prompt RenderableType

The prompt for the option.

required id str | None

The optional ID for the option.

None disabled bool

The initial enabled/disabled state. Enabled by default.

False"},{"location":"widgets/option_list/#textual.widgets._option_list.Option.id","title":"id property","text":"
id: str | None\n

The optional ID for the option.

"},{"location":"widgets/option_list/#textual.widgets._option_list.Option.prompt","title":"prompt property","text":"
prompt: RenderableType\n

The prompt for the option.

"},{"location":"widgets/option_list/#textual.widgets._option_list.Option.set_prompt","title":"set_prompt method","text":"
def set_prompt(self, prompt):\n

Set the prompt for the option.

Parameters Name Type Description Default prompt RenderableType

The new prompt for the option.

required"},{"location":"widgets/option_list/#textual.widgets.option_list.OptionDoesNotExist","title":"OptionDoesNotExist class","text":"

Bases: Exception

Exception raised when a request has been made for an option that doesn't exist.

"},{"location":"widgets/option_list/#textual.widgets.option_list.Separator","title":"Separator class","text":"

Class used to add a separator to an OptionList.

"},{"location":"widgets/placeholder/","title":"Placeholder","text":"

Added in version 0.6.0

A widget that is meant to have no complex functionality. Use the placeholder widget when studying the layout of your app before having to develop your custom widgets.

The placeholder widget has variants that display different bits of useful information. Clicking a placeholder will cycle through its variants.

  • Focusable
  • Container
"},{"location":"widgets/placeholder/#example","title":"Example","text":"

The example below shows each placeholder variant.

Outputplaceholder.pyplaceholder.tcss

PlaceholderApp Placeholder\u00a0p2\u00a0here! This\u00a0is\u00a0a\u00a0custom\u00a0label\u00a0for\u00a0p1. #p4 #p3#p5Placeholde r Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0 26\u00a0x\u00a06amet,\u00a0consectetur\u00a027\u00a0x\u00a06 adipiscing\u00a0elit.\u00a0Etiam\u00a0 feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0 Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0 consectetur\u00a0adipiscing\u00a0elit.\u00a0Etiam\u00a040\u00a0x\u00a06 feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0accumsan.\u00a0 Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis\u00a0 gravida.\u00a0Phasellus\u00a0id\u00a0eleifend\u00a0ligula. Nullam\u00a0imperdiet\u00a0sem\u00a0tellus,\u00a0sed\u00a0 vehicula\u00a0nisl\u00a0faucibus\u00a0sit\u00a0amet.\u00a0Lorem\u00a0ipsum\u00a0dolor\u00a0sit\u00a0amet,\u00a0 Praesent\u00a0iaculis\u00a0tempor\u00a0ultricies.\u00a0Sedconsectetur\u00a0adipiscing\u00a0elit.\u00a0Etiam\u00a0 lacinia,\u00a0tellus\u00a0id\u00a0rutrum\u00a0lacinia,\u00a0feugiat\u00a0ac\u00a0elit\u00a0sit\u00a0amet\u00a0accumsan.\u00a0 sapien\u00a0sapien\u00a0congue\u00a0mauris,\u00a0sit\u00a0amet\u00a0Suspendisse\u00a0bibendum\u00a0nec\u00a0libero\u00a0quis\u00a0

from textual.app import App, ComposeResult\nfrom textual.containers import Container, Horizontal, VerticalScroll\nfrom textual.widgets import Placeholder\nclass PlaceholderApp(App):\nCSS_PATH = \"placeholder.tcss\"\ndef compose(self) -> ComposeResult:\nyield VerticalScroll(\nContainer(\nPlaceholder(\"This is a custom label for p1.\", id=\"p1\"),\nPlaceholder(\"Placeholder p2 here!\", id=\"p2\"),\nPlaceholder(id=\"p3\"),\nPlaceholder(id=\"p4\"),\nPlaceholder(id=\"p5\"),\nPlaceholder(),\nHorizontal(\nPlaceholder(variant=\"size\", id=\"col1\"),\nPlaceholder(variant=\"text\", id=\"col2\"),\nPlaceholder(variant=\"size\", id=\"col3\"),\nid=\"c1\",\n),\nid=\"bot\",\n),\nContainer(\nPlaceholder(variant=\"text\", id=\"left\"),\nPlaceholder(variant=\"size\", id=\"topright\"),\nPlaceholder(variant=\"text\", id=\"botright\"),\nid=\"top\",\n),\nid=\"content\",\n)\nif __name__ == \"__main__\":\napp = PlaceholderApp()\napp.run()\n
Placeholder {\nheight: 100%;\n}\n#top {\nheight: 50%;\nwidth: 100%;\nlayout: grid;\ngrid-size: 2 2;\n}\n#left {\nrow-span: 2;\n}\n#bot {\nheight: 50%;\nwidth: 100%;\nlayout: grid;\ngrid-size: 8 8;\n}\n#c1 {\nrow-span: 4;\ncolumn-span: 8;\nheight: 100%;\n}\n#col1, #col2, #col3 {\nwidth: 1fr;\n}\n#p1 {\nrow-span: 4;\ncolumn-span: 4;\n}\n#p2 {\nrow-span: 2;\ncolumn-span: 4;\n}\n#p3 {\nrow-span: 2;\ncolumn-span: 2;\n}\n#p4 {\nrow-span: 1;\ncolumn-span: 2;\n}\n
"},{"location":"widgets/placeholder/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description variant str \"default\" Styling variant. One of default, size, text."},{"location":"widgets/placeholder/#messages","title":"Messages","text":"

This widget posts no messages.

"},{"location":"widgets/placeholder/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/placeholder/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/placeholder/#textual.widgets.Placeholder","title":"textual.widgets.Placeholder class","text":"
def __init__(\nself,\nlabel=None,\nvariant=\"default\",\n*,\nname=None,\nid=None,\nclasses=None\n):\n

Bases: Widget

A simple placeholder widget to use before you build your custom widgets.

This placeholder has a couple of variants that show different data. Clicking the placeholder cycles through the available variants, but a placeholder can also be initialised in a specific variant.

The variants available are:

Variant Placeholder shows default Identifier label or the ID of the placeholder. size Size of the placeholder. text Lorem Ipsum text. Parameters Name Type Description Default label str | None

The label to identify the placeholder. If no label is present, uses the placeholder ID instead.

None variant PlaceholderVariant

The variant of the placeholder.

'default' name str | None

The name of the placeholder.

None id str | None

The ID of the placeholder in the DOM.

None classes str | None

A space separated string with the CSS classes of the placeholder, if any.

None"},{"location":"widgets/placeholder/#textual.widgets._placeholder.Placeholder.variant","title":"variant instance-attribute class-attribute","text":"
variant: Reactive[\nPlaceholderVariant\n] = self.validate_variant(variant)\n

The current variant of the placeholder.

"},{"location":"widgets/placeholder/#textual.widgets._placeholder.Placeholder.cycle_variant","title":"cycle_variant method","text":"
def cycle_variant(self):\n

Get the next variant in the cycle.

Returns Type Description Self

The Placeholder instance.

"},{"location":"widgets/placeholder/#textual.widgets._placeholder.Placeholder.validate_variant","title":"validate_variant method","text":"
def validate_variant(self, variant):\n

Validate the variant to which the placeholder was set.

"},{"location":"widgets/pretty/","title":"Pretty","text":"

Display a pretty-formatted object.

  • Focusable
  • Container
"},{"location":"widgets/pretty/#example","title":"Example","text":"

The example below shows a pretty-formatted dict, but Pretty can display any Python object.

Outputpretty.py

PrettyExample { 'title':\u00a0'Back\u00a0to\u00a0the\u00a0Future', 'releaseYear':\u00a01985, 'director':\u00a0'Robert\u00a0Zemeckis', 'genre':\u00a0'Adventure,\u00a0Comedy,\u00a0Sci-Fi', 'cast':\u00a0[ {'actor':\u00a0'Michael\u00a0J.\u00a0Fox',\u00a0'character':\u00a0'Marty\u00a0McFly'}, {'actor':\u00a0'Christopher\u00a0Lloyd',\u00a0'character':\u00a0'Dr.\u00a0Emmett\u00a0Brown'} ] }

from textual.app import App, ComposeResult\nfrom textual.widgets import Pretty\nDATA = {\n\"title\": \"Back to the Future\",\n\"releaseYear\": 1985,\n\"director\": \"Robert Zemeckis\",\n\"genre\": \"Adventure, Comedy, Sci-Fi\",\n\"cast\": [\n{\"actor\": \"Michael J. Fox\", \"character\": \"Marty McFly\"},\n{\"actor\": \"Christopher Lloyd\", \"character\": \"Dr. Emmett Brown\"},\n],\n}\nclass PrettyExample(App):\ndef compose(self) -> ComposeResult:\nyield Pretty(DATA)\napp = PrettyExample()\nif __name__ == \"__main__\":\napp.run()\n
"},{"location":"widgets/pretty/#reactive-attributes","title":"Reactive Attributes","text":"

This widget has no reactive attributes.

"},{"location":"widgets/pretty/#messages","title":"Messages","text":"

This widget posts no messages.

"},{"location":"widgets/pretty/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/pretty/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/pretty/#textual.widgets.Pretty","title":"textual.widgets.Pretty class","text":"
def __init__(self, object, *, name=None, id=None, classes=None):\n

Bases: Widget

A pretty-printing widget.

Used to pretty-print any object.

Parameters Name Type Description Default object Any

The object to pretty-print.

required name str | None

The name of the pretty widget.

None id str | None

The ID of the pretty in the DOM.

None classes str | None

The CSS classes of the pretty.

None"},{"location":"widgets/pretty/#textual.widgets._pretty.Pretty.update","title":"update method","text":"
def update(self, object):\n

Update the content of the pretty widget.

Parameters Name Type Description Default object Any

The object to pretty-print.

required"},{"location":"widgets/progress_bar/","title":"ProgressBar","text":"

A widget that displays progress on a time-consuming task.

  • Focusable
  • Container
"},{"location":"widgets/progress_bar/#examples","title":"Examples","text":""},{"location":"widgets/progress_bar/#progress-bar-in-isolation","title":"Progress Bar in Isolation","text":"

The example below shows a progress bar in isolation. It shows the progress bar in:

  • its indeterminate state, when the total progress hasn't been set yet;
  • the middle of the progress; and
  • the completed state.
Indeterminate state39% doneCompletedprogress_bar_isolated.py

IndeterminateProgressBar \u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501--%--:--:-- \u00a0S\u00a0\u00a0Start\u00a0

IndeterminateProgressBar \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u250139%00:00:07 \u00a0S\u00a0\u00a0Start\u00a0

IndeterminateProgressBar \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501100%--:--:-- \u00a0S\u00a0\u00a0Start\u00a0

from textual.app import App, ComposeResult\nfrom textual.containers import Center, Middle\nfrom textual.timer import Timer\nfrom textual.widgets import Footer, ProgressBar\nclass IndeterminateProgressBar(App[None]):\nBINDINGS = [(\"s\", \"start\", \"Start\")]\nprogress_timer: Timer\n\"\"\"Timer to simulate progress happening.\"\"\"\ndef compose(self) -> ComposeResult:\nwith Center():\nwith Middle():\nyield ProgressBar()\nyield Footer()\ndef on_mount(self) -> None:\n\"\"\"Set up a timer to simulate progess happening.\"\"\"\nself.progress_timer = self.set_interval(1 / 10, self.make_progress, pause=True)\ndef make_progress(self) -> None:\n\"\"\"Called automatically to advance the progress bar.\"\"\"\nself.query_one(ProgressBar).advance(1)\ndef action_start(self) -> None:\n\"\"\"Start the progress tracking.\"\"\"\nself.query_one(ProgressBar).update(total=100)\nself.progress_timer.resume()\nif __name__ == \"__main__\":\nIndeterminateProgressBar().run()\n
"},{"location":"widgets/progress_bar/#complete-app-example","title":"Complete App Example","text":"

The example below shows a simple app with a progress bar that is keeping track of a fictitious funding level for an organisation.

OutputOutput (partial funding)Output (full funding)progress_bar.pyprogress_bar.tcss

Funding\u00a0tracking \u2b58Funding\u00a0tracking Funding:\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u25010% \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258a$$$\u258e\u00a0Donate\u00a0 \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

Funding\u00a0tracking \u2b58Funding\u00a0tracking Funding:\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u250135% \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258a$$$\u258e\u00a0Donate\u00a0 \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 Donation\u00a0for\u00a0$15\u00a0received! Donation\u00a0for\u00a0$20\u00a0received!

Funding\u00a0tracking \u2b58Funding\u00a0tracking Funding:\u00a0\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501100% \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 \u258a$$$\u258e\u00a0Donate\u00a0 \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 Donation\u00a0for\u00a0$15\u00a0received! Donation\u00a0for\u00a0$20\u00a0received! Donation\u00a0for\u00a0$65\u00a0received!

from textual.app import App, ComposeResult\nfrom textual.containers import Center, VerticalScroll\nfrom textual.widgets import Button, Header, Input, Label, ProgressBar\nclass FundingProgressApp(App[None]):\nCSS_PATH = \"progress_bar.tcss\"\nTITLE = \"Funding tracking\"\ndef compose(self) -> ComposeResult:\nyield Header()\nwith Center():\nyield Label(\"Funding: \")\nyield ProgressBar(total=100, show_eta=False)  # (1)!\nwith Center():\nyield Input(placeholder=\"$$$\")\nyield Button(\"Donate\")\nyield VerticalScroll(id=\"history\")\ndef on_button_pressed(self) -> None:\nself.add_donation()\ndef on_input_submitted(self) -> None:\nself.add_donation()\ndef add_donation(self) -> None:\ntext_value = self.query_one(Input).value\ntry:\nvalue = int(text_value)\nexcept ValueError:\nreturn\nself.query_one(ProgressBar).advance(value)\nself.query_one(VerticalScroll).mount(Label(f\"Donation for ${value} received!\"))\nself.query_one(Input).value = \"\"\nif __name__ == \"__main__\":\nFundingProgressApp().run()\n
  1. We create a progress bar with a total of 100 steps and we hide the ETA countdown because we are not keeping track of a continuous, uninterrupted task.
Container {\noverflow: hidden hidden;\nheight: auto;\n}\nCenter {\nmargin-top: 1;\nmargin-bottom: 1;\nlayout: horizontal;\n}\nProgressBar {\npadding-left: 3;\n}\nInput {\nwidth: 16;\n}\nVerticalScroll {\nheight: auto;\n}\n
"},{"location":"widgets/progress_bar/#custom-styling","title":"Custom Styling","text":"

This shows a progress bar with custom styling. Refer to the section below for more information.

Indeterminate state39% doneCompletedprogress_bar_styled.pyprogress_bar_styled.tcss

StyledProgressBar \u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501--%--:--:-- \u00a0S\u00a0\u00a0Start\u00a0

StyledProgressBar \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u250139%00:00:07 \u00a0S\u00a0\u00a0Start\u00a0

StyledProgressBar \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501100%--:--:-- \u00a0S\u00a0\u00a0Start\u00a0

from textual.app import App, ComposeResult\nfrom textual.containers import Center, Middle\nfrom textual.timer import Timer\nfrom textual.widgets import Footer, ProgressBar\nclass StyledProgressBar(App[None]):\nBINDINGS = [(\"s\", \"start\", \"Start\")]\nCSS_PATH = \"progress_bar_styled.tcss\"\nprogress_timer: Timer\n\"\"\"Timer to simulate progress happening.\"\"\"\ndef compose(self) -> ComposeResult:\nwith Center():\nwith Middle():\nyield ProgressBar()\nyield Footer()\ndef on_mount(self) -> None:\n\"\"\"Set up a timer to simulate progess happening.\"\"\"\nself.progress_timer = self.set_interval(1 / 10, self.make_progress, pause=True)\ndef make_progress(self) -> None:\n\"\"\"Called automatically to advance the progress bar.\"\"\"\nself.query_one(ProgressBar).advance(1)\ndef action_start(self) -> None:\n\"\"\"Start the progress tracking.\"\"\"\nself.query_one(ProgressBar).update(total=100)\nself.progress_timer.resume()\nif __name__ == \"__main__\":\nStyledProgressBar().run()\n
Bar > .bar--indeterminate {\ncolor: $primary;\nbackground: $secondary;\n}\nBar > .bar--bar {\ncolor: $primary;\nbackground: $primary 30%;\n}\nBar > .bar--complete {\ncolor: $error;\n}\nPercentageStatus {\ntext-style: reverse;\ncolor: $secondary;\n}\nETAStatus {\ntext-style: underline;\n}\n
"},{"location":"widgets/progress_bar/#styling-the-progress-bar","title":"Styling the Progress Bar","text":"

The progress bar is composed of three sub-widgets that can be styled independently:

Widget name ID Description Bar #bar The bar that visually represents the progress made. PercentageStatus #percentage Label that shows the percentage of completion. ETAStatus #eta Label that shows the estimated time to completion."},{"location":"widgets/progress_bar/#bar-component-classes","title":"Bar Component Classes","text":"

The bar sub-widget provides the component classes that follow.

These component classes let you modify the foreground and background color of the bar in its different states.

Class Description bar--bar Style of the bar (may be used to change the color). bar--complete Style of the bar when it's complete. bar--indeterminate Style of the bar when it's in an indeterminate state."},{"location":"widgets/progress_bar/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description percentage float | None The read-only percentage of progress that has been made. This is None if the total hasn't been set. progress float 0 The number of steps of progress already made. total float | None The total number of steps that we are keeping track of."},{"location":"widgets/progress_bar/#messages","title":"Messages","text":"

This widget posts no messages.

"},{"location":"widgets/progress_bar/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/progress_bar/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/progress_bar/#textual.widgets.ProgressBar","title":"textual.widgets.ProgressBar class","text":"
def __init__(\nself,\ntotal=None,\n*,\nshow_bar=True,\nshow_percentage=True,\nshow_eta=True,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: Widget

A progress bar widget.

The progress bar uses \"steps\" as the measurement unit.

Example
class MyApp(App):\ndef compose(self):\nyield ProgressBar(total=100)\ndef key_space(self):\nself.query_one(ProgressBar).advance(5)\n
Parameters Name Type Description Default total float | None

The total number of steps in the progress if known.

None show_bar bool

Whether to show the bar portion of the progress bar.

True show_percentage bool

Whether to show the percentage status of the bar.

True show_eta bool

Whether to show the ETA countdown of the progress bar.

True name str | None

The name of the widget.

None id str | None

The ID of the widget in the DOM.

None classes str | None

The CSS classes for the widget.

None disabled bool

Whether the widget is disabled or not.

False"},{"location":"widgets/progress_bar/#textual.widgets._progress_bar.ProgressBar.percentage","title":"percentage instance-attribute class-attribute","text":"
percentage: reactive[float | None] = reactive[\nOptional[float]\n](None)\n

The percentage of progress that has been completed.

The percentage is a value between 0 and 1 and the returned value is only None if the total progress of the bar hasn't been set yet.

Example
progress_bar = ProgressBar()\nprint(progress_bar.percentage)  # None\nprogress_bar.update(total=100)\nprogress_bar.advance(50)\nprint(progress_bar.percentage)  # 0.5\n
"},{"location":"widgets/progress_bar/#textual.widgets._progress_bar.ProgressBar.progress","title":"progress instance-attribute class-attribute","text":"
progress: reactive[float] = reactive(0.0)\n

The progress so far, in number of steps.

"},{"location":"widgets/progress_bar/#textual.widgets._progress_bar.ProgressBar.total","title":"total instance-attribute class-attribute","text":"
total: reactive[float | None] = total\n

The total number of steps associated with this progress bar, when known.

The value None will render an indeterminate progress bar.

"},{"location":"widgets/progress_bar/#textual.widgets._progress_bar.ProgressBar.advance","title":"advance method","text":"
def advance(self, advance=1):\n

Advance the progress of the progress bar by the given amount.

Example
progress_bar.advance(10)  # Advance 10 steps.\n
Parameters Name Type Description Default advance float

Number of steps to advance progress by.

1"},{"location":"widgets/progress_bar/#textual.widgets._progress_bar.ProgressBar.compute_percentage","title":"compute_percentage method","text":"
def compute_percentage(self):\n

Keep the percentage of progress updated automatically.

This will report a percentage of 1 if the total is zero.

"},{"location":"widgets/progress_bar/#textual.widgets._progress_bar.ProgressBar.update","title":"update method","text":"
def update(\nself, *, total=UNUSED, progress=UNUSED, advance=UNUSED\n):\n

Update the progress bar with the given options.

Example
progress_bar.update(\ntotal=200,  # Set new total to 200 steps.\nprogress=50,  # Set the progress to 50 (out of 200).\n)\n
Parameters Name Type Description Default total None | float | UnusedParameter

New total number of steps.

UNUSED progress float | UnusedParameter

Set the progress to the given number of steps.

UNUSED advance float | UnusedParameter

Advance the progress by this number of steps.

UNUSED"},{"location":"widgets/progress_bar/#textual.widgets._progress_bar.ProgressBar.validate_progress","title":"validate_progress method","text":"
def validate_progress(self, progress):\n

Clamp the progress between 0 and the maximum total.

"},{"location":"widgets/progress_bar/#textual.widgets._progress_bar.ProgressBar.validate_total","title":"validate_total method","text":"
def validate_total(self, total):\n

Ensure the total is not negative.

"},{"location":"widgets/progress_bar/#textual.widgets._progress_bar.ProgressBar.watch_total","title":"watch_total method","text":"
def watch_total(self, total):\n

Re-validate progress.

"},{"location":"widgets/radiobutton/","title":"RadioButton","text":"

Added in version 0.13.0

A simple radio button which stores a boolean value.

  • Focusable
  • Container

A radio button is best used with others inside a RadioSet.

"},{"location":"widgets/radiobutton/#example","title":"Example","text":"

The example below shows radio buttons, used within a RadioSet.

Outputradio_button.pyradio_button.tcss

RadioChoicesApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590\u25cf\u258cBattlestar\u00a0Galactica\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a01984\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a02021\u258e \u258a\u2590\u25cf\u258c\u00a0Serenity\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Trek:\u00a0The\u00a0Motion\u00a0Picture\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Wars:\u00a0A\u00a0New\u00a0Hope\u258e \u258a\u2590\u25cf\u258c\u00a0The\u00a0Last\u00a0Starfighter\u258e \u258a\u2590\u25cf\u258c\u00a0Total\u00a0Recall\u00a0\ud83d\udc49\u00a0\ud83d\udd34\u258e \u258a\u2590\u25cf\u258c\u00a0Wing\u00a0Commander\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

from textual.app import App, ComposeResult\nfrom textual.widgets import RadioButton, RadioSet\nclass RadioChoicesApp(App[None]):\nCSS_PATH = \"radio_button.tcss\"\ndef compose(self) -> ComposeResult:\nwith RadioSet():\nyield RadioButton(\"Battlestar Galactica\")\nyield RadioButton(\"Dune 1984\")\nyield RadioButton(\"Dune 2021\", id=\"focus_me\")\nyield RadioButton(\"Serenity\", value=True)\nyield RadioButton(\"Star Trek: The Motion Picture\")\nyield RadioButton(\"Star Wars: A New Hope\")\nyield RadioButton(\"The Last Starfighter\")\nyield RadioButton(\n\"Total Recall :backhand_index_pointing_right: :red_circle:\"\n)\nyield RadioButton(\"Wing Commander\")\ndef on_mount(self) -> None:\nself.query_one(RadioSet).focus()\nif __name__ == \"__main__\":\nRadioChoicesApp().run()\n
Screen {\nalign: center middle;\n}\nRadioSet {\nwidth: 50%;\n}\n
"},{"location":"widgets/radiobutton/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description value bool False The value of the radio button."},{"location":"widgets/radiobutton/#messages","title":"Messages","text":"
  • RadioButton.Changed
"},{"location":"widgets/radiobutton/#bindings","title":"Bindings","text":"

The radio button widget defines the following bindings:

Key(s) Description enter, space Toggle the value."},{"location":"widgets/radiobutton/#component-classes","title":"Component Classes","text":"

The checkbox widget inherits the following component classes:

Class Description toggle--button Targets the toggle button itself. toggle--label Targets the text label of the toggle button."},{"location":"widgets/radiobutton/#see-also","title":"See Also","text":"
  • RadioSet
"},{"location":"widgets/radiobutton/#textual.widgets.RadioButton","title":"textual.widgets.RadioButton class","text":"

Bases: ToggleButton

A radio button widget that represents a boolean value.

Note

A RadioButton is best used within a RadioSet.

"},{"location":"widgets/radiobutton/#textual.widgets._radio_button.RadioButton.BUTTON_INNER","title":"BUTTON_INNER instance-attribute class-attribute","text":"
BUTTON_INNER = '\u25cf'\n

The character used for the inside of the button.

"},{"location":"widgets/radiobutton/#textual.widgets._radio_button.RadioButton.Changed","title":"Changed class","text":"

Bases: ToggleButton.Changed

Posted when the value of the radio button changes.

This message can be handled using an on_radio_button_changed method.

"},{"location":"widgets/radiobutton/#textual.widgets._radio_button.RadioButton.Changed.control","title":"control property","text":"
control: RadioButton\n

Alias for Changed.radio_button.

"},{"location":"widgets/radiobutton/#textual.widgets._radio_button.RadioButton.Changed.radio_button","title":"radio_button property","text":"
radio_button: RadioButton\n

The radio button that was changed.

"},{"location":"widgets/radioset/","title":"RadioSet","text":"

Added in version 0.13.0

A container widget that groups RadioButtons together.

  • Focusable
  • Container
"},{"location":"widgets/radioset/#example","title":"Example","text":""},{"location":"widgets/radioset/#simple-example","title":"Simple example","text":"

The example below shows two radio sets, one built using a collection of radio buttons, the other a collection of simple strings.

Outputradio_set.pyradio_set.tcss

RadioChoicesApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e\u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590\u25cf\u258cBattlestar\u00a0Galactica\u258e\u258a\u2590\u25cf\u258c\u00a0Amanda\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a01984\u258e\u258a\u2590\u25cf\u258c\u00a0Connor\u00a0MacLeod\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a02021\u258e\u258a\u2590\u25cf\u258c\u00a0Duncan\u00a0MacLeod\u258e \u258a\u2590\u25cf\u258c\u00a0Serenity\u258e\u258a\u2590\u25cf\u258c\u00a0Heather\u00a0MacLeod\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Trek:\u00a0The\u00a0Motion\u00a0Picture\u258e\u258a\u2590\u25cf\u258c\u00a0Joe\u00a0Dawson\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Wars:\u00a0A\u00a0New\u00a0Hope\u258e\u258a\u2590\u25cf\u258c\u00a0Kurgan,\u00a0The\u258e \u258a\u2590\u25cf\u258c\u00a0The\u00a0Last\u00a0Starfighter\u258e\u258a\u2590\u25cf\u258c\u00a0Methos\u258e \u258a\u2590\u25cf\u258c\u00a0Total\u00a0Recall\u00a0\ud83d\udc49\u00a0\ud83d\udd34\u258e\u258a\u2590\u25cf\u258c\u00a0Rachel\u00a0Ellenstein\u258e \u258a\u2590\u25cf\u258c\u00a0Wing\u00a0Commander\u258e\u258a\u2590\u25cf\u258c\u00a0Ram\u00edrez\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e\u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.widgets import RadioButton, RadioSet\nclass RadioChoicesApp(App[None]):\nCSS_PATH = \"radio_set.tcss\"\ndef compose(self) -> ComposeResult:\nwith Horizontal():\n# A RadioSet built up from RadioButtons.\nwith RadioSet(id=\"focus_me\"):\nyield RadioButton(\"Battlestar Galactica\")\nyield RadioButton(\"Dune 1984\")\nyield RadioButton(\"Dune 2021\")\nyield RadioButton(\"Serenity\", value=True)\nyield RadioButton(\"Star Trek: The Motion Picture\")\nyield RadioButton(\"Star Wars: A New Hope\")\nyield RadioButton(\"The Last Starfighter\")\nyield RadioButton(\n\"Total Recall :backhand_index_pointing_right: :red_circle:\"\n)\nyield RadioButton(\"Wing Commander\")\n# A RadioSet built up from a collection of strings.\nyield RadioSet(\n\"Amanda\",\n\"Connor MacLeod\",\n\"Duncan MacLeod\",\n\"Heather MacLeod\",\n\"Joe Dawson\",\n\"Kurgan, [bold italic red]The[/]\",\n\"Methos\",\n\"Rachel Ellenstein\",\n\"Ram\u00edrez\",\n)\ndef on_mount(self) -> None:\nself.query_one(\"#focus_me\").focus()\nif __name__ == \"__main__\":\nRadioChoicesApp().run()\n
Screen {\nalign: center middle;\n}\nHorizontal {\nalign: center middle;\nheight: auto;\n}\nRadioSet {\nwidth: 45%;\n}\n
"},{"location":"widgets/radioset/#reacting-to-changes-in-a-radio-set","title":"Reacting to Changes in a Radio Set","text":"

Here is an example of using the message to react to changes in a RadioSet:

Outputradio_set_changed.pyradio_set_changed.tcss

RadioSetChangedApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u2590\u25cf\u258cBattlestar\u00a0Galactica\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a01984\u258e \u258a\u2590\u25cf\u258c\u00a0Dune\u00a02021\u258e \u258a\u2590\u25cf\u258c\u00a0Serenity\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Trek:\u00a0The\u00a0Motion\u00a0Picture\u258e \u258a\u2590\u25cf\u258c\u00a0Star\u00a0Wars:\u00a0A\u00a0New\u00a0Hope\u258e \u258a\u2590\u25cf\u258c\u00a0The\u00a0Last\u00a0Starfighter\u258e \u258a\u2590\u25cf\u258c\u00a0Total\u00a0Recall\u00a0\ud83d\udc49\u00a0\ud83d\udd34\u258e \u258a\u2590\u25cf\u258c\u00a0Wing\u00a0Commander\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e Pressed\u00a0button\u00a0label:\u00a0Battlestar\u00a0Galactica Pressed\u00a0button\u00a0index:\u00a00

from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal, VerticalScroll\nfrom textual.widgets import Label, RadioButton, RadioSet\nclass RadioSetChangedApp(App[None]):\nCSS_PATH = \"radio_set_changed.tcss\"\ndef compose(self) -> ComposeResult:\nwith VerticalScroll():\nwith Horizontal():\nwith RadioSet(id=\"focus_me\"):\nyield RadioButton(\"Battlestar Galactica\")\nyield RadioButton(\"Dune 1984\")\nyield RadioButton(\"Dune 2021\")\nyield RadioButton(\"Serenity\", value=True)\nyield RadioButton(\"Star Trek: The Motion Picture\")\nyield RadioButton(\"Star Wars: A New Hope\")\nyield RadioButton(\"The Last Starfighter\")\nyield RadioButton(\n\"Total Recall :backhand_index_pointing_right: :red_circle:\"\n)\nyield RadioButton(\"Wing Commander\")\nwith Horizontal():\nyield Label(id=\"pressed\")\nwith Horizontal():\nyield Label(id=\"index\")\ndef on_mount(self) -> None:\nself.query_one(RadioSet).focus()\ndef on_radio_set_changed(self, event: RadioSet.Changed) -> None:\nself.query_one(\"#pressed\", Label).update(\nf\"Pressed button label: {event.pressed.label}\"\n)\nself.query_one(\"#index\", Label).update(\nf\"Pressed button index: {event.radio_set.pressed_index}\"\n)\nif __name__ == \"__main__\":\nRadioSetChangedApp().run()\n
VerticalScroll {\nalign: center middle;\n}\nHorizontal {\nalign: center middle;\nheight: auto;\n}\nRadioSet {\nwidth: 45%;\n}\n
"},{"location":"widgets/radioset/#messages","title":"Messages","text":"
  • RadioSet.Changed
"},{"location":"widgets/radioset/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/radioset/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/radioset/#see-also","title":"See Also","text":"
  • RadioButton
"},{"location":"widgets/radioset/#textual.widgets.RadioSet","title":"textual.widgets.RadioSet class","text":"
def __init__(\nself,\n*buttons,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: Container

Widget for grouping a collection of radio buttons into a set.

When a collection of RadioButtons are grouped with this widget, they will be treated as a mutually-exclusive grouping. If one button is turned on, the previously-on button will be turned off.

Parameters Name Type Description Default buttons str | RadioButton

The labels or RadioButtons to group together.

() name str | None

The name of the radio set.

None id str | None

The ID of the radio set in the DOM.

None classes str | None

The CSS classes of the radio set.

None disabled bool

Whether the radio set is disabled or not.

False Note

When a str label is provided, a RadioButton will be created from it.

"},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.BINDINGS","title":"BINDINGS class-attribute","text":"
BINDINGS: list[BindingType] = [\nBinding(\"down,right\", \"next_button\", \"\", show=False),\nBinding(\"enter,space\", \"toggle\", \"Toggle\", show=False),\nBinding(\"up,left\", \"previous_button\", \"\", show=False),\n]\n
Key(s) Description enter, space Toggle the currently-selected button. left, up Select the previous radio button in the set. right, down Select the next radio button in the set."},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.pressed_button","title":"pressed_button property","text":"
pressed_button: RadioButton | None\n

The currently-pressed RadioButton, or None if none are pressed.

"},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.pressed_index","title":"pressed_index property","text":"
pressed_index: int\n

The index of the currently-pressed RadioButton, or -1 if none are pressed.

"},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.Changed","title":"Changed class","text":"
def __init__(self, radio_set, pressed):\n

Bases: Message

Posted when the pressed button in the set changes.

This message can be handled using an on_radio_set_changed method.

Parameters Name Type Description Default pressed RadioButton

The radio button that was pressed.

required"},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.Changed.ALLOW_SELECTOR_MATCH","title":"ALLOW_SELECTOR_MATCH instance-attribute class-attribute","text":"
ALLOW_SELECTOR_MATCH = {'pressed'}\n

Additional message attributes that can be used with the on decorator.

"},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.Changed.control","title":"control property","text":"
control: RadioSet\n

A reference to the RadioSet that was changed.

This is an alias for Changed.radio_set and is used by the on decorator.

"},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.Changed.index","title":"index instance-attribute","text":"
index = radio_set.pressed_index\n

The index of the RadioButton that was pressed to make the change.

"},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.Changed.pressed","title":"pressed instance-attribute","text":"
pressed = pressed\n

The RadioButton that was pressed to make the change.

"},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.Changed.radio_set","title":"radio_set instance-attribute","text":"
radio_set = radio_set\n

A reference to the RadioSet that was changed.

"},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.action_next_button","title":"action_next_button method","text":"
def action_next_button(self):\n

Navigate to the next button in the set.

Note that this will wrap around to the start if at the end.

"},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.action_previous_button","title":"action_previous_button method","text":"
def action_previous_button(self):\n

Navigate to the previous button in the set.

Note that this will wrap around to the end if at the start.

"},{"location":"widgets/radioset/#textual.widgets._radio_set.RadioSet.action_toggle","title":"action_toggle method","text":"
def action_toggle(self):\n

Toggle the state of the currently-selected button.

"},{"location":"widgets/rich_log/","title":"RichLog","text":"

A RichLog is a widget which displays scrollable content that may be appended to in realtime.

Call RichLog.write with a string or Rich Renderable to write content to the end of the RichLog. Call RichLog.clear to clear the content.

Tip

See also Log which is an alternative to RichLog but specialized for simple text.

  • Focusable
  • Container
"},{"location":"widgets/rich_log/#example","title":"Example","text":"

The example below shows an application showing a RichLog with different kinds of data logged.

Outputrich_log.py

RichLogApp \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0previous_value=next(iter_values) \u2502\u00a0\u00a0\u00a0exceptStopIteration: \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0return \u2502\u00a0\u00a0\u00a0first=True\u2585\u2585 \u2502\u00a0\u00a0\u00a0forvalueiniter_values: \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0yieldfirst,False,previous_value \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0first=False \u2502\u00a0\u00a0\u00a0\u2502\u00a0\u00a0\u00a0previous_value=value \u2502\u00a0\u00a0\u00a0yieldfirst,True,previous_value \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u2503lane\u2503swimmer\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503country\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2503time\u00a0\u2503 \u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529 \u25024\u00a0\u00a0\u00a0\u2502Joseph\u00a0Schooling\u00a0\u00a0\u00a0\u00a0\u2502Singapore\u00a0\u00a0\u00a0\u00a0\u250250.39\u2502 \u25022\u00a0\u00a0\u00a0\u2502Michael\u00a0Phelps\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502United\u00a0States\u250251.14\u2502 \u25025\u00a0\u00a0\u00a0\u2502Chad\u00a0le\u00a0Clos\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502South\u00a0Africa\u00a0\u250251.14\u2502 \u25026\u00a0\u00a0\u00a0\u2502L\u00e1szl\u00f3\u00a0Cseh\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502Hungary\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250251.14\u2502 \u25023\u00a0\u00a0\u00a0\u2502Li\u00a0Zhuhao\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502China\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250251.26\u2502 \u25028\u00a0\u00a0\u00a0\u2502Mehdy\u00a0Metella\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502France\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250251.58\u2502 \u25027\u00a0\u00a0\u00a0\u2502Tom\u00a0Shields\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u2502United\u00a0States\u250251.73\u2502 \u25021\u00a0\u00a0\u00a0\u2502Aleksandr\u00a0Sadovnikov\u2502Russia\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u250251.84\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 Write\u00a0text\u00a0or\u00a0any\u00a0Rich\u00a0renderable! Key(key='H',\u00a0character='H',\u00a0name='upper_h',\u00a0is_printable=True) Key(key='i',\u00a0character='i',\u00a0name='i',\u00a0is_printable=True)

import csv\nimport io\nfrom rich.syntax import Syntax\nfrom rich.table import Table\nfrom textual import events\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import RichLog\nCSV = \"\"\"lane,swimmer,country,time\n4,Joseph Schooling,Singapore,50.39\n2,Michael Phelps,United States,51.14\n5,Chad le Clos,South Africa,51.14\n6,L\u00e1szl\u00f3 Cseh,Hungary,51.14\n3,Li Zhuhao,China,51.26\n8,Mehdy Metella,France,51.58\n7,Tom Shields,United States,51.73\n1,Aleksandr Sadovnikov,Russia,51.84\"\"\"\nCODE = '''\\\ndef loop_first_last(values: Iterable[T]) -> Iterable[tuple[bool, bool, T]]:\n    \"\"\"Iterate and generate a tuple with a flag for first and last value.\"\"\"\n    iter_values = iter(values)\n    try:\n        previous_value = next(iter_values)\n    except StopIteration:\n        return\n    first = True\n    for value in iter_values:\n        yield first, False, previous_value\n        first = False\n        previous_value = value\n    yield first, True, previous_value\\\n'''\nclass RichLogApp(App):\ndef compose(self) -> ComposeResult:\nyield RichLog(highlight=True, markup=True)\ndef on_ready(self) -> None:\n\"\"\"Called  when the DOM is ready.\"\"\"\ntext_log = self.query_one(RichLog)\ntext_log.write(Syntax(CODE, \"python\", indent_guides=True))\nrows = iter(csv.reader(io.StringIO(CSV)))\ntable = Table(*next(rows))\nfor row in rows:\ntable.add_row(*row)\ntext_log.write(table)\ntext_log.write(\"[bold magenta]Write text or any Rich renderable!\")\ndef on_key(self, event: events.Key) -> None:\n\"\"\"Write Key events to log.\"\"\"\ntext_log = self.query_one(RichLog)\ntext_log.write(event)\nif __name__ == \"__main__\":\napp = RichLogApp()\napp.run()\n
"},{"location":"widgets/rich_log/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description highlight bool False Automatically highlight content. markup bool False Apply Rich console markup. max_lines int None Maximum number of lines in the log or None for no maximum. min_width int 78 Minimum width of renderables. wrap bool False Enable word wrapping."},{"location":"widgets/rich_log/#messages","title":"Messages","text":"

This widget sends no messages.

"},{"location":"widgets/rich_log/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/rich_log/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/rich_log/#textual.widgets.RichLog","title":"textual.widgets.RichLog class","text":"
def __init__(\nself,\n*,\nmax_lines=None,\nmin_width=78,\nwrap=False,\nhighlight=False,\nmarkup=False,\nauto_scroll=True,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: ScrollView

A widget for logging text.

Parameters Name Type Description Default max_lines int | None

Maximum number of lines in the log or None for no maximum.

None min_width int

Minimum width of renderables.

78 wrap bool

Enable word wrapping (default is off).

False highlight bool

Automatically highlight content.

False markup bool

Apply Rich console markup.

False auto_scroll bool

Enable automatic scrolling to end.

True name str | None

The name of the text log.

None id str | None

The ID of the text log in the DOM.

None classes str | None

The CSS classes of the text log.

None disabled bool

Whether the text log is disabled or not.

False"},{"location":"widgets/rich_log/#textual.widgets._rich_log.RichLog.auto_scroll","title":"auto_scroll instance-attribute class-attribute","text":"
auto_scroll: var[bool] = auto_scroll\n

Automatically scroll to the end on write.

"},{"location":"widgets/rich_log/#textual.widgets._rich_log.RichLog.highlight","title":"highlight instance-attribute class-attribute","text":"
highlight: var[bool] = highlight\n

Automatically highlight content.

"},{"location":"widgets/rich_log/#textual.widgets._rich_log.RichLog.markup","title":"markup instance-attribute class-attribute","text":"
markup: var[bool] = markup\n

Apply Rich console markup.

"},{"location":"widgets/rich_log/#textual.widgets._rich_log.RichLog.max_lines","title":"max_lines instance-attribute class-attribute","text":"
max_lines: var[int | None] = max_lines\n

Maximum number of lines in the log or None for no maximum.

"},{"location":"widgets/rich_log/#textual.widgets._rich_log.RichLog.min_width","title":"min_width instance-attribute class-attribute","text":"
min_width: var[int] = min_width\n

Minimum width of renderables.

"},{"location":"widgets/rich_log/#textual.widgets._rich_log.RichLog.wrap","title":"wrap instance-attribute class-attribute","text":"
wrap: var[bool] = wrap\n

Enable word wrapping.

"},{"location":"widgets/rich_log/#textual.widgets._rich_log.RichLog.clear","title":"clear method","text":"
def clear(self):\n

Clear the text log.

Returns Type Description Self

The RichLog instance.

"},{"location":"widgets/rich_log/#textual.widgets._rich_log.RichLog.write","title":"write method","text":"
def write(\nself,\ncontent,\nwidth=None,\nexpand=False,\nshrink=True,\nscroll_end=None,\n):\n

Write text or a rich renderable.

Parameters Name Type Description Default content RenderableType | object

Rich renderable (or text).

required width int | None

Width to render or None to use optimal width.

None expand bool

Enable expand to widget width, or False to use width.

False shrink bool

Enable shrinking of content to fit width.

True scroll_end bool | None

Enable automatic scroll to end, or None to use self.auto_scroll.

None Returns Type Description Self

The RichLog instance.

"},{"location":"widgets/rule/","title":"Rule","text":"

A rule widget to separate content, similar to a <hr> HTML tag.

  • Focusable
  • Container
"},{"location":"widgets/rule/#examples","title":"Examples","text":""},{"location":"widgets/rule/#horizontal-rule","title":"Horizontal Rule","text":"

The default orientation of a rule is horizontal.

The example below shows horizontal rules with all the available line styles.

Outputhorizontal_rules.pyhorizontal_rules.tcss

HorizontalRulesApp \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0solid\u00a0(default)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0heavy\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0thick\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0dashed\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d\u254d \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0double\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0ascii\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 ----------------------------------------------------------------

from textual.app import App, ComposeResult\nfrom textual.containers import Vertical\nfrom textual.widgets import Label, Rule\nclass HorizontalRulesApp(App):\nCSS_PATH = \"horizontal_rules.tcss\"\ndef compose(self) -> ComposeResult:\nwith Vertical():\nyield Label(\"solid (default)\")\nyield Rule()\nyield Label(\"heavy\")\nyield Rule(line_style=\"heavy\")\nyield Label(\"thick\")\nyield Rule(line_style=\"thick\")\nyield Label(\"dashed\")\nyield Rule(line_style=\"dashed\")\nyield Label(\"double\")\nyield Rule(line_style=\"double\")\nyield Label(\"ascii\")\nyield Rule(line_style=\"ascii\")\nif __name__ == \"__main__\":\napp = HorizontalRulesApp()\napp.run()\n
Screen {\nalign: center middle;\n}\nVertical {\nheight: auto;\nwidth: 80%;\n}\nLabel {\nwidth: 100%;\ntext-align: center;\n}\n
"},{"location":"widgets/rule/#vertical-rule","title":"Vertical Rule","text":"

The example below shows vertical rules with all the available line styles.

Outputvertical_rules.pyvertical_rules.tcss

VerticalRulesApp solid\u00a0\u2502heavy\u00a0\u2503thick\u00a0\u2588dashed\u254fdouble\u2551ascii\u00a0| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551| \u2502\u2503\u2588\u254f\u2551|

from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.widgets import Label, Rule\nclass VerticalRulesApp(App):\nCSS_PATH = \"vertical_rules.tcss\"\ndef compose(self) -> ComposeResult:\nwith Horizontal():\nyield Label(\"solid\")\nyield Rule(orientation=\"vertical\")\nyield Label(\"heavy\")\nyield Rule(orientation=\"vertical\", line_style=\"heavy\")\nyield Label(\"thick\")\nyield Rule(orientation=\"vertical\", line_style=\"thick\")\nyield Label(\"dashed\")\nyield Rule(orientation=\"vertical\", line_style=\"dashed\")\nyield Label(\"double\")\nyield Rule(orientation=\"vertical\", line_style=\"double\")\nyield Label(\"ascii\")\nyield Rule(orientation=\"vertical\", line_style=\"ascii\")\nif __name__ == \"__main__\":\napp = VerticalRulesApp()\napp.run()\n
Screen {\nalign: center middle;\n}\nHorizontal {\nwidth: auto;\nheight: 80%;\n}\nLabel {\nwidth: 6;\nheight: 100%;\ntext-align: center;\n}\n
"},{"location":"widgets/rule/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description orientation RuleOrientation \"horizontal\" The orientation of the rule. line_style LineStyle \"solid\" The line style of the rule."},{"location":"widgets/rule/#messages","title":"Messages","text":"

This widget sends no messages.

"},{"location":"widgets/rule/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/rule/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/rule/#textual.widgets.Rule","title":"textual.widgets.Rule class","text":"
def __init__(\nself,\norientation=\"horizontal\",\nline_style=\"solid\",\n*,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: Widget

A rule widget to separate content, similar to a <hr> HTML tag.

Parameters Name Type Description Default orientation RuleOrientation

The orientation of the rule.

'horizontal' line_style LineStyle

The line style of the rule.

'solid' name str | None

The name of the widget.

None id str | None

The ID of the widget in the DOM.

None classes str | None

The CSS classes of the widget.

None disabled bool

Whether the widget is disabled or not.

False"},{"location":"widgets/rule/#textual.widgets._rule.Rule.line_style","title":"line_style instance-attribute class-attribute","text":"
line_style: Reactive[LineStyle] = line_style\n

The line style of the rule.

"},{"location":"widgets/rule/#textual.widgets._rule.Rule.orientation","title":"orientation instance-attribute class-attribute","text":"
orientation: Reactive[RuleOrientation] = orientation\n

The orientation of the rule.

"},{"location":"widgets/rule/#textual.widgets._rule.Rule.horizontal","title":"horizontal classmethod","text":"
def horizontal(\ncls,\nline_style=\"solid\",\nname=None,\nid=None,\nclasses=None,\ndisabled=False,\n):\n

Utility constructor for creating a horizontal rule.

Parameters Name Type Description Default line_style LineStyle

The line style of the rule.

'solid' name str | None

The name of the widget.

None id str | None

The ID of the widget in the DOM.

None classes str | None

The CSS classes of the widget.

None disabled bool

Whether the widget is disabled or not.

False Returns Type Description Rule

A rule widget with horizontal orientation.

"},{"location":"widgets/rule/#textual.widgets._rule.Rule.vertical","title":"vertical classmethod","text":"
def vertical(\ncls,\nline_style=\"solid\",\nname=None,\nid=None,\nclasses=None,\ndisabled=False,\n):\n

Utility constructor for creating a vertical rule.

Parameters Name Type Description Default line_style LineStyle

The line style of the rule.

'solid' name str | None

The name of the widget.

None id str | None

The ID of the widget in the DOM.

None classes str | None

The CSS classes of the widget.

None disabled bool

Whether the widget is disabled or not.

False Returns Type Description Rule

A rule widget with vertical orientation.

"},{"location":"widgets/rule/#textual.widgets.rule","title":"textual.widgets.rule","text":""},{"location":"widgets/rule/#textual.widgets.rule.LineStyle","title":"LineStyle module-attribute","text":"
LineStyle = Literal[\n\"ascii\",\n\"blank\",\n\"dashed\",\n\"double\",\n\"heavy\",\n\"hidden\",\n\"none\",\n\"solid\",\n\"thick\",\n]\n

The valid line styles of the rule widget.

"},{"location":"widgets/rule/#textual.widgets.rule.RuleOrientation","title":"RuleOrientation module-attribute","text":"
RuleOrientation = Literal['horizontal', 'vertical']\n

The valid orientations of the rule widget.

"},{"location":"widgets/rule/#textual.widgets.rule.InvalidLineStyle","title":"InvalidLineStyle class","text":"

Bases: Exception

Exception raised for an invalid rule line style.

"},{"location":"widgets/rule/#textual.widgets.rule.InvalidRuleOrientation","title":"InvalidRuleOrientation class","text":"

Bases: Exception

Exception raised for an invalid rule orientation.

"},{"location":"widgets/select/","title":"Select","text":"

Added in version 0.24.0

A Select widget is a compact control to allow the user to select between a number of possible options.

  • Focusable
  • Container

The options in a select control may be passed in to the constructor or set later with set_options. Options should be given as a sequence of tuples consisting of two values: the first is the string (or Rich Renderable) to display in the control and list of options, the second is the value of option.

The value of the currently selected option is stored in the value attribute of the widget, and the value attribute of the Changed message.

"},{"location":"widgets/select/#typing","title":"Typing","text":"

The Select control is a typing Generic which allows you to set the type of the option values. For instance, if the data type for your values is an integer, you would type the widget as follows:

options = [(\"First\", 1), (\"Second\", 2)]\nmy_select: Select[int] =  Select(options)\n

Note

Typing is entirely optional.

If you aren't familiar with typing or don't want to worry about it right now, feel free to ignore it.

"},{"location":"widgets/select/#example","title":"Example","text":"

The following example presents a Select with a number of options.

OutputOutput (expanded)select_widget.pyselect.tcss

SelectApp \u2b58SelectApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aSelect\u25bc\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

SelectApp \u2b58SelectApp \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aSelect\u25b2\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258aSelect\u258e \u258aI\u00a0must\u00a0not\u00a0fear.\u258e \u258aFear\u00a0is\u00a0the\u00a0mind-killer.\u258e \u258aFear\u00a0is\u00a0the\u00a0little-death\u00a0that\u00a0brings\u00a0total\u00a0\u258e \u258aobliteration.\u258e \u258aI\u00a0will\u00a0face\u00a0my\u00a0fear.\u258e \u258aI\u00a0will\u00a0permit\u00a0it\u00a0to\u00a0pass\u00a0over\u00a0me\u00a0and\u00a0through\u00a0me.\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

from textual import on\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Header, Select\nLINES = \"\"\"I must not fear.\nFear is the mind-killer.\nFear is the little-death that brings total obliteration.\nI will face my fear.\nI will permit it to pass over me and through me.\"\"\".splitlines()\nclass SelectApp(App):\nCSS_PATH = \"select.tcss\"\ndef compose(self) -> ComposeResult:\nyield Header()\nyield Select((line, line) for line in LINES)\n@on(Select.Changed)\ndef select_changed(self, event: Select.Changed) -> None:\nself.title = str(event.value)\nif __name__ == \"__main__\":\napp = SelectApp()\napp.run()\n
Screen {\nalign: center top;\n}\nSelect {\nwidth: 60;\nmargin: 2;\n}\n
"},{"location":"widgets/select/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description expanded bool False True to expand the options overlay. value SelectType | None None Current value of the Select."},{"location":"widgets/select/#messages","title":"Messages","text":"
  • Select.Changed
"},{"location":"widgets/select/#bindings","title":"Bindings","text":"

The Select widget defines the following bindings:

Key(s) Description enter,down,space,up Activate the overlay"},{"location":"widgets/select/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/select/#textual.widgets.Select","title":"textual.widgets.Select class","text":"
def __init__(\nself,\noptions,\n*,\nprompt=\"Select\",\nallow_blank=True,\nvalue=None,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: Generic[SelectType], Vertical

Widget to select from a list of possible options.

A Select displays the current selection. When activated with Enter the widget displays an overlay with a list of all possible options.

Parameters Name Type Description Default options Iterable[tuple[str, SelectType]]

Options to select from.

required prompt str

Text to show in the control when no option is select.

'Select' allow_blank bool

Allow the selection of a blank option.

True value SelectType | None

Initial value (should be one of the values in options).

None name str | None

The name of the select control.

None id str | None

The ID of the control the DOM.

None classes str | None

The CSS classes of the control.

None disabled bool

Whether the control is disabled or not.

False"},{"location":"widgets/select/#textual.widgets._select.Select.BINDINGS","title":"BINDINGS instance-attribute class-attribute","text":"
BINDINGS = [('enter,down,space,up', 'show_overlay')]\n
Key(s) Description enter,down,space,up Activate the overlay"},{"location":"widgets/select/#textual.widgets._select.Select.expanded","title":"expanded instance-attribute class-attribute","text":"
expanded: var[bool] = var(False, init=False)\n

True to show the overlay, otherwise False.

"},{"location":"widgets/select/#textual.widgets._select.Select.prompt","title":"prompt instance-attribute class-attribute","text":"
prompt: var[str] = prompt\n

The prompt to show when no value is selected.

"},{"location":"widgets/select/#textual.widgets._select.Select.value","title":"value instance-attribute class-attribute","text":"
value: var[SelectType | None] = var[Optional[SelectType]](\nNone\n)\n

The value of the select.

"},{"location":"widgets/select/#textual.widgets._select.Select.Changed","title":"Changed class","text":"
def __init__(self, select, value):\n

Bases: Message

Posted when the select value was changed.

This message can be handled using a on_select_changed method.

"},{"location":"widgets/select/#textual.widgets._select.Select.Changed.control","title":"control property","text":"
control: Select\n

The Select that sent the message.

"},{"location":"widgets/select/#textual.widgets._select.Select.Changed.select","title":"select instance-attribute","text":"
select = select\n

The select widget.

"},{"location":"widgets/select/#textual.widgets._select.Select.Changed.value","title":"value instance-attribute","text":"
value = value\n

The value of the Select when it changed.

"},{"location":"widgets/select/#textual.widgets._select.Select.action_show_overlay","title":"action_show_overlay method","text":"
def action_show_overlay(self):\n

Show the overlay.

"},{"location":"widgets/select/#textual.widgets._select.Select.set_options","title":"set_options method","text":"
def set_options(self, options):\n

Set the options for the Select.

Parameters Name Type Description Default options Iterable[tuple[RenderableType, SelectType]]

An iterable of tuples containing (STRING, VALUE).

required"},{"location":"widgets/selection_list/","title":"SelectionList","text":"

Added in version 0.27.0

A widget for showing a vertical list of selectable options.

  • Focusable
  • Container
"},{"location":"widgets/selection_list/#typing","title":"Typing","text":"

The SelectionList control is a Generic, which allows you to set the type of the selection values. For instance, if the data type for your values is an integer, you would type the widget as follows:

selections = [(\"First\", 1), (\"Second\", 2)]\nmy_selection_list: SelectionList[int] =  SelectionList(*selections)\n

Note

Typing is entirely optional.

If you aren't familiar with typing or don't want to worry about it right now, feel free to ignore it.

"},{"location":"widgets/selection_list/#examples","title":"Examples","text":"

A selection list is designed to be built up of single-line prompts (which can be Rich renderables) and an associated unique value.

"},{"location":"widgets/selection_list/#selections-as-tuples","title":"Selections as tuples","text":"

A selection list can be built with tuples, either of two or three values in length. Each tuple must contain a prompt and a value, and it can also optionally contain a flag for the initial selected state of the option.

Outputselection_list_tuples.pyselection_list.tcss

SelectionListApp \u2b58SelectionListApp \u250c\u2500\u00a0Shall\u00a0we\u00a0play\u00a0some\u00a0games?\u00a0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502 \u2502\u2590X\u258cFalken's\u00a0Maze\u2502 \u2502\u2590X\u258cBlack\u00a0Jack\u2502 \u2502\u2590X\u258cGin\u00a0Rummy\u2502 \u2502\u2590X\u258cHearts\u2502 \u2502\u2590X\u258cBridge\u2502 \u2502\u2590X\u258cCheckers\u2502 \u2502\u2590X\u258cChess\u2502 \u2502\u2590X\u258cPoker\u2502 \u2502\u2590X\u258cFighter\u00a0Combat\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

from textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Header, SelectionList\nclass SelectionListApp(App[None]):\nCSS_PATH = \"selection_list.tcss\"\ndef compose(self) -> ComposeResult:\nyield Header()\nyield SelectionList[int](  # (1)!\n(\"Falken's Maze\", 0, True),\n(\"Black Jack\", 1),\n(\"Gin Rummy\", 2),\n(\"Hearts\", 3),\n(\"Bridge\", 4),\n(\"Checkers\", 5),\n(\"Chess\", 6, True),\n(\"Poker\", 7),\n(\"Fighter Combat\", 8, True),\n)\nyield Footer()\ndef on_mount(self) -> None:\nself.query_one(SelectionList).border_title = \"Shall we play some games?\"\nif __name__ == \"__main__\":\nSelectionListApp().run()\n
  1. Note that the SelectionList is typed as int, for the type of the values.
Screen {\nalign: center middle;\n}\nSelectionList {\npadding: 1;\nborder: solid $accent;\nwidth: 80%;\nheight: 80%;\n}\n
"},{"location":"widgets/selection_list/#selections-as-selection-objects","title":"Selections as Selection objects","text":"

Alternatively, selections can be passed in as Selections:

Outputselection_list_selections.pyselection_list.tcss

SelectionListApp \u2b58SelectionListApp \u250c\u2500\u00a0Shall\u00a0we\u00a0play\u00a0some\u00a0games?\u00a0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502 \u2502\u2590X\u258cFalken's\u00a0Maze\u2502 \u2502\u2590X\u258cBlack\u00a0Jack\u2502 \u2502\u2590X\u258cGin\u00a0Rummy\u2502 \u2502\u2590X\u258cHearts\u2502 \u2502\u2590X\u258cBridge\u2502 \u2502\u2590X\u258cCheckers\u2502 \u2502\u2590X\u258cChess\u2502 \u2502\u2590X\u258cPoker\u2502 \u2502\u2590X\u258cFighter\u00a0Combat\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

from textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Header, SelectionList\nfrom textual.widgets.selection_list import Selection\nclass SelectionListApp(App[None]):\nCSS_PATH = \"selection_list.tcss\"\ndef compose(self) -> ComposeResult:\nyield Header()\nyield SelectionList[int](  # (1)!\nSelection(\"Falken's Maze\", 0, True),\nSelection(\"Black Jack\", 1),\nSelection(\"Gin Rummy\", 2),\nSelection(\"Hearts\", 3),\nSelection(\"Bridge\", 4),\nSelection(\"Checkers\", 5),\nSelection(\"Chess\", 6, True),\nSelection(\"Poker\", 7),\nSelection(\"Fighter Combat\", 8, True),\n)\nyield Footer()\ndef on_mount(self) -> None:\nself.query_one(SelectionList).border_title = \"Shall we play some games?\"\nif __name__ == \"__main__\":\nSelectionListApp().run()\n
  1. Note that the SelectionList is typed as int, for the type of the values.
Screen {\nalign: center middle;\n}\nSelectionList {\npadding: 1;\nborder: solid $accent;\nwidth: 80%;\nheight: 80%;\n}\n
"},{"location":"widgets/selection_list/#handling-changes-to-the-selections","title":"Handling changes to the selections","text":"

Most of the time, when using the SelectionList, you will want to know when the collection of selected items has changed; this is ideally done using the SelectedChanged message. Here is an example of using that message to update a Pretty with the collection of selected values:

Outputselection_list_selections.pyselection_list.tcss

SelectionListApp \u2b58SelectionListApp \u250c\u2500\u00a0Shall\u00a0we\u00a0play\u00a0some\u00a0games?\u00a0\u2500\u2500\u2510\u250c\u2500\u00a0Selected\u00a0games\u00a0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\u2502\u2502[\u2502 \u2502\u2590X\u258cFalken's\u00a0Maze\u2502\u2502'secret_back_door',\u2502 \u2502\u2590X\u258cBlack\u00a0Jack\u2502\u2502'a_nice_game_of_chess',\u2502 \u2502\u2590X\u258cGin\u00a0Rummy\u2502\u2502'fighter_combat'\u2502 \u2502\u2590X\u258cHearts\u2502\u2502]\u2502 \u2502\u2590X\u258cBridge\u2502\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502\u2590X\u258cCheckers\u2502 \u2502\u2590X\u258cChess\u2502 \u2502\u2590X\u258cPoker\u2502 \u2502\u2590X\u258cFighter\u00a0Combat\u2502 \u2502\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

from textual import on\nfrom textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.events import Mount\nfrom textual.widgets import Footer, Header, Pretty, SelectionList\nfrom textual.widgets.selection_list import Selection\nclass SelectionListApp(App[None]):\nCSS_PATH = \"selection_list_selected.tcss\"\ndef compose(self) -> ComposeResult:\nyield Header()\nwith Horizontal():\nyield SelectionList[str](  # (1)!\nSelection(\"Falken's Maze\", \"secret_back_door\", True),\nSelection(\"Black Jack\", \"black_jack\"),\nSelection(\"Gin Rummy\", \"gin_rummy\"),\nSelection(\"Hearts\", \"hearts\"),\nSelection(\"Bridge\", \"bridge\"),\nSelection(\"Checkers\", \"checkers\"),\nSelection(\"Chess\", \"a_nice_game_of_chess\", True),\nSelection(\"Poker\", \"poker\"),\nSelection(\"Fighter Combat\", \"fighter_combat\", True),\n)\nyield Pretty([])\nyield Footer()\ndef on_mount(self) -> None:\nself.query_one(SelectionList).border_title = \"Shall we play some games?\"\nself.query_one(Pretty).border_title = \"Selected games\"\n@on(Mount)\n@on(SelectionList.SelectedChanged)\ndef update_selected_view(self) -> None:\nself.query_one(Pretty).update(self.query_one(SelectionList).selected)\nif __name__ == \"__main__\":\nSelectionListApp().run()\n
  1. Note that the SelectionList is typed as str, for the type of the values.
Screen {\nalign: center middle;\n}\nHorizontal {\nwidth: 80%;\nheight: 80%;\n}\nSelectionList {\npadding: 1;\nborder: solid $accent;\nwidth: 1fr;\n}\nPretty {\nwidth: 1fr;\nborder: solid $accent;\n}\n
"},{"location":"widgets/selection_list/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description highlighted int | None None The index of the highlighted selection. None means nothing is highlighted."},{"location":"widgets/selection_list/#messages","title":"Messages","text":"

The following messages will be posted as the user interacts with the list:

  • SelectionList.SelectionHighlighted
  • SelectionList.SelectionToggled

The following message will be posted if the content of selected changes, either by user interaction or by API calls:

  • SelectionList.SelectedChanged
"},{"location":"widgets/selection_list/#bindings","title":"Bindings","text":"

The selection list widget defines the following bindings:

Key(s) Description space Toggle the state of the highlighted selection.

It inherits from OptionList and so also inherits the following bindings:

Key(s) Description down Move the highlight down. end Move the highlight to the last option. enter Select the current option. home Move the highlight to the first option. pagedown Move the highlight down a page of options. pageup Move the highlight up a page of options. up Move the highlight up."},{"location":"widgets/selection_list/#component-classes","title":"Component Classes","text":"

The selection list provides the following component classes:

Class Description selection-list--button Target the default button style. selection-list--button-selected Target a selected button style. selection-list--button-highlighted Target a highlighted button style. selection-list--button-selected-highlighted Target a highlighted selected button style.

It inherits from OptionList and so also makes use of the following component classes:

Class Description option-list--option-disabled Target disabled options. option-list--option-highlighted Target the highlighted option. option-list--option-highlighted-disabled Target a disabled option that is also highlighted. option-list--option-hover Target an option that has the mouse over it. option-list--option-hover-disabled Target a disabled option that has the mouse over it. option-list--option-hover-highlighted Target a highlighted option that has the mouse over it. option-list--option-hover-highlighted-disabled Target a disabled highlighted option that has the mouse over it. option-list--separator Target the separators."},{"location":"widgets/selection_list/#textual.widgets.SelectionList","title":"textual.widgets.SelectionList class","text":"
def __init__(\nself,\n*selections,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: Generic[SelectionType], OptionList

A vertical selection list that allows making multiple selections.

Parameters Name Type Description Default *selections Selection | tuple[TextType, SelectionType] | tuple[TextType, SelectionType, bool]

The content for the selection list.

() name str | None

The name of the selection list.

None id str | None

The ID of the selection list in the DOM.

None classes str | None

The CSS classes of the selection list.

None disabled bool

Whether the selection list is disabled or not.

False"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.BINDINGS","title":"BINDINGS instance-attribute class-attribute","text":"
BINDINGS = [Binding('space', 'select')]\n
Key(s) Description space Toggle the state of the highlighted selection."},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
COMPONENT_CLASSES: set[str] = {\n\"selection-list--button\",\n\"selection-list--button-selected\",\n\"selection-list--button-highlighted\",\n\"selection-list--button-selected-highlighted\",\n}\n
Class Description selection-list--button Target the default button style. selection-list--button-selected Target a selected button style. selection-list--button-highlighted Target a highlighted button style. selection-list--button-selected-highlighted Target a highlighted selected button style."},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.selected","title":"selected property","text":"
selected: list[SelectionType]\n

The selected values.

This is a list of all of the values associated with selections in the list that are currently in the selected state.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectedChanged","title":"SelectedChanged class","text":"

Bases: Generic[MessageSelectionType], Message

Message sent when the collection of selected values changes.

This message is sent when any change to the collection of selected values takes place; either by user interaction or by API calls.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectedChanged.control","title":"control property","text":"
control: SelectionList[MessageSelectionType]\n

An alias for selection_list.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectedChanged.selection_list","title":"selection_list instance-attribute","text":"
selection_list: SelectionList[MessageSelectionType]\n

The SelectionList that sent the message.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectionHighlighted","title":"SelectionHighlighted class","text":"

Bases: SelectionMessage

Message sent when a selection is highlighted.

Can be handled using on_selection_list_selection_highlighted in a subclass of SelectionList or in a parent node in the DOM.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectionMessage","title":"SelectionMessage class","text":"
def __init__(self, selection_list, index):\n

Bases: Generic[MessageSelectionType], Message

Base class for all selection messages.

Parameters Name Type Description Default selection_list SelectionList

The selection list that owns the selection.

required index int

The index of the selection that the message relates to.

required"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectionMessage.control","title":"control property","text":"
control: OptionList\n

The selection list that sent the message.

This is an alias for SelectionMessage.selection_list and is used by the on decorator.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectionMessage.selection","title":"selection instance-attribute","text":"
selection: Selection[\nMessageSelectionType\n] = selection_list.get_option_at_index(index)\n

The highlighted selection.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectionMessage.selection_index","title":"selection_index instance-attribute","text":"
selection_index: int = index\n

The index of the selection that the message relates to.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectionMessage.selection_list","title":"selection_list instance-attribute","text":"
selection_list: SelectionList[\nMessageSelectionType\n] = selection_list\n

The selection list that sent the message.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.SelectionToggled","title":"SelectionToggled class","text":"

Bases: SelectionMessage

Message sent when a selection is toggled.

Can be handled using on_selection_list_selection_toggled in a subclass of SelectionList or in a parent node in the DOM.

Note

This message is only sent if the selection is toggled by user interaction. See SelectedChanged for a message sent when any change (selected or deselected, either by user interaction or by API calls) is made to the selected values.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.add_option","title":"add_option method","text":"
def add_option(self, item=None):\n

Add a new selection option to the end of the list.

Parameters Name Type Description Default item NewOptionListContent | Selection | tuple[TextType, SelectionType] | tuple[TextType, SelectionType, bool]

The new item to add.

None Returns Type Description Self

The SelectionList instance.

Raises Type Description DuplicateID

If there is an attempt to use a duplicate ID.

SelectionError

If the selection option is of the wrong form.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.add_options","title":"add_options method","text":"
def add_options(self, items):\n

Add new selection options to the end of the list.

Parameters Name Type Description Default items Iterable[NewOptionListContent | Selection | tuple[TextType, SelectionType] | tuple[TextType, SelectionType, bool]]

The new items to add.

required Returns Type Description Self

The SelectionList instance.

Raises Type Description DuplicateID

If there is an attempt to use a duplicate ID.

SelectionError

If one of the selection options is of the wrong form.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.clear_options","title":"clear_options method","text":"
def clear_options(self):\n

Clear the content of the selection list.

Returns Type Description Self

The SelectionList instance.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.deselect","title":"deselect method","text":"
def deselect(self, selection):\n

Mark the given selection as not selected.

Parameters Name Type Description Default selection Selection[SelectionType] | SelectionType

The selection to mark as not selected.

required Returns Type Description Self

The SelectionList instance.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.deselect_all","title":"deselect_all method","text":"
def deselect_all(self):\n

Deselect all items.

Returns Type Description Self

The SelectionList instance.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.get_option","title":"get_option method","text":"
def get_option(self, option_id):\n

Get the selection option with the given ID.

Parameters Name Type Description Default option_id str

The ID of the selection option to get.

required Returns Type Description Selection[SelectionType]

The selection option with the ID.

Raises Type Description OptionDoesNotExist

If no selection option has the given ID.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.get_option_at_index","title":"get_option_at_index method","text":"
def get_option_at_index(self, index):\n

Get the selection option at the given index.

Parameters Name Type Description Default index int

The index of the selection option to get.

required Returns Type Description Selection[SelectionType]

The selection option at that index.

Raises Type Description OptionDoesNotExist

If there is no selection option with the index.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.select","title":"select method","text":"
def select(self, selection):\n

Mark the given selection as selected.

Parameters Name Type Description Default selection Selection[SelectionType] | SelectionType

The selection to mark as selected.

required Returns Type Description Self

The SelectionList instance.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.select_all","title":"select_all method","text":"
def select_all(self):\n

Select all items.

Returns Type Description Self

The SelectionList instance.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.toggle","title":"toggle method","text":"
def toggle(self, selection):\n

Toggle the selected state of the given selection.

Parameters Name Type Description Default selection Selection[SelectionType] | SelectionType

The selection to toggle.

required Returns Type Description Self

The SelectionList instance.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.SelectionList.toggle_all","title":"toggle_all method","text":"
def toggle_all(self):\n

Toggle all items.

Returns Type Description Self

The SelectionList instance.

"},{"location":"widgets/selection_list/#textual.widgets.selection_list.MessageSelectionType","title":"MessageSelectionType module-attribute","text":"
MessageSelectionType = TypeVar('MessageSelectionType')\n

The type for the value of a Selection in a SelectionList message.

"},{"location":"widgets/selection_list/#textual.widgets.selection_list.SelectionType","title":"SelectionType module-attribute","text":"
SelectionType = TypeVar('SelectionType')\n

The type for the value of a Selection in a SelectionList

"},{"location":"widgets/selection_list/#textual.widgets.selection_list.Selection","title":"Selection class","text":"
def __init__(\nself,\nprompt,\nvalue,\ninitial_state=False,\nid=None,\ndisabled=False,\n):\n

Bases: Generic[SelectionType], Option

A selection for a SelectionList.

Parameters Name Type Description Default prompt TextType

The prompt for the selection.

required value SelectionType

The value for the selection.

required initial_state bool

The initial selected state of the selection.

False id str | None

The optional ID for the selection.

None disabled bool

The initial enabled/disabled state. Enabled by default.

False"},{"location":"widgets/selection_list/#textual.widgets._selection_list.Selection.initial_state","title":"initial_state property","text":"
initial_state: bool\n

The initial selected state for the selection.

"},{"location":"widgets/selection_list/#textual.widgets._selection_list.Selection.value","title":"value property","text":"
value: SelectionType\n

The value for this selection.

"},{"location":"widgets/selection_list/#textual.widgets.selection_list.SelectionError","title":"SelectionError class","text":"

Bases: TypeError

Type of an error raised if a selection is badly-formed.

"},{"location":"widgets/sparkline/","title":"Sparkline","text":"

Added in version 0.27.0

A widget that is used to visually represent numerical data.

  • Focusable
  • Container
"},{"location":"widgets/sparkline/#examples","title":"Examples","text":""},{"location":"widgets/sparkline/#basic-example","title":"Basic example","text":"

The example below illustrates the relationship between the data, its length, the width of the sparkline, and the number of bars displayed.

Tip

The sparkline data is split into equally-sized chunks. Each chunk is represented by a bar and the width of the sparkline dictates how many bars there are.

Outputsparkline_basic.pysparkline_basic.tcss

SparklineBasicApp \u2582\u2584\u2588

from textual.app import App, ComposeResult\nfrom textual.widgets import Sparkline\ndata = [1, 2, 2, 1, 1, 4, 3, 1, 1, 8, 8, 2]  # (1)!\nclass SparklineBasicApp(App[None]):\nCSS_PATH = \"sparkline_basic.tcss\"\ndef compose(self) -> ComposeResult:\nyield Sparkline(  # (2)!\ndata,  # (3)!\nsummary_function=max,  # (4)!\n)\napp = SparklineBasicApp()\nif __name__ == \"__main__\":\napp.run()\n
  1. We have 12 data points.
  2. This sparkline will have its width set to 3 via CSS.
  3. The data (12 numbers) will be split across 3 bars, so 4 data points are associated with each bar.
  4. Each bar will represent its largest value. The largest value of each chunk is 2, 4, and 8, respectively. That explains why the first bar is half the height of the second and the second bar is half the height of the third.
Screen {\nalign: center middle;\n}\nSparkline {\nwidth: 3;  /* (1)! */\nmargin: 2;\n}\n
  1. By setting the width to 3 we get three buckets.
"},{"location":"widgets/sparkline/#different-summary-functions","title":"Different summary functions","text":"

The example below shows a sparkline widget with different summary functions. The summary function is what determines the height of each bar.

Outputsparkline.pysparkline.tcss

SparklineSummaryFunctionApp \u2582\u2584\u2582\u2584\u2583\u2583\u2586\u2585\u2583\u2582\u2583\u2582\u2583\u2582\u2584\u2587\u2583\u2583\u2587\u2585\u2584\u2583\u2584\u2584\u2583\u2582\u2583\u2582\u2583\u2584\u2584\u2588\u2586\u2582\u2583\u2583\u2585\u2583\u2583\u2584\u2583\u2587\u2583\u2583\u2583\u2584\u2584\u2586\u2583\u2583\u2585\u2582\u2585\u2583\u2584\u2583\u2583\u2584\u2583\u2585\u2586\u2582\u2582\u2583\u2586\u2582\u2583\u2584\u2585\u2584\u2583\u2584\u2584\u2581\u2583\u2582 \u2581\u2582\u2581\u2582\u2581\u2581\u2582\u2582\u2581\u2581\u2581\u2581\u2582\u2581\u2581\u2582\u2581\u2581\u2582\u2582\u2582\u2582\u2582\u2582\u2581\u2581\u2581\u2581\u2581\u2582\u2581\u2582\u2582\u2581\u2581\u2581\u2582\u2581\u2581\u2582\u2581\u2582\u2581\u2581\u2581\u2581\u2582\u2582\u2581\u2581\u2581\u2581\u2582\u2581\u2581\u2582\u2581\u2582\u2581\u2581\u2582\u2581\u2581\u2581\u2581\u2581\u2581\u2582\u2582\u2582\u2581\u2582\u2581\u2581\u2581\u2581 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581

import random\nfrom statistics import mean\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Sparkline\nrandom.seed(73)\ndata = [random.expovariate(1 / 3) for _ in range(1000)]\nclass SparklineSummaryFunctionApp(App[None]):\nCSS_PATH = \"sparkline.tcss\"\ndef compose(self) -> ComposeResult:\nyield Sparkline(data, summary_function=max)  # (1)!\nyield Sparkline(data, summary_function=mean)  # (2)!\nyield Sparkline(data, summary_function=min)  # (3)!\napp = SparklineSummaryFunctionApp()\nif __name__ == \"__main__\":\napp.run()\n
  1. Each bar will show the largest value of that bucket.
  2. Each bar will show the mean value of that bucket.
  3. Each bar will show the smaller value of that bucket.
Sparkline {\nwidth: 100%;\nmargin: 2;\n}\n
"},{"location":"widgets/sparkline/#changing-the-colors","title":"Changing the colors","text":"

The example below shows how to use component classes to change the colors of the sparkline.

Outputsparkline_colors.pysparkline_colors.tcss

SparklineColorsApp \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582 \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582 \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582 \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582 \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582 \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582 \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582 \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582 \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582 \u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2583\u2583\u2584\u2585\u2586\u2586\u2586\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2584\u2584\u2583\u2582\u2581\u2581\u2582\u2582\u2583\u2584\u2585\u2585\u2586\u2586\u2587\u2587\u2587\u2587\u2588\u2587\u2587\u2587\u2587\u2586\u2586\u2585\u2585\u2584\u2583\u2582

from math import sin\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import Sparkline\nclass SparklineColorsApp(App[None]):\nCSS_PATH = \"sparkline_colors.tcss\"\ndef compose(self) -> ComposeResult:\nnums = [abs(sin(x / 3.14)) for x in range(0, 360 * 6, 20)]\nyield Sparkline(nums, summary_function=max, id=\"fst\")\nyield Sparkline(nums, summary_function=max, id=\"snd\")\nyield Sparkline(nums, summary_function=max, id=\"trd\")\nyield Sparkline(nums, summary_function=max, id=\"frt\")\nyield Sparkline(nums, summary_function=max, id=\"fft\")\nyield Sparkline(nums, summary_function=max, id=\"sxt\")\nyield Sparkline(nums, summary_function=max, id=\"svt\")\nyield Sparkline(nums, summary_function=max, id=\"egt\")\nyield Sparkline(nums, summary_function=max, id=\"nnt\")\nyield Sparkline(nums, summary_function=max, id=\"tnt\")\napp = SparklineColorsApp()\nif __name__ == \"__main__\":\napp.run()\n
Sparkline {\nwidth: 100%;\nmargin: 1;\n}\n#fst > .sparkline--max-color {\ncolor: $success;\n}\n#fst > .sparkline--min-color {\ncolor: $warning;\n}\n#snd > .sparkline--max-color {\ncolor: $warning;\n}\n#snd > .sparkline--min-color {\ncolor: $success;\n}\n#trd > .sparkline--max-color {\ncolor: $error;\n}\n#trd > .sparkline--min-color {\ncolor: $warning;\n}\n#frt > .sparkline--max-color {\ncolor: $warning;\n}\n#frt > .sparkline--min-color {\ncolor: $error;\n}\n#fft > .sparkline--max-color {\ncolor: $accent;\n}\n#fft > .sparkline--min-color {\ncolor: $accent 30%;\n}\n#sxt > .sparkline--max-color {\ncolor: $accent 30%;\n}\n#sxt > .sparkline--min-color {\ncolor: $accent;\n}\n#svt > .sparkline--max-color {\ncolor: $error;\n}\n#svt > .sparkline--min-color {\ncolor: $error 30%;\n}\n#egt > .sparkline--max-color {\ncolor: $error 30%;\n}\n#egt > .sparkline--min-color {\ncolor: $error;\n}\n#nnt > .sparkline--max-color {\ncolor: $success;\n}\n#nnt > .sparkline--min-color {\ncolor: $success 30%;\n}\n#tnt > .sparkline--max-color {\ncolor: $success 30%;\n}\n#tnt > .sparkline--min-color {\ncolor: $success;\n}\n
"},{"location":"widgets/sparkline/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description data Sequence[float] | None None The data represented by the sparkline. summary_function Callable[[Sequence[float]], float] max The function that computes the height of each bar."},{"location":"widgets/sparkline/#messages","title":"Messages","text":"

This widget posts no messages.

"},{"location":"widgets/sparkline/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/sparkline/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/sparkline/#textual.widgets.Sparkline","title":"textual.widgets.Sparkline class","text":"
def __init__(\nself,\ndata=None,\n*,\nsummary_function=None,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: Widget

A sparkline widget to display numerical data.

Parameters Name Type Description Default data Sequence[float] | None

The initial data to populate the sparkline with.

None summary_function Callable[[Sequence[float]], float] | None

Summarises bar values into a single value used to represent each bar.

None name str | None

The name of the widget.

None id str | None

The ID of the widget in the DOM.

None classes str | None

The CSS classes for the widget.

None disabled bool

Whether the widget is disabled or not.

False"},{"location":"widgets/sparkline/#textual.widgets._sparkline.Sparkline.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
COMPONENT_CLASSES: set[str] = {\n\"sparkline--max-color\",\n\"sparkline--min-color\",\n}\n

Use these component classes to define the two colors that the sparkline interpolates to represent its numerical data.

Note

These two component classes are used exclusively for the color of the sparkline widget. Setting any style other than color will have no effect.

Class Description sparkline--max-color The color used for the larger values in the data. sparkline--min-color The colour used for the smaller values in the data."},{"location":"widgets/sparkline/#textual.widgets._sparkline.Sparkline.data","title":"data instance-attribute class-attribute","text":"
data = data\n

The data that populates the sparkline.

"},{"location":"widgets/sparkline/#textual.widgets._sparkline.Sparkline.summary_function","title":"summary_function instance-attribute class-attribute","text":"
summary_function = reactive[\nCallable[[Sequence[float]], float]\n](_max_factory)\n

The function that computes the value that represents each bar.

"},{"location":"widgets/static/","title":"Static","text":"

A widget which displays static content. Can be used for Rich renderables and can also be the base for other types of widgets.

  • Focusable
  • Container
"},{"location":"widgets/static/#example","title":"Example","text":"

The example below shows how you can use a Static widget as a simple text label (but see Label as a way of displaying text).

Outputstatic.py

StaticApp Hello,\u00a0world!

from textual.app import App, ComposeResult\nfrom textual.widgets import Static\nclass StaticApp(App):\ndef compose(self) -> ComposeResult:\nyield Static(\"Hello, world!\")\nif __name__ == \"__main__\":\napp = StaticApp()\napp.run()\n
"},{"location":"widgets/static/#reactive-attributes","title":"Reactive Attributes","text":"

This widget has no reactive attributes.

"},{"location":"widgets/static/#messages","title":"Messages","text":"

This widget posts no messages.

"},{"location":"widgets/static/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/static/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/static/#see-also","title":"See Also","text":"
  • Label
  • Pretty
"},{"location":"widgets/static/#textual.widgets.Static","title":"textual.widgets.Static class","text":"
def __init__(\nself,\nrenderable=\"\",\n*,\nexpand=False,\nshrink=False,\nmarkup=True,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: Widget

A widget to display simple static content, or use as a base class for more complex widgets.

Parameters Name Type Description Default renderable RenderableType

A Rich renderable, or string containing console markup.

'' expand bool

Expand content if required to fill container.

False shrink bool

Shrink content if required to fill container.

False markup bool

True if markup should be parsed and rendered.

True name str | None

Name of widget.

None id str | None

ID of Widget.

None classes str | None

Space separated list of class names.

None disabled bool

Whether the static is disabled or not.

False"},{"location":"widgets/static/#textual.widgets._static.Static.update","title":"update method","text":"
def update(self, renderable=''):\n

Update the widget's content area with new text or Rich renderable.

Parameters Name Type Description Default renderable RenderableType

A new rich renderable. Defaults to empty renderable;

''"},{"location":"widgets/switch/","title":"Switch","text":"

A simple switch widget which stores a boolean value.

  • Focusable
  • Container
"},{"location":"widgets/switch/#example","title":"Example","text":"

The example below shows switches in various states.

Outputswitch.pyswitch.tcss

SwitchApp Example\u00a0switches \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e off:\u00a0\u00a0\u00a0\u00a0\u00a0\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e on:\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e focused:\u00a0\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e custom:\u00a0\u00a0\u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e

from textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.widgets import Static, Switch\nclass SwitchApp(App):\ndef compose(self) -> ComposeResult:\nyield Static(\"[b]Example switches\\n\", classes=\"label\")\nyield Horizontal(\nStatic(\"off:     \", classes=\"label\"),\nSwitch(animate=False),\nclasses=\"container\",\n)\nyield Horizontal(\nStatic(\"on:      \", classes=\"label\"),\nSwitch(value=True),\nclasses=\"container\",\n)\nfocused_switch = Switch()\nfocused_switch.focus()\nyield Horizontal(\nStatic(\"focused: \", classes=\"label\"), focused_switch, classes=\"container\"\n)\nyield Horizontal(\nStatic(\"custom:  \", classes=\"label\"),\nSwitch(id=\"custom-design\"),\nclasses=\"container\",\n)\napp = SwitchApp(css_path=\"switch.tcss\")\nif __name__ == \"__main__\":\napp.run()\n
Screen {\nalign: center middle;\n}\n.container {\nheight: auto;\nwidth: auto;\n}\nSwitch {\nheight: auto;\nwidth: auto;\n}\n.label {\nheight: 3;\ncontent-align: center middle;\nwidth: auto;\n}\n#custom-design {\nbackground: darkslategrey;\n}\n#custom-design > .switch--slider {\ncolor: dodgerblue;\nbackground: darkslateblue;\n}\n
"},{"location":"widgets/switch/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description value bool False The value of the switch."},{"location":"widgets/switch/#messages","title":"Messages","text":"
  • Switch.Changed
"},{"location":"widgets/switch/#bindings","title":"Bindings","text":"

The switch widget defines the following bindings:

Key(s) Description enter,space Toggle the switch state."},{"location":"widgets/switch/#component-classes","title":"Component Classes","text":"

The switch widget provides the following component classes:

Class Description switch--slider Targets the slider of the switch."},{"location":"widgets/switch/#additional-notes","title":"Additional Notes","text":"
  • To remove the spacing around a Switch, set border: none; and padding: 0;.
"},{"location":"widgets/switch/#textual.widgets.Switch","title":"textual.widgets.Switch class","text":"
def __init__(\nself,\nvalue=False,\n*,\nanimate=True,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: Widget

A switch widget that represents a boolean value.

Can be toggled by clicking on it or through its bindings.

The switch widget also contains component classes that enable more customization.

Parameters Name Type Description Default value bool

The initial value of the switch.

False animate bool

True if the switch should animate when toggled.

True name str | None

The name of the switch.

None id str | None

The ID of the switch in the DOM.

None classes str | None

The CSS classes of the switch.

None disabled bool

Whether the switch is disabled or not.

False"},{"location":"widgets/switch/#textual.widgets._switch.Switch.BINDINGS","title":"BINDINGS class-attribute","text":"
BINDINGS: list[BindingType] = [\nBinding(\"enter,space\", \"toggle\", \"Toggle\", show=False)\n]\n
Key(s) Description enter,space Toggle the switch state."},{"location":"widgets/switch/#textual.widgets._switch.Switch.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
COMPONENT_CLASSES: set[str] = {'switch--slider'}\n
Class Description switch--slider Targets the slider of the switch."},{"location":"widgets/switch/#textual.widgets._switch.Switch.slider_pos","title":"slider_pos instance-attribute class-attribute","text":"
slider_pos = reactive(0.0)\n

The position of the slider.

"},{"location":"widgets/switch/#textual.widgets._switch.Switch.value","title":"value instance-attribute class-attribute","text":"
value = reactive(False, init=False)\n

The value of the switch; True for on and False for off.

"},{"location":"widgets/switch/#textual.widgets._switch.Switch.Changed","title":"Changed class","text":"
def __init__(self, switch, value):\n

Bases: Message

Posted when the status of the switch changes.

Can be handled using on_switch_changed in a subclass of Switch or in a parent widget in the DOM.

Attributes Name Type Description value bool

The value that the switch was changed to.

switch Switch

The Switch widget that was changed.

"},{"location":"widgets/switch/#textual.widgets._switch.Switch.Changed.control","title":"control property","text":"
control: Switch\n

Alias for self.switch.

"},{"location":"widgets/switch/#textual.widgets._switch.Switch.action_toggle","title":"action_toggle method","text":"
def action_toggle(self):\n

Toggle the state of the switch.

"},{"location":"widgets/switch/#textual.widgets._switch.Switch.toggle","title":"toggle method","text":"
def toggle(self):\n

Toggle the switch value.

As a result of the value changing, a Switch.Changed message will be posted.

Returns Type Description Self

The Switch instance.

"},{"location":"widgets/tabbed_content/","title":"TabbedContent","text":"

Added in version 0.16.0

Switch between mutually exclusive content panes via a row of tabs.

  • Focusable
  • Container

This widget combines the Tabs and ContentSwitcher widgets to create a convenient way of navigating content.

Only a single child of TabbedContent is visible at once. Each child has an associated tab which will make it visible and hide the others.

"},{"location":"widgets/tabbed_content/#composing","title":"Composing","text":"

There are two ways to provide the titles for the tab. You can pass them as positional arguments to the TabbedContent constructor:

def compose(self) -> ComposeResult:\nwith TabbedContent(\"Leto\", \"Jessica\", \"Paul\"):\nyield Markdown(LETO)\nyield Markdown(JESSICA)\nyield Markdown(PAUL)\n

Alternatively you can wrap the content in a TabPane widget, which takes the tab title as the first parameter:

def compose(self) -> ComposeResult:\nwith TabbedContent():\nwith TabPane(\"Leto\"):\nyield Markdown(LETO)\nwith TabPane(\"Jessica\"):\nyield Markdown(JESSICA)\nwith TabPane(\"Paul\"):\nyield Markdown(PAUL)\n
"},{"location":"widgets/tabbed_content/#switching-tabs","title":"Switching tabs","text":"

If you need to programmatically switch tabs, you should provide an id attribute to the TabPanes.

def compose(self) -> ComposeResult:\nwith TabbedContent():\nwith TabPane(\"Leto\", id=\"leto\"):\nyield Markdown(LETO)\nwith TabPane(\"Jessica\", id=\"jessica\"):\nyield Markdown(JESSICA)\nwith TabPane(\"Paul\", id=\"paul\"):\nyield Markdown(PAUL)\n

You can then switch tabs by setting the active reactive attribute:

# Switch to Jessica tab\nself.query_one(TabbedContent).active = \"jessica\"\n

Note

If you don't provide id attributes to the tab panes, they will be assigned sequentially starting at tab-1 (then tab-2 etc).

"},{"location":"widgets/tabbed_content/#initial-tab","title":"Initial tab","text":"

The first child of TabbedContent will be the initial active tab by default. You can pick a different initial tab by setting the initial argument to the id of the tab:

def compose(self) -> ComposeResult:\nwith TabbedContent(initial=\"jessica\"):\nwith TabPane(\"Leto\", id=\"leto\"):\nyield Markdown(LETO)\nwith TabPane(\"Jessica\", id=\"jessica\"):\nyield Markdown(JESSICA)\nwith TabPane(\"Paul\", id=\"paul\"):\nyield Markdown(PAUL)\n
"},{"location":"widgets/tabbed_content/#example","title":"Example","text":"

The following example contains a TabbedContent with three tabs.

Outputtabbed_content.py

TabbedApp LetoJessicaPaul \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581 \u258e\u258a \u258eLady\u00a0Jessica\u258a \u258e\u258a \u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594 Bene\u00a0Gesserit\u00a0and\u00a0concubine\u00a0of\u00a0Leto,\u00a0and\u00a0mother\u00a0of\u00a0Paul\u00a0and\u00a0Alia. PaulAlia \u2501\u2578\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 First\u00a0child \u00a0L\u00a0\u00a0Leto\u00a0\u00a0J\u00a0\u00a0Jessica\u00a0\u00a0P\u00a0\u00a0Paul\u00a0

from textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Label, Markdown, TabbedContent, TabPane\nLETO = \"\"\"\n# Duke Leto I Atreides\nHead of House Atreides.\n\"\"\"\nJESSICA = \"\"\"\n# Lady Jessica\nBene Gesserit and concubine of Leto, and mother of Paul and Alia.\n\"\"\"\nPAUL = \"\"\"\n# Paul Atreides\nSon of Leto and Jessica.\n\"\"\"\nclass TabbedApp(App):\n\"\"\"An example of tabbed content.\"\"\"\nBINDINGS = [\n(\"l\", \"show_tab('leto')\", \"Leto\"),\n(\"j\", \"show_tab('jessica')\", \"Jessica\"),\n(\"p\", \"show_tab('paul')\", \"Paul\"),\n]\ndef compose(self) -> ComposeResult:\n\"\"\"Compose app with tabbed content.\"\"\"\n# Footer to show keys\nyield Footer()\n# Add the TabbedContent widget\nwith TabbedContent(initial=\"jessica\"):\nwith TabPane(\"Leto\", id=\"leto\"):  # First tab\nyield Markdown(LETO)  # Tab content\nwith TabPane(\"Jessica\", id=\"jessica\"):\nyield Markdown(JESSICA)\nwith TabbedContent(\"Paul\", \"Alia\"):\nyield TabPane(\"Paul\", Label(\"First child\"))\nyield TabPane(\"Alia\", Label(\"Second child\"))\nwith TabPane(\"Paul\", id=\"paul\"):\nyield Markdown(PAUL)\ndef action_show_tab(self, tab: str) -> None:\n\"\"\"Switch to a new tab.\"\"\"\nself.get_child_by_type(TabbedContent).active = tab\nif __name__ == \"__main__\":\napp = TabbedApp()\napp.run()\n
"},{"location":"widgets/tabbed_content/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description active str \"\" The id attribute of the active tab. Set this to switch tabs."},{"location":"widgets/tabbed_content/#messages","title":"Messages","text":"
  • TabbedContent.TabActivated
"},{"location":"widgets/tabbed_content/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/tabbed_content/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/tabbed_content/#see-also","title":"See also","text":"
  • Tabs
  • ContentSwitcher
"},{"location":"widgets/tabbed_content/#textual.widgets.TabbedContent","title":"textual.widgets.TabbedContent class","text":"
def __init__(\nself,\n*titles,\ninitial=\"\",\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: Widget

A container with associated tabs to toggle content visibility.

Parameters Name Type Description Default *titles TextType

Positional argument will be used as title.

() initial str

The id of the initial tab, or empty string to select the first tab.

'' name str | None

The name of the button.

None id str | None

The ID of the button in the DOM.

None classes str | None

The CSS classes of the button.

None disabled bool

Whether the button is disabled or not.

False"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.active","title":"active instance-attribute class-attribute","text":"
active: reactive[str] = reactive('', init=False)\n

The ID of the active tab, or empty string if none are active.

"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.tab_count","title":"tab_count property","text":"
tab_count: int\n

Total number of tabs.

"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.Cleared","title":"Cleared class","text":"
def __init__(self, tabbed_content):\n

Bases: Message

Posted when there are no more tab panes.

Parameters Name Type Description Default tabbed_content TabbedContent

The TabbedContent widget.

required"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.Cleared.control","title":"control property","text":"
control: TabbedContent\n

The TabbedContent widget that was cleared of all tab panes.

This is an alias for Cleared.tabbed_content and is used by the on decorator.

"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.Cleared.tabbed_content","title":"tabbed_content instance-attribute","text":"
tabbed_content = tabbed_content\n

The TabbedContent widget that contains the tab activated.

"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.TabActivated","title":"TabActivated class","text":"
def __init__(self, tabbed_content, tab):\n

Bases: Message

Posted when the active tab changes.

Parameters Name Type Description Default tabbed_content TabbedContent

The TabbedContent widget.

required tab Tab

The Tab widget that was selected (contains the tab label).

required"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.TabActivated.ALLOW_SELECTOR_MATCH","title":"ALLOW_SELECTOR_MATCH instance-attribute class-attribute","text":"
ALLOW_SELECTOR_MATCH = {'tab'}\n

Additional message attributes that can be used with the on decorator.

"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.TabActivated.control","title":"control property","text":"
control: TabbedContent\n

The TabbedContent widget that contains the tab activated.

This is an alias for TabActivated.tabbed_content and is used by the on decorator.

"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.TabActivated.tab","title":"tab instance-attribute","text":"
tab = tab\n

The Tab widget that was selected (contains the tab label).

"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.TabActivated.tabbed_content","title":"tabbed_content instance-attribute","text":"
tabbed_content = tabbed_content\n

The TabbedContent widget that contains the tab activated.

"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.add_pane","title":"add_pane method","text":"
def add_pane(self, pane, *, before=None, after=None):\n

Add a new pane to the tabbed content.

Parameters Name Type Description Default pane TabPane

The pane to add.

required before TabPane | str | None

Optional pane or pane ID to add the pane before.

None after TabPane | str | None

Optional pane or pane ID to add the pane after.

None Returns Type Description AwaitTabbedContent

An awaitable object that waits for the pane to be added.

Raises Type Description Tabs.TabError

If there is a problem with the addition request.

Note

Only one of before or after can be provided. If both are provided a Tabs.TabError will be raised.

"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.clear_panes","title":"clear_panes method","text":"
def clear_panes(self):\n

Remove all the panes in the tabbed content.

"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.disable_tab","title":"disable_tab method","text":"
def disable_tab(self, tab_id):\n

Disables the tab with the given ID.

Parameters Name Type Description Default tab_id str

The ID of the TabPane to disable.

required Raises Type Description Tabs.TabError

If there are any issues with the request.

"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.enable_tab","title":"enable_tab method","text":"
def enable_tab(self, tab_id):\n

Enables the tab with the given ID.

Parameters Name Type Description Default tab_id str

The ID of the TabPane to enable.

required Raises Type Description Tabs.TabError

If there are any issues with the request.

"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.hide_tab","title":"hide_tab method","text":"
def hide_tab(self, tab_id):\n

Hides the tab with the given ID.

Parameters Name Type Description Default tab_id str

The ID of the TabPane to hide.

required Raises Type Description Tabs.TabError

If there are any issues with the request.

"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.remove_pane","title":"remove_pane method","text":"
def remove_pane(self, pane_id):\n

Remove a given pane from the tabbed content.

Parameters Name Type Description Default pane_id str

The ID of the pane to remove.

required Returns Type Description AwaitTabbedContent

An awaitable object that waits for the pane to be removed.

"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.show_tab","title":"show_tab method","text":"
def show_tab(self, tab_id):\n

Shows the tab with the given ID.

Parameters Name Type Description Default tab_id str

The ID of the TabPane to show.

required Raises Type Description Tabs.TabError

If there are any issues with the request.

"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabbedContent.validate_active","title":"validate_active method","text":"
def validate_active(self, active):\n

It doesn't make sense for active to be an empty string.

Parameters Name Type Description Default active str

Attribute to be validated.

required Returns Type Description str

Value of active.

Raises Type Description ValueError

If the active attribute is set to empty string when there are tabs available.

"},{"location":"widgets/tabbed_content/#textual.widgets.TabPane","title":"textual.widgets.TabPane class","text":"
def __init__(\nself,\ntitle,\n*children,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: Widget

A container for switchable content, with additional title.

This widget is intended to be used with TabbedContent.

Parameters Name Type Description Default title TextType

Title of the TabPane (will be displayed in a tab label).

required *children Widget

Widget to go inside the TabPane.

() name str | None

Optional name for the TabPane.

None id str | None

Optional ID for the TabPane.

None classes str | None

Optional initial classes for the widget.

None disabled bool

Whether the TabPane is disabled or not.

False"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabPane.Disabled","title":"Disabled class","text":"

Bases: TabPaneMessage

Sent when a tab pane is disabled via its reactive disabled.

"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabPane.Enabled","title":"Enabled class","text":"

Bases: TabPaneMessage

Sent when a tab pane is enabled via its reactive disabled.

"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabPane.TabPaneMessage","title":"TabPaneMessage class","text":"

Bases: Message

Base class for TabPane messages.

"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabPane.TabPaneMessage.control","title":"control property","text":"
control: TabPane\n

The tab pane that is the object of this message.

This is an alias for the attribute tab_pane and is used by the on decorator.

"},{"location":"widgets/tabbed_content/#textual.widgets._tabbed_content.TabPane.TabPaneMessage.tab_pane","title":"tab_pane instance-attribute","text":"
tab_pane: TabPane\n

The TabPane that is he object of this message.

"},{"location":"widgets/tabs/","title":"Tabs","text":"

Added in version 0.15.0

Displays a number of tab headers which may be activated with a click or navigated with cursor keys.

  • Focusable
  • Container

Construct a Tabs widget with strings or Text objects as positional arguments, which will set the labels in the tabs. Here's an example with three tabs:

def compose(self) -> ComposeResult:\nyield Tabs(\"First tab\", \"Second tab\", Text.from_markup(\"[u]Third[/u] tab\"))\n

This will create Tab widgets internally, with auto-incrementing id attributes (\"tab-1\", \"tab-2\" etc). You can also supply Tab objects directly in the constructor, which will allow you to explicitly set an id. Here's an example:

def compose(self) -> ComposeResult:\nyield Tabs(\nTab(\"First tab\", id=\"one\"),\nTab(\"Second tab\", id=\"two\"),\n)\n

When the user switches to a tab by clicking or pressing keys, then Tabs will send a Tabs.TabActivated message which contains the tab that was activated. You can then use event.tab.id attribute to perform any related actions.

"},{"location":"widgets/tabs/#clearing-tabs","title":"Clearing tabs","text":"

Clear tabs by calling the clear method. Clearing the tabs will send a Tabs.TabActivated message with the tab attribute set to None.

"},{"location":"widgets/tabs/#adding-tabs","title":"Adding tabs","text":"

Tabs may be added dynamically with the add_tab method, which accepts strings, Text, or Tab objects.

"},{"location":"widgets/tabs/#example","title":"Example","text":"

The following example adds a Tabs widget above a text label. Press A to add a tab, C to clear the tabs.

Outputtabs.py

TabsApp \u00a0AtreidiesDuke\u00a0Leto\u00a0AtreidesLady\u00a0JessicaGurney\u00a0HalleckBaron\u00a0Vladimir \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2578\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u257a\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 \u258a\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u2594\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258aLady\u00a0Jessica\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u258e \u258a\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u2581\u258e \u00a0A\u00a0\u00a0Add\u00a0tab\u00a0\u00a0R\u00a0\u00a0Remove\u00a0active\u00a0tab\u00a0\u00a0C\u00a0\u00a0Clear\u00a0tabs\u00a0

from textual.app import App, ComposeResult\nfrom textual.widgets import Footer, Label, Tabs\nNAMES = [\n\"Paul Atreidies\",\n\"Duke Leto Atreides\",\n\"Lady Jessica\",\n\"Gurney Halleck\",\n\"Baron Vladimir Harkonnen\",\n\"Glossu Rabban\",\n\"Chani\",\n\"Silgar\",\n]\nclass TabsApp(App):\n\"\"\"Demonstrates the Tabs widget.\"\"\"\nCSS = \"\"\"\n    Tabs {\n        dock: top;\n    }\n    Screen {\n        align: center middle;\n    }\n    Label {\n        margin:1 1;\n        width: 100%;\n        height: 100%;\n        background: $panel;\n        border: tall $primary;\n        content-align: center middle;\n    }\n    \"\"\"\nBINDINGS = [\n(\"a\", \"add\", \"Add tab\"),\n(\"r\", \"remove\", \"Remove active tab\"),\n(\"c\", \"clear\", \"Clear tabs\"),\n]\ndef compose(self) -> ComposeResult:\nyield Tabs(NAMES[0])\nyield Label()\nyield Footer()\ndef on_mount(self) -> None:\n\"\"\"Focus the tabs when the app starts.\"\"\"\nself.query_one(Tabs).focus()\ndef on_tabs_tab_activated(self, event: Tabs.TabActivated) -> None:\n\"\"\"Handle TabActivated message sent by Tabs.\"\"\"\nlabel = self.query_one(Label)\nif event.tab is None:\n# When the tabs are cleared, event.tab will be None\nlabel.visible = False\nelse:\nlabel.visible = True\nlabel.update(event.tab.label)\ndef action_add(self) -> None:\n\"\"\"Add a new tab.\"\"\"\ntabs = self.query_one(Tabs)\n# Cycle the names\nNAMES[:] = [*NAMES[1:], NAMES[0]]\ntabs.add_tab(NAMES[0])\ndef action_remove(self) -> None:\n\"\"\"Remove active tab.\"\"\"\ntabs = self.query_one(Tabs)\nactive_tab = tabs.active_tab\nif active_tab is not None:\ntabs.remove_tab(active_tab.id)\ndef action_clear(self) -> None:\n\"\"\"Clear the tabs.\"\"\"\nself.query_one(Tabs).clear()\nif __name__ == \"__main__\":\napp = TabsApp()\napp.run()\n
"},{"location":"widgets/tabs/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description active str \"\" The ID of the active tab. Set this attribute to a tab ID to change the active tab."},{"location":"widgets/tabs/#messages","title":"Messages","text":"
  • Tabs.TabActivated
  • Tabs.Cleared
"},{"location":"widgets/tabs/#bindings","title":"Bindings","text":"

The Tabs widget defines the following bindings:

Key(s) Description left Move to the previous tab. right Move to the next tab."},{"location":"widgets/tabs/#component-classes","title":"Component Classes","text":"

This widget has no component classes.

"},{"location":"widgets/tabs/#textual.widgets.Tabs","title":"textual.widgets.Tabs class","text":"
def __init__(\nself,\n*tabs,\nactive=None,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: Widget

A row of tabs.

Parameters Name Type Description Default *tabs Tab | TextType

Positional argument should be explicit Tab objects, or a str or Text.

() active str | None

ID of the tab which should be active on start.

None name str | None

Optional name for the input widget.

None id str | None

Optional ID for the widget.

None classes str | None

Optional initial classes for the widget.

None disabled bool

Whether the input is disabled or not.

False"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.BINDINGS","title":"BINDINGS class-attribute","text":"
BINDINGS: list[BindingType] = [\nBinding(\n\"left\", \"previous_tab\", \"Previous tab\", show=False\n),\nBinding(\"right\", \"next_tab\", \"Next tab\", show=False),\n]\n
Key(s) Description left Move to the previous tab. right Move to the next tab."},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.active","title":"active instance-attribute class-attribute","text":"
active: reactive[str] = reactive('', init=False)\n

The ID of the active tab, or empty string if none are active.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.active_tab","title":"active_tab property","text":"
active_tab: Tab | None\n

The currently active tab, or None if there are no active tabs.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.tab_count","title":"tab_count property","text":"
tab_count: int\n

Total number of tabs.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.Cleared","title":"Cleared class","text":"
def __init__(self, tabs):\n

Bases: Message

Sent when there are no active tabs.

Parameters Name Type Description Default tabs Tabs

The tabs widget.

required"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.Cleared.control","title":"control property","text":"
control: Tabs\n

The tabs widget which was cleared.

This is an alias for Cleared.tabs which is used by the on decorator.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.Cleared.tabs","title":"tabs instance-attribute","text":"
tabs: Tabs = tabs\n

The tabs widget which was cleared.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabActivated","title":"TabActivated class","text":"

Bases: TabMessage

Sent when a new tab is activated.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabDisabled","title":"TabDisabled class","text":"

Bases: TabMessage

Sent when a tab is disabled.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabEnabled","title":"TabEnabled class","text":"

Bases: TabMessage

Sent when a tab is enabled.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabError","title":"TabError class","text":"

Bases: Exception

Exception raised when there is an error relating to tabs.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabHidden","title":"TabHidden class","text":"

Bases: TabMessage

Sent when a tab is hidden.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabMessage","title":"TabMessage class","text":"
def __init__(self, tabs, tab):\n

Bases: Message

Parent class for all messages that have to do with a specific tab.

Parameters Name Type Description Default tabs Tabs

The Tabs widget.

required tab Tab

The tab that is the object of this message.

required"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabMessage.ALLOW_SELECTOR_MATCH","title":"ALLOW_SELECTOR_MATCH instance-attribute class-attribute","text":"
ALLOW_SELECTOR_MATCH = {'tab'}\n

Additional message attributes that can be used with the on decorator.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabMessage.control","title":"control property","text":"
control: Tabs\n

The tabs widget containing the tab that is the object of this message.

This is an alias for the attribute tabs and is used by the on decorator.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabMessage.tab","title":"tab instance-attribute","text":"
tab: Tab = tab\n

The tab that is the object of this message.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabMessage.tabs","title":"tabs instance-attribute","text":"
tabs: Tabs = tabs\n

The tabs widget containing the tab.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.TabShown","title":"TabShown class","text":"

Bases: TabMessage

Sent when a tab is shown.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.action_next_tab","title":"action_next_tab method","text":"
def action_next_tab(self):\n

Make the next tab active.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.action_previous_tab","title":"action_previous_tab method","text":"
def action_previous_tab(self):\n

Make the previous tab active.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.add_tab","title":"add_tab method","text":"
def add_tab(self, tab, *, before=None, after=None):\n

Add a new tab to the end of the tab list.

Parameters Name Type Description Default tab Tab | str | Text

A new tab object, or a label (str or Text).

required before Tab | str | None

Optional tab or tab ID to add the tab before.

None after Tab | str | None

Optional tab or tab ID to add the tab after.

None Returns Type Description AwaitMount

An awaitable object that waits for the tab to be mounted.

Raises Type Description Tabs.TabError

If there is a problem with the addition request.

Note

Only one of before or after can be provided. If both are provided a Tabs.TabError will be raised.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.clear","title":"clear method","text":"
def clear(self):\n

Clear all the tabs.

Returns Type Description AwaitRemove

An awaitable object that waits for the tabs to be removed.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.disable","title":"disable method","text":"
def disable(self, tab_id):\n

Disable the indicated tab.

Parameters Name Type Description Default tab_id str

The ID of the Tab to disable.

required Returns Type Description Tab

The Tab that was targeted.

Raises Type Description TabError

If there are any issues with the request.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.enable","title":"enable method","text":"
def enable(self, tab_id):\n

Enable the indicated tab.

Parameters Name Type Description Default tab_id str

The ID of the Tab to enable.

required Returns Type Description Tab

The Tab that was targeted.

Raises Type Description TabError

If there are any issues with the request.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.hide","title":"hide method","text":"
def hide(self, tab_id):\n

Hide the indicated tab.

Parameters Name Type Description Default tab_id str

The ID of the Tab to hide.

required Returns Type Description Tab

The Tab that was targeted.

Raises Type Description TabError

If there are any issues with the request.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.remove_tab","title":"remove_tab method","text":"
def remove_tab(self, tab_or_id):\n

Remove a tab.

Parameters Name Type Description Default tab_or_id Tab | str | None

The Tab to remove or its id.

required Returns Type Description AwaitRemove

An awaitable object that waits for the tab to be removed.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.show","title":"show method","text":"
def show(self, tab_id):\n

Show the indicated tab.

Parameters Name Type Description Default tab_id str

The ID of the Tab to show.

required Returns Type Description Tab

The Tab that was targeted.

Raises Type Description TabError

If there are any issues with the request.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.validate_active","title":"validate_active method","text":"
def validate_active(self, active):\n

Check id assigned to active attribute is a valid tab.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tabs.watch_active","title":"watch_active method","text":"
def watch_active(self, previously_active, active):\n

Handle a change to the active tab.

"},{"location":"widgets/tabs/#textual.widgets.Tab","title":"textual.widgets.Tab class","text":"
def __init__(\nself, label, *, id=None, classes=None, disabled=False\n):\n

Bases: Static

A Widget to manage a single tab within a Tabs widget.

Parameters Name Type Description Default label TextType

The label to use in the tab.

required id str | None

Optional ID for the widget.

None classes str | None

Space separated list of class names.

None disabled bool

Whether the tab is disabled or not.

False"},{"location":"widgets/tabs/#textual.widgets._tabs.Tab.label_text","title":"label_text property","text":"
label_text: str\n

Undecorated text of the label.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tab.Clicked","title":"Clicked class","text":"

Bases: TabMessage

A tab was clicked.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tab.Disabled","title":"Disabled class","text":"

Bases: TabMessage

A tab was disabled.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tab.Enabled","title":"Enabled class","text":"

Bases: TabMessage

A tab was enabled.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tab.TabMessage","title":"TabMessage class","text":"

Bases: Message

Tab-related messages.

These are mostly intended for internal use when interacting with Tabs.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tab.TabMessage.control","title":"control property","text":"
control: Tab\n

The tab that is the object of this message.

This is an alias for the attribute tab and is used by the on decorator.

"},{"location":"widgets/tabs/#textual.widgets._tabs.Tab.TabMessage.tab","title":"tab instance-attribute","text":"
tab: Tab\n

The tab that is the object of this message.

"},{"location":"widgets/text_area/","title":"TextArea","text":"

Added in version 0.38.0

A widget for editing text which may span multiple lines. Supports syntax highlighting for a selection of languages.

  • Focusable
  • Container
"},{"location":"widgets/text_area/#guide","title":"Guide","text":""},{"location":"widgets/text_area/#loading-text","title":"Loading text","text":"

In this example we load some initial text into the TextArea, and set the language to \"python\" to enable syntax highlighting.

Outputtext_area_example.py

TextAreaExample 1\u00a0\u00a0defhello(name):\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 2\u00a0\u00a0print(\"hello\"+\u00a0name)\u00a0\u00a0\u00a0 3\u00a0\u00a0 4\u00a0\u00a0defgoodbye(name):\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 5\u00a0\u00a0print(\"goodbye\"+\u00a0name)\u00a0 6\u00a0\u00a0

from textual.app import App, ComposeResult\nfrom textual.widgets import TextArea\nTEXT = \"\"\"\\\ndef hello(name):\n    print(\"hello\" + name)\ndef goodbye(name):\n    print(\"goodbye\" + name)\n\"\"\"\nclass TextAreaExample(App):\ndef compose(self) -> ComposeResult:\nyield TextArea(TEXT, language=\"python\")\napp = TextAreaExample()\nif __name__ == \"__main__\":\napp.run()\n

To load content into the TextArea after it has already been created, use the load_text method.

To update the parser used for syntax highlighting, set the language reactive attribute:

# Set the language to Markdown\ntext_area.language = \"markdown\"\n

Note

Syntax highlighting is unavailable on Python 3.7.

Note

More built-in languages will be added in the future. For now, you can add your own.

"},{"location":"widgets/text_area/#reading-content-from-textarea","title":"Reading content from TextArea","text":"

There are a number of ways to retrieve content from the TextArea:

  • The TextArea.text property returns all content in the text area as a string.
  • The TextArea.selected_text property returns the text corresponding to the current selection.
  • The TextArea.get_text_range method returns the text between two locations.

In all cases, when multiple lines of text are retrieved, the document line separator will be used.

"},{"location":"widgets/text_area/#editing-content-inside-textarea","title":"Editing content inside TextArea","text":"

The content of the TextArea can be updated using the replace method. This method is the programmatic equivalent of selecting some text and then pasting.

Some other convenient methods are available, such as insert, delete, and clear.

"},{"location":"widgets/text_area/#working-with-the-cursor","title":"Working with the cursor","text":""},{"location":"widgets/text_area/#moving-the-cursor","title":"Moving the cursor","text":"

The cursor location is available via the cursor_location property, which represents the location of the cursor as a tuple (row_index, column_index). These indices are zero-based. Writing a new value to cursor_location will immediately update the location of the cursor.

>>> text_area = TextArea()\n>>> text_area.cursor_location\n(0, 0)\n>>> text_area.cursor_location = (0, 4)\n>>> text_area.cursor_location\n(0, 4)\n

cursor_location is a simple way to move the cursor programmatically, but it doesn't let us select text.

"},{"location":"widgets/text_area/#selecting-text","title":"Selecting text","text":"

To select text, we can use the selection reactive attribute. Let's select the first two lines of text in a document by adding text_area.selection = Selection(start=(0, 0), end=(2, 0)) to our code:

Outputtext_area_selection.py

TextAreaSelection 1\u00a0\u00a0defhello(name): 2\u00a0\u00a0print(\"hello\"+\u00a0name) 3\u00a0\u00a0 4\u00a0\u00a0defgoodbye(name):\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 5\u00a0\u00a0print(\"goodbye\"+\u00a0name)\u00a0 6\u00a0\u00a0

from textual.app import App, ComposeResult\nfrom textual.widgets import TextArea\nfrom textual.widgets.text_area import Selection\nTEXT = \"\"\"\\\ndef hello(name):\n    print(\"hello\" + name)\ndef goodbye(name):\n    print(\"goodbye\" + name)\n\"\"\"\nclass TextAreaSelection(App):\ndef compose(self) -> ComposeResult:\ntext_area = TextArea(TEXT, language=\"python\")\ntext_area.selection = Selection(start=(0, 0), end=(2, 0))  # (1)!\nyield text_area\napp = TextAreaSelection()\nif __name__ == \"__main__\":\napp.run()\n
  1. Selects the first two lines of text.

Note that selections can happen in both directions, so Selection((2, 0), (0, 0)) is also valid.

Tip

The end attribute of the selection is always equal to TextArea.cursor_location. In other words, the cursor_location attribute is simply a convenience for accessing text_area.selection.end.

"},{"location":"widgets/text_area/#more-cursor-utilities","title":"More cursor utilities","text":"

There are a number of additional utility methods available for interacting with the cursor.

"},{"location":"widgets/text_area/#location-information","title":"Location information","text":"

A number of properties exist on TextArea which give information about the current cursor location. These properties begin with cursor_at_, and return booleans. For example, cursor_at_start_of_line tells us if the cursor is at a start of line.

We can also check the location the cursor would arrive at if we were to move it. For example, get_cursor_right_location returns the location the cursor would move to if it were to move right. A number of similar methods exist, with names like get_cursor_*_location.

"},{"location":"widgets/text_area/#cursor-movement-methods","title":"Cursor movement methods","text":"

The move_cursor method allows you to move the cursor to a new location while selecting text, or move the cursor and scroll to keep it centered.

# Move the cursor from its current location to row index 4,\n# column index 8, while selecting all the text between.\ntext_area.move_cursor((4, 8), select=True)\n

The move_cursor_relative method offers a very similar interface, but moves the cursor relative to its current location.

"},{"location":"widgets/text_area/#common-selections","title":"Common selections","text":"

There are some methods available which make common selections easier:

  • select_line selects a line by index. Bound to F6 by default.
  • select_all selects all text. Bound to F7 by default.
"},{"location":"widgets/text_area/#themes","title":"Themes","text":"

TextArea ships with some builtin themes, and you can easily add your own.

Themes give you control over the look and feel, including syntax highlighting, the cursor, selection, gutter, and more.

"},{"location":"widgets/text_area/#using-builtin-themes","title":"Using builtin themes","text":"

The initial theme of the TextArea is determined by the theme parameter.

# Create a TextArea with the 'dracula' theme.\nyield TextArea(\"print(123)\", language=\"python\", theme=\"dracula\")\n

You can check which themes are available using the available_themes property.

>>> text_area = TextArea()\n>>> print(text_area.available_themes)\n{'dracula', 'github_light', 'monokai', 'vscode_dark'}\n

After creating a TextArea, you can change the theme by setting the theme attribute to one of the available themes.

text_area.theme = \"vscode_dark\"\n

On setting this attribute the TextArea will immediately refresh to display the updated theme.

"},{"location":"widgets/text_area/#custom-themes","title":"Custom themes","text":"

Using custom (non-builtin) themes is two-step process:

  1. Create an instance of TextAreaTheme.
  2. Register it using TextArea.register_theme.
"},{"location":"widgets/text_area/#1-creating-a-theme","title":"1. Creating a theme","text":"

Let's create a simple theme, \"my_cool_theme\", which colors the cursor blue, and the cursor line yellow. Our theme will also syntax highlight strings as red, and comments as magenta.

from rich.style import Style\nfrom textual.widgets.text_area import TextAreaTheme\n# ...\nmy_theme = TextAreaTheme(\n# This name will be used to refer to the theme...\nname=\"my_cool_theme\",\n# Basic styles such as background, cursor, selection, gutter, etc...\ncursor_style=Style(color=\"white\", bgcolor=\"blue\"),\ncursor_line_style=Style(bgcolor=\"yellow\"),\n# `syntax_styles` is for syntax highlighting.\n# It maps tokens parsed from the document to Rich styles.\nsyntax_styles={\n\"string\": Style(color=\"red\"),\n\"comment\": Style(color=\"magenta\"),\n}\n)\n

Attributes like cursor_style and cursor_line_style apply general language-agnostic styling to the widget.

The syntax_styles attribute of TextAreaTheme is used for syntax highlighting and depends on the language currently in use. For more details, see syntax highlighting.

If you wish to build on an existing theme, you can obtain a reference to it using the TextAreaTheme.get_builtin_theme classmethod:

from textual.widgets.text_area import TextAreaTheme\nmonokai = TextAreaTheme.get_builtin_theme(\"monokai\")\n
"},{"location":"widgets/text_area/#2-registering-a-theme","title":"2. Registering a theme","text":"

Our theme can now be registered with the TextArea instance.

text_area.register_theme(my_theme)\n

After registering a theme, it'll appear in the available_themes:

>>> print(text_area.available_themes)\n{'dracula', 'github_light', 'monokai', 'vscode_dark', 'my_cool_theme'}\n

We can now switch to it:

text_area.theme = \"my_cool_theme\"\n

This immediately updates the appearance of the TextArea:

TextAreaCustomThemes 1\u00a0\u00a0#\u00a0says\u00a0hello 2\u00a0\u00a0def\u00a0hello(name):\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0print(\"hello\"\u00a0+\u00a0name)\u00a0\u00a0\u00a0 4\u00a0\u00a0 5\u00a0\u00a0#\u00a0says\u00a0goodbye 6\u00a0\u00a0def\u00a0goodbye(name):\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 7\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0print(\"goodbye\"\u00a0+\u00a0name)\u00a0 8\u00a0\u00a0

"},{"location":"widgets/text_area/#indentation","title":"Indentation","text":"

The character(s) inserted when you press tab is controlled by setting the indent_type attribute to either tabs or spaces.

If indent_type == \"spaces\", pressing Tab will insert up to indent_width spaces in order to align with the next tab stop.

"},{"location":"widgets/text_area/#line-separators","title":"Line separators","text":"

When content is loaded into TextArea, the content is scanned from beginning to end and the first occurrence of a line separator is recorded.

This separator will then be used when content is later read from the TextArea via the text property. The TextArea widget does not support exporting text which contains mixed line endings.

Similarly, newline characters pasted into the TextArea will be converted.

You can check the line separator of the current document by inspecting TextArea.document.newline:

>>> text_area = TextArea()\n>>> text_area.document.newline\n'\\n'\n
"},{"location":"widgets/text_area/#line-numbers","title":"Line numbers","text":"

The gutter (column on the left containing line numbers) can be toggled by setting the show_line_numbers attribute to True or False.

Setting this attribute will immediately repaint the TextArea to reflect the new value.

"},{"location":"widgets/text_area/#extending-textarea","title":"Extending TextArea","text":"

Sometimes, you may wish to subclass TextArea to add some extra functionality. In this section, we'll briefly explore how we can extend the widget to achieve common goals.

"},{"location":"widgets/text_area/#hooking-into-key-presses","title":"Hooking into key presses","text":"

You may wish to hook into certain key presses to inject some functionality. This can be done by over-riding _on_key and adding the required functionality.

"},{"location":"widgets/text_area/#example-closing-parentheses-automatically","title":"Example - closing parentheses automatically","text":"

Let's extend TextArea to add a feature which automatically closes parentheses and moves the cursor to a sensible location.

from textual import events\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import TextArea\nclass ExtendedTextArea(TextArea):\n\"\"\"A subclass of TextArea with parenthesis-closing functionality.\"\"\"\ndef _on_key(self, event: events.Key) -> None:\nif event.character == \"(\":\nself.insert(\"()\")\nself.move_cursor_relative(columns=-1)\nevent.prevent_default()\nclass TextAreaKeyPressHook(App):\ndef compose(self) -> ComposeResult:\nyield ExtendedTextArea(language=\"python\")\napp = TextAreaKeyPressHook()\nif __name__ == \"__main__\":\napp.run()\n

This intercepts the key handler when \"(\" is pressed, and inserts \"()\" instead. It then moves the cursor so that it lands between the open and closing parentheses.

Typing def hello( into the TextArea results in the bracket automatically being closed:

TextAreaKeyPressHook 1\u00a0\u00a0def\u00a0hello()

"},{"location":"widgets/text_area/#advanced-concepts","title":"Advanced concepts","text":""},{"location":"widgets/text_area/#syntax-highlighting","title":"Syntax highlighting","text":"

Syntax highlighting inside the TextArea is powered by a library called tree-sitter.

Each time you update the document in a TextArea, an internal syntax tree is updated. This tree is frequently queried to find location ranges relevant to syntax highlighting. We give these ranges names, and ultimately map them to Rich styles inside TextAreaTheme.syntax_styles.

To illustrate how this works, lets look at how the \"Monokai\" TextAreaTheme highlights Markdown files.

When the language attribute is set to \"markdown\", a highlight query similar to the one below is used (trimmed for brevity).

(heading_content) @heading\n(link) @link\n

This highlight query maps heading_content nodes returned by the Markdown parser to the name @heading, and link nodes to the name @link.

Inside our TextAreaTheme.syntax_styles dict, we can map the name @heading to a Rich style. Here's a snippet from the \"Monokai\" theme which does just that:

TextAreaTheme(\nname=\"monokai\",\nbase_style=Style(color=\"#f8f8f2\", bgcolor=\"#272822\"),\ngutter_style=Style(color=\"#90908a\", bgcolor=\"#272822\"),\n# ...\nsyntax_styles={\n# Colorise @heading and make them bold\n\"heading\": Style(color=\"#F92672\", bold=True),\n# Colorise and underline @link\n\"link\": Style(color=\"#66D9EF\", underline=True),\n# ...\n},\n)\n

To understand which names can be mapped inside syntax_styles, we recommend looking at the existing themes and highlighting queries (.scm files) in the Textual repository.

Tip

You may also wish to take a look at the contents of TextArea._highlights on an active TextArea instance to see which highlights have been generated for the open document.

"},{"location":"widgets/text_area/#adding-support-for-custom-languages","title":"Adding support for custom languages","text":"

To add support for a language to a TextArea, use the register_language method.

To register a language, we require two things:

  1. A tree-sitter Language object which contains the grammar for the language.
  2. A highlight query which is used for syntax highlighting.
"},{"location":"widgets/text_area/#example-adding-java-support","title":"Example - adding Java support","text":"

The easiest way to obtain a Language object is using the py-tree-sitter-languages package. Here's how we can use this package to obtain a reference to a Language object representing Java:

from tree_sitter_languages import get_language\njava_language = get_language(\"java\")\n

Note

py-tree-sitter-languages may not be available on some architectures (e.g. Macbooks with Apple Silicon running Python 3.7).

The exact version of the parser used when you call get_language can be checked via the repos.txt file in the version of py-tree-sitter-languages you're using. This file contains links to the GitHub repos and commit hashes of the tree-sitter parsers. In these repos you can often find pre-made highlight queries at queries/highlights.scm, and a file showing all the available node types which can be used in highlight queries at src/node-types.json.

Since we're adding support for Java, lets grab the Java highlight query from the repo by following these steps:

  1. Open repos.txt file from the py-tree-sitter-languages repo.
  2. Find the link corresponding to tree-sitter-java and go to the repo on GitHub (you may also need to go to the specific commit referenced in repos.txt).
  3. Go to queries/highlights.scm to see the example highlight query for Java.

Be sure to check the license in the repo to ensure it can be freely copied.

Warning

It's important to use a highlight query which is compatible with the parser in use, so pay attention to the commit hash when visiting the repo via repos.txt.

We now have our Language and our highlight query, so we can register Java as a language.

from pathlib import Path\nfrom tree_sitter_languages import get_language\nfrom textual.app import App, ComposeResult\nfrom textual.widgets import TextArea\njava_language = get_language(\"java\")\njava_highlight_query = (Path(__file__).parent / \"java_highlights.scm\").read_text()\njava_code = \"\"\"\\\nclass HelloWorld {\n    public static void main(String[] args) {\n        System.out.println(\"Hello, World!\");\n    }\n}\n\"\"\"\nclass TextAreaCustomLanguage(App):\ndef compose(self) -> ComposeResult:\ntext_area = TextArea(text=java_code)\ntext_area.cursor_blink = False\n# Register the Java language and highlight query\ntext_area.register_language(java_language, java_highlight_query)\n# Switch to Java\ntext_area.language = \"java\"\nyield text_area\napp = TextAreaCustomLanguage()\nif __name__ == \"__main__\":\napp.run()\n

Running our app, we can see that the Java code is highlighted. We can freely edit the text, and the syntax highlighting will update immediately.

TextAreaCustomLanguage 1\u00a0\u00a0class\u00a0HelloWorld\u00a0{\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 2\u00a0\u00a0publicstatic\u00a0void\u00a0main(String[]\u00a0args)\u00a0{\u00a0 3\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0System.out.println(\"Hello,\u00a0World!\");\u00a0 4\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 5\u00a0\u00a0}\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 6\u00a0\u00a0

Recall that we map names (like @heading) from the tree-sitter highlight query to Rich style objects inside the TextAreaTheme.syntax_styles dictionary. If you notice some highlights are missing after registering a language, the issue may be:

  1. The current TextAreaTheme doesn't contain a mapping for the name in the highlight query. Adding a new to syntax_styles should resolve the issue.
  2. The highlight query doesn't assign a name to the pattern you expect to be highlighted. In this case you'll need to update the highlight query to assign to the name.

Tip

The names assigned in tree-sitter highlight queries are often reused across multiple languages. For example, @string is used in many languages to highlight strings.

"},{"location":"widgets/text_area/#reactive-attributes","title":"Reactive attributes","text":"Name Type Default Description language str | None None The language to use for syntax highlighting. theme str | None TextAreaTheme.default() The theme to use for syntax highlighting. selection Selection Selection() The current selection. show_line_numbers bool True Show or hide line numbers. indent_width int 4 The number of spaces to indent and width of tabs. match_cursor_bracket bool True Enable/disable highlighting matching brackets under cursor. cursor_blink bool True Enable/disable blinking of the cursor when the widget has focus."},{"location":"widgets/text_area/#bindings","title":"Bindings","text":"

The TextArea widget defines the following bindings:

Key(s) Description escape Focus on the next item. up Move the cursor up. down Move the cursor down. left Move the cursor left. ctrl+left Move the cursor to the start of the word. ctrl+shift+left Move the cursor to the start of the word and select. right Move the cursor right. ctrl+right Move the cursor to the end of the word. ctrl+shift+right Move the cursor to the end of the word and select. home,ctrl+a Move the cursor to the start of the line. end,ctrl+e Move the cursor to the end of the line. shift+home Move the cursor to the start of the line and select. shift+end Move the cursor to the end of the line and select. pageup Move the cursor one page up. pagedown Move the cursor one page down. shift+up Select while moving the cursor up. shift+down Select while moving the cursor down. shift+left Select while moving the cursor left. shift+right Select while moving the cursor right. backspace Delete character to the left of cursor. ctrl+w Delete from cursor to start of the word. delete,ctrl+d Delete character to the right of cursor. ctrl+f Delete from cursor to end of the word. ctrl+x Delete the current line. ctrl+u Delete from cursor to the start of the line. ctrl+k Delete from cursor to the end of the line. f6 Select the current line. f7 Select all text in the document."},{"location":"widgets/text_area/#component-classes","title":"Component classes","text":"

The TextArea widget defines no component classes.

Styling should be done exclusively via TextAreaTheme.

"},{"location":"widgets/text_area/#see-also","title":"See also","text":"
  • Input - for single-line text input.
  • TextAreaTheme - for theming the TextArea.
  • The tree-sitter documentation website.
  • The tree-sitter Python bindings repository.
  • py-tree-sitter-languages repository (provides binary wheels for a large variety of tree-sitter languages).
"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea","title":"textual.widgets._text_area.TextArea class","text":"
def __init__(\nself,\ntext=\"\",\n*,\nlanguage=None,\ntheme=None,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: ScrollView

Parameters Name Type Description Default text str

The initial text to load into the TextArea.

'' language str | None

The language to use.

None theme str | None

The theme to use.

None name str | None

The name of the TextArea widget.

None id str | None

The ID of the widget, used to refer to it from Textual CSS.

None classes str | None

One or more Textual CSS compatible class names separated by spaces.

None disabled bool

True if the widget is disabled.

False"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.BINDINGS","title":"BINDINGS instance-attribute class-attribute","text":"
BINDINGS = [\nBinding(\n\"escape\",\n\"screen.focus_next\",\n\"Shift Focus\",\nshow=False,\n),\nBinding(\"up\", \"cursor_up\", \"cursor up\", show=False),\nBinding(\n\"down\", \"cursor_down\", \"cursor down\", show=False\n),\nBinding(\n\"left\", \"cursor_left\", \"cursor left\", show=False\n),\nBinding(\n\"right\", \"cursor_right\", \"cursor right\", show=False\n),\nBinding(\n\"ctrl+left\",\n\"cursor_word_left\",\n\"cursor word left\",\nshow=False,\n),\nBinding(\n\"ctrl+right\",\n\"cursor_word_right\",\n\"cursor word right\",\nshow=False,\n),\nBinding(\n\"home,ctrl+a\",\n\"cursor_line_start\",\n\"cursor line start\",\nshow=False,\n),\nBinding(\n\"end,ctrl+e\",\n\"cursor_line_end\",\n\"cursor line end\",\nshow=False,\n),\nBinding(\n\"pageup\",\n\"cursor_page_up\",\n\"cursor page up\",\nshow=False,\n),\nBinding(\n\"pagedown\",\n\"cursor_page_down\",\n\"cursor page down\",\nshow=False,\n),\nBinding(\n\"ctrl+shift+left\",\n\"cursor_word_left(True)\",\n\"cursor left word select\",\nshow=False,\n),\nBinding(\n\"ctrl+shift+right\",\n\"cursor_word_right(True)\",\n\"cursor right word select\",\nshow=False,\n),\nBinding(\n\"shift+home\",\n\"cursor_line_start(True)\",\n\"cursor line start select\",\nshow=False,\n),\nBinding(\n\"shift+end\",\n\"cursor_line_end(True)\",\n\"cursor line end select\",\nshow=False,\n),\nBinding(\n\"shift+up\",\n\"cursor_up(True)\",\n\"cursor up select\",\nshow=False,\n),\nBinding(\n\"shift+down\",\n\"cursor_down(True)\",\n\"cursor down select\",\nshow=False,\n),\nBinding(\n\"shift+left\",\n\"cursor_left(True)\",\n\"cursor left select\",\nshow=False,\n),\nBinding(\n\"shift+right\",\n\"cursor_right(True)\",\n\"cursor right select\",\nshow=False,\n),\nBinding(\"f6\", \"select_line\", \"select line\", show=False),\nBinding(\"f7\", \"select_all\", \"select all\", show=False),\nBinding(\n\"backspace\",\n\"delete_left\",\n\"delete left\",\nshow=False,\n),\nBinding(\n\"ctrl+w\",\n\"delete_word_left\",\n\"delete left to start of word\",\nshow=False,\n),\nBinding(\n\"delete,ctrl+d\",\n\"delete_right\",\n\"delete right\",\nshow=False,\n),\nBinding(\n\"ctrl+f\",\n\"delete_word_right\",\n\"delete right to start of word\",\nshow=False,\n),\nBinding(\n\"ctrl+x\", \"delete_line\", \"delete line\", show=False\n),\nBinding(\n\"ctrl+u\",\n\"delete_to_start_of_line\",\n\"delete to line start\",\nshow=False,\n),\nBinding(\n\"ctrl+k\",\n\"delete_to_end_of_line\",\n\"delete to line end\",\nshow=False,\n),\n]\n
Key(s) Description escape Focus on the next item. up Move the cursor up. down Move the cursor down. left Move the cursor left. ctrl+left Move the cursor to the start of the word. ctrl+shift+left Move the cursor to the start of the word and select. right Move the cursor right. ctrl+right Move the cursor to the end of the word. ctrl+shift+right Move the cursor to the end of the word and select. home,ctrl+a Move the cursor to the start of the line. end,ctrl+e Move the cursor to the end of the line. shift+home Move the cursor to the start of the line and select. shift+end Move the cursor to the end of the line and select. pageup Move the cursor one page up. pagedown Move the cursor one page down. shift+up Select while moving the cursor up. shift+down Select while moving the cursor down. shift+left Select while moving the cursor left. shift+right Select while moving the cursor right. backspace Delete character to the left of cursor. ctrl+w Delete from cursor to start of the word. delete,ctrl+d Delete character to the right of cursor. ctrl+f Delete from cursor to end of the word. ctrl+x Delete the current line. ctrl+u Delete from cursor to the start of the line. ctrl+k Delete from cursor to the end of the line. f6 Select the current line. f7 Select all text in the document."},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.available_languages","title":"available_languages property","text":"
available_languages: set[str]\n

A list of the names of languages available to the TextArea.

The values in this list can be assigned to the language reactive attribute of TextArea.

The returned list contains the builtin languages plus those registered via the register_language method. Builtin languages will be listed before user-registered languages, but there are no other ordering guarantees.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.available_themes","title":"available_themes property","text":"
available_themes: set[str]\n

A list of the names of the themes available to the TextArea.

The values in this list can be assigned theme reactive attribute of TextArea.

You can retrieve the full specification for a theme by passing one of the strings from this list into TextAreaTheme.get_by_name(theme_name: str).

Alternatively, you can directly retrieve a list of TextAreaTheme objects (which contain the full theme specification) by calling TextAreaTheme.builtin_themes().

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cursor_at_end_of_line","title":"cursor_at_end_of_line property","text":"
cursor_at_end_of_line: bool\n

True if and only if the cursor is at the end of a row.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cursor_at_end_of_text","title":"cursor_at_end_of_text property","text":"
cursor_at_end_of_text: bool\n

True if and only if the cursor is at the very end of the document.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cursor_at_first_line","title":"cursor_at_first_line property","text":"
cursor_at_first_line: bool\n

True if and only if the cursor is on the first line.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cursor_at_last_line","title":"cursor_at_last_line property","text":"
cursor_at_last_line: bool\n

True if and only if the cursor is on the last line.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cursor_at_start_of_line","title":"cursor_at_start_of_line property","text":"
cursor_at_start_of_line: bool\n

True if and only if the cursor is at column 0.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cursor_at_start_of_text","title":"cursor_at_start_of_text property","text":"
cursor_at_start_of_text: bool\n

True if and only if the cursor is at location (0, 0)

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cursor_blink","title":"cursor_blink instance-attribute class-attribute","text":"
cursor_blink: Reactive[bool] = reactive(True)\n

True if the cursor should blink.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cursor_location","title":"cursor_location writable property","text":"
cursor_location: Location\n

The current location of the cursor in the document.

This is a utility for accessing the end of TextArea.selection.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.document","title":"document instance-attribute","text":"
document: DocumentBase = Document(text)\n

The document this widget is currently editing.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.gutter_width","title":"gutter_width property","text":"
gutter_width: int\n

The width of the gutter (the left column containing line numbers).

Returns Type Description int

The cell-width of the line number column. If show_line_numbers is False returns 0.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.indent_type","title":"indent_type instance-attribute","text":"
indent_type: Literal['tabs', 'spaces'] = 'spaces'\n

Whether to indent using tabs or spaces.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.indent_width","title":"indent_width instance-attribute class-attribute","text":"
indent_width: Reactive[int] = reactive(4)\n

The width of tabs or the multiple of spaces to align to on pressing the tab key.

If the document currently open contains tabs that are currently visible on screen, altering this value will immediately change the display width of the visible tabs.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.is_syntax_aware","title":"is_syntax_aware property","text":"
is_syntax_aware: bool\n

True if the TextArea is currently syntax aware - i.e. it's parsing document content.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.language","title":"language instance-attribute class-attribute","text":"
language: Reactive[str | None] = language\n

The language of the TextArea.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.match_cursor_bracket","title":"match_cursor_bracket instance-attribute class-attribute","text":"
match_cursor_bracket: Reactive[bool] = reactive(True)\n

If the cursor is at a bracket, highlight the matching bracket (if found).

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.selected_text","title":"selected_text property","text":"
selected_text: str\n

The text between the start and end points of the current selection.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.selection","title":"selection instance-attribute class-attribute","text":"
selection: Reactive[Selection] = reactive(\nSelection(), always_update=True\n)\n

The selection start and end locations (zero-based line_index, offset).

This represents the cursor location and the current selection.

The Selection.end always refers to the cursor location.

If no text is selected, then Selection.end == Selection.start is True.

The text selected in the document is available via the TextArea.selected_text property.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.show_line_numbers","title":"show_line_numbers instance-attribute class-attribute","text":"
show_line_numbers: Reactive[bool] = reactive(True)\n

True to show the line number column on the left edge, otherwise False.

Changing this value will immediately re-render the TextArea.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.text","title":"text property","text":"
text: str\n

The entire text content of the document.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.theme","title":"theme instance-attribute class-attribute","text":"
theme: str | None = theme\n

The name of the theme of the TextArea as set by the user.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_down","title":"action_cursor_down method","text":"
def action_cursor_down(self, select=False):\n

Move the cursor down one cell.

Parameters Name Type Description Default select bool

If True, select the text while moving.

False"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_left","title":"action_cursor_left method","text":"
def action_cursor_left(self, select=False):\n

Move the cursor one location to the left.

If the cursor is at the left edge of the document, try to move it to the end of the previous line.

Parameters Name Type Description Default select bool

If True, select the text while moving.

False"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_line_end","title":"action_cursor_line_end method","text":"
def action_cursor_line_end(self, select=False):\n

Move the cursor to the end of the line.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_line_start","title":"action_cursor_line_start method","text":"
def action_cursor_line_start(self, select=False):\n

Move the cursor to the start of the line.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_page_down","title":"action_cursor_page_down method","text":"
def action_cursor_page_down(self):\n

Move the cursor and scroll down one page.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_page_up","title":"action_cursor_page_up method","text":"
def action_cursor_page_up(self):\n

Move the cursor and scroll up one page.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_right","title":"action_cursor_right method","text":"
def action_cursor_right(self, select=False):\n

Move the cursor one location to the right.

If the cursor is at the end of a line, attempt to go to the start of the next line.

Parameters Name Type Description Default select bool

If True, select the text while moving.

False"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_up","title":"action_cursor_up method","text":"
def action_cursor_up(self, select=False):\n

Move the cursor up one cell.

Parameters Name Type Description Default select bool

If True, select the text while moving.

False"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_word_left","title":"action_cursor_word_left method","text":"
def action_cursor_word_left(self, select=False):\n

Move the cursor left by a single word, skipping trailing whitespace.

Parameters Name Type Description Default select bool

Whether to select while moving the cursor.

False"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_cursor_word_right","title":"action_cursor_word_right method","text":"
def action_cursor_word_right(self, select=False):\n

Move the cursor right by a single word, skipping leading whitespace.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_delete_left","title":"action_delete_left method","text":"
def action_delete_left(self):\n

Deletes the character to the left of the cursor and updates the cursor location.

If there's a selection, then the selected range is deleted.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_delete_line","title":"action_delete_line method","text":"
def action_delete_line(self):\n

Deletes the lines which intersect with the selection.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_delete_right","title":"action_delete_right method","text":"
def action_delete_right(self):\n

Deletes the character to the right of the cursor and keeps the cursor at the same location.

If there's a selection, then the selected range is deleted.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_delete_to_end_of_line","title":"action_delete_to_end_of_line method","text":"
def action_delete_to_end_of_line(self):\n

Deletes from the cursor location to the end of the line.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_delete_to_start_of_line","title":"action_delete_to_start_of_line method","text":"
def action_delete_to_start_of_line(self):\n

Deletes from the cursor location to the start of the line.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_delete_word_left","title":"action_delete_word_left method","text":"
def action_delete_word_left(self):\n

Deletes the word to the left of the cursor and updates the cursor location.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_delete_word_right","title":"action_delete_word_right method","text":"
def action_delete_word_right(self):\n

Deletes the word to the right of the cursor and keeps the cursor at the same location.

Note that the location that we delete to using this action is not the same as the location we move to when we move the cursor one word to the right. This action does not skip leading whitespace, whereas cursor movement does.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_select_all","title":"action_select_all method","text":"
def action_select_all(self):\n

Select all the text in the document.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.action_select_line","title":"action_select_line method","text":"
def action_select_line(self):\n

Select all the text on the current line.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.cell_width_to_column_index","title":"cell_width_to_column_index method","text":"
def cell_width_to_column_index(self, cell_width, row_index):\n

Return the column that the cell width corresponds to on the given row.

Parameters Name Type Description Default cell_width int

The cell width to convert.

required row_index int

The index of the row to examine.

required Returns Type Description int

The column corresponding to the cell width on that row.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.clamp_visitable","title":"clamp_visitable method","text":"
def clamp_visitable(self, location):\n

Clamp the given location to the nearest visitable location.

Parameters Name Type Description Default location Location

The location to clamp.

required Returns Type Description Location

The nearest location that we could conceivably navigate to using the cursor.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.clear","title":"clear method","text":"
def clear(self):\n

Delete all text from the document.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.delete","title":"delete method","text":"
def delete(self, start, end, *, maintain_selection_offset=True):\n

Delete the text between two locations in the document.

Parameters Name Type Description Default start Location

The start location.

required end Location

The end location.

required maintain_selection_offset bool

If True, the active Selection will be updated such that the same text is selected before and after the selection, if possible. Otherwise, the cursor will jump to the end point of the edit.

True Returns Type Description EditResult

An EditResult containing information about the edit.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.edit","title":"edit method","text":"
def edit(self, edit):\n

Perform an Edit.

Parameters Name Type Description Default edit Edit

The Edit to perform.

required Returns Type Description Any

Data relating to the edit that may be useful. The data returned

Any

may be different depending on the edit performed.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.find_matching_bracket","title":"find_matching_bracket method","text":"
def find_matching_bracket(self, bracket, search_from):\n

If the character is a bracket, find the matching bracket.

Parameters Name Type Description Default bracket str

The character we're searching for the matching bracket of.

required search_from Location

The location to start the search.

required Returns Type Description Location | None

The Location of the matching bracket, or None if it's not found.

Location | None

If the character is not available for bracket matching, None is returned.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_column_width","title":"get_column_width method","text":"
def get_column_width(self, row, column):\n

Get the cell offset of the column from the start of the row.

Parameters Name Type Description Default row int

The row index.

required column int

The column index (codepoint offset from start of row).

required Returns Type Description int

The cell width of the column relative to the start of the row.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_cursor_down_location","title":"get_cursor_down_location method","text":"
def get_cursor_down_location(self):\n

Get the location the cursor will move to if it moves down.

Returns Type Description Location

The location the cursor will move to if it moves down.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_cursor_left_location","title":"get_cursor_left_location method","text":"
def get_cursor_left_location(self):\n

Get the location the cursor will move to if it moves left.

Returns Type Description Location

The location of the cursor if it moves left.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_cursor_line_end_location","title":"get_cursor_line_end_location method","text":"
def get_cursor_line_end_location(self):\n

Get the location of the end of the current line.

Returns Type Description Location

The (row, column) location of the end of the cursors current line.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_cursor_line_start_location","title":"get_cursor_line_start_location method","text":"
def get_cursor_line_start_location(self):\n

Get the location of the start of the current line.

Returns Type Description Location

The (row, column) location of the start of the cursors current line.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_cursor_right_location","title":"get_cursor_right_location method","text":"
def get_cursor_right_location(self):\n

Get the location the cursor will move to if it moves right.

Returns Type Description Location

the location the cursor will move to if it moves right.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_cursor_up_location","title":"get_cursor_up_location method","text":"
def get_cursor_up_location(self):\n

Get the location the cursor will move to if it moves up.

Returns Type Description Location

The location the cursor will move to if it moves up.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_cursor_word_left_location","title":"get_cursor_word_left_location method","text":"
def get_cursor_word_left_location(self):\n

Get the location the cursor will jump to if it goes 1 word left.

Returns Type Description Location

The location the cursor will jump on \"jump word left\".

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_cursor_word_right_location","title":"get_cursor_word_right_location method","text":"
def get_cursor_word_right_location(self):\n

Get the location the cursor will jump to if it goes 1 word right.

Returns Type Description Location

The location the cursor will jump on \"jump word right\".

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_target_document_location","title":"get_target_document_location method","text":"
def get_target_document_location(self, event):\n

Given a MouseEvent, return the row and column offset of the event in document-space.

Parameters Name Type Description Default event MouseEvent

The MouseEvent.

required Returns Type Description Location

The location of the mouse event within the document.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.get_text_range","title":"get_text_range method","text":"
def get_text_range(self, start, end):\n

Get the text between a start and end location.

Parameters Name Type Description Default start Location

The start location.

required end Location

The end location.

required Returns Type Description str

The text between start and end.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.insert","title":"insert method","text":"
def insert(\nself,\ntext,\nlocation=None,\n*,\nmaintain_selection_offset=True\n):\n

Insert text into the document.

Parameters Name Type Description Default text str

The text to insert.

required location Location | None

The location to insert text, or None to use the cursor location.

None maintain_selection_offset bool

If True, the active Selection will be updated such that the same text is selected before and after the selection, if possible. Otherwise, the cursor will jump to the end point of the edit.

True Returns Type Description EditResult

An EditResult containing information about the edit.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.load_document","title":"load_document method","text":"
def load_document(self, document):\n

Load a document into the TextArea.

Parameters Name Type Description Default document DocumentBase

The document to load into the TextArea.

required"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.load_text","title":"load_text method","text":"
def load_text(self, text):\n

Load text into the TextArea.

This will replace the text currently in the TextArea.

Parameters Name Type Description Default text str

The text to load into the TextArea.

required"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.move_cursor","title":"move_cursor method","text":"
def move_cursor(\nself,\nlocation,\nselect=False,\ncenter=False,\nrecord_width=True,\n):\n

Move the cursor to a location.

Parameters Name Type Description Default location Location

The location to move the cursor to.

required select bool

If True, select text between the old and new location.

False center bool

If True, scroll such that the cursor is centered.

False record_width bool

If True, record the cursor column cell width after navigating so that we jump back to the same width the next time we move to a row that is wide enough.

True"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.move_cursor_relative","title":"move_cursor_relative method","text":"
def move_cursor_relative(\nself,\nrows=0,\ncolumns=0,\nselect=False,\ncenter=False,\nrecord_width=True,\n):\n

Move the cursor relative to its current location.

Parameters Name Type Description Default rows int

The number of rows to move down by (negative to move up)

0 columns int

The number of columns to move right by (negative to move left)

0 select bool

If True, select text between the old and new location.

False center bool

If True, scroll such that the cursor is centered.

False record_width bool

If True, record the cursor column cell width after navigating so that we jump back to the same width the next time we move to a row that is wide enough.

True"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.record_cursor_width","title":"record_cursor_width method","text":"
def record_cursor_width(self):\n

Record the current cell width of the cursor.

This is used where we navigate up and down through rows. If we're in the middle of a row, and go down to a row with no content, then we go down to another row, we want our cursor to jump back to the same offset that we were originally at.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.register_language","title":"register_language method","text":"
def register_language(self, language, highlight_query):\n

Register a language and corresponding highlight query.

Calling this method does not change the language of the TextArea. On switching to this language (via the language reactive attribute), syntax highlighting will be performed using the given highlight query.

If a string name is supplied for a builtin supported language, then this method will update the default highlight query for that language.

Registering a language only registers it to this instance of TextArea.

Parameters Name Type Description Default language str | 'Language'

A string referring to a builtin language or a tree-sitter Language object.

required highlight_query str

The highlight query to use for syntax highlighting this language.

required"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.register_theme","title":"register_theme method","text":"
def register_theme(self, theme):\n

Register a theme for use by the TextArea.

After registering a theme, you can set themes by assigning the theme name to the TextArea.theme reactive attribute. For example text_area.theme = \"my_custom_theme\" where \"my_custom_theme\" is the name of the theme you registered.

If you supply a theme with a name that already exists that theme will be overwritten.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.replace","title":"replace method","text":"
def replace(\nself,\ninsert,\nstart,\nend,\n*,\nmaintain_selection_offset=True\n):\n

Replace text in the document with new text.

Parameters Name Type Description Default insert str

The text to insert.

required start Location

The start location

required end Location

The end location.

required maintain_selection_offset bool

If True, the active Selection will be updated such that the same text is selected before and after the selection, if possible. Otherwise, the cursor will jump to the end point of the edit.

True Returns Type Description EditResult

An EditResult containing information about the edit.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.scroll_cursor_visible","title":"scroll_cursor_visible method","text":"
def scroll_cursor_visible(self, center=False, animate=False):\n

Scroll the TextArea such that the cursor is visible on screen.

Parameters Name Type Description Default center bool

True if the cursor should be scrolled to the center.

False animate bool

True if we should animate while scrolling.

False Returns Type Description Offset

The offset that was scrolled to bring the cursor into view.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.select_all","title":"select_all method","text":"
def select_all(self):\n

Select all of the text in the TextArea.

"},{"location":"widgets/text_area/#textual.widgets._text_area.TextArea.select_line","title":"select_line method","text":"
def select_line(self, index):\n

Select all the text in the specified line.

Parameters Name Type Description Default index int

The index of the line to select (starting from 0).

required"},{"location":"widgets/text_area/#textual.widgets.text_area.Highlight","title":"Highlight module-attribute","text":"
Highlight = Tuple[StartColumn, EndColumn, HighlightName]\n

A tuple representing a syntax highlight within one line.

"},{"location":"widgets/text_area/#textual.widgets.text_area.Location","title":"Location module-attribute","text":"
Location = Tuple[int, int]\n

A location (row, column) within the document. Indexing starts at 0.

"},{"location":"widgets/text_area/#textual.widgets.text_area.Document","title":"Document class","text":"
def __init__(self, text):\n

Bases: DocumentBase

A document which can be opened in a TextArea.

"},{"location":"widgets/text_area/#textual.document._document.Document.line_count","title":"line_count property","text":"
line_count: int\n

Returns the number of lines in the document.

"},{"location":"widgets/text_area/#textual.document._document.Document.lines","title":"lines property","text":"
lines: list[str]\n

Get the document as a list of strings, where each string represents a line.

Newline characters are not included in at the end of the strings.

The newline character used in this document can be found via the Document.newline property.

"},{"location":"widgets/text_area/#textual.document._document.Document.newline","title":"newline property","text":"
newline: Newline\n

Get the Newline used in this document (e.g. ' ', ' '. etc.)

"},{"location":"widgets/text_area/#textual.document._document.Document.text","title":"text property","text":"
text: str\n

Get the text from the document.

"},{"location":"widgets/text_area/#textual.document._document.Document.get_line","title":"get_line method","text":"
def get_line(self, index):\n

Returns the line with the given index from the document.

Parameters Name Type Description Default index int

The index of the line in the document.

required Returns Type Description str

The string representing the line.

"},{"location":"widgets/text_area/#textual.document._document.Document.get_size","title":"get_size method","text":"
def get_size(self, tab_width):\n

The Size of the document, taking into account the tab rendering width.

Parameters Name Type Description Default tab_width int

The width to use for tab indents.

required Returns Type Description Size

The size (width, height) of the document.

"},{"location":"widgets/text_area/#textual.document._document.Document.get_text_range","title":"get_text_range method","text":"
def get_text_range(self, start, end):\n

Get the text that falls between the start and end locations.

Returns the text between start and end, including the appropriate line separator character as specified by Document._newline. Note that _newline is set automatically to the first line separator character found in the document.

Parameters Name Type Description Default start Location

The start location of the selection.

required end Location

The end location of the selection.

required Returns Type Description str

The text between start (inclusive) and end (exclusive).

"},{"location":"widgets/text_area/#textual.document._document.Document.replace_range","title":"replace_range method","text":"
def replace_range(self, start, end, text):\n

Replace text at the given range.

Parameters Name Type Description Default start Location

A tuple (row, column) where the edit starts.

required end Location

A tuple (row, column) where the edit ends.

required text str

The text to insert between start and end.

required Returns Type Description EditResult

The EditResult containing information about the completed replace operation.

"},{"location":"widgets/text_area/#textual.widgets.text_area.DocumentBase","title":"DocumentBase class","text":"

Bases: ABC

Describes the minimum functionality a Document implementation must provide in order to be used by the TextArea widget.

"},{"location":"widgets/text_area/#textual.document._document.DocumentBase.line_count","title":"line_count abstractmethod property","text":"
line_count: int\n

Returns the number of lines in the document.

"},{"location":"widgets/text_area/#textual.document._document.DocumentBase.newline","title":"newline abstractmethod property","text":"
newline: Newline\n

Return the line separator used in the document.

"},{"location":"widgets/text_area/#textual.document._document.DocumentBase.text","title":"text abstractmethod property","text":"
text: str\n

The text from the document as a string.

"},{"location":"widgets/text_area/#textual.document._document.DocumentBase.get_line","title":"get_line abstractmethod","text":"
def get_line(self, index):\n

Returns the line with the given index from the document.

This is used in rendering lines, and will be called by the TextArea for each line that is rendered.

Parameters Name Type Description Default index int

The index of the line in the document.

required Returns Type Description str

The str instance representing the line.

"},{"location":"widgets/text_area/#textual.document._document.DocumentBase.get_size","title":"get_size abstractmethod","text":"
def get_size(self, indent_width):\n

Get the size of the document.

The height is generally the number of lines, and the width is generally the maximum cell length of all the lines.

Parameters Name Type Description Default indent_width int

The width to use for tab characters.

required Returns Type Description Size

The Size of the document bounding box.

"},{"location":"widgets/text_area/#textual.document._document.DocumentBase.get_text_range","title":"get_text_range abstractmethod","text":"
def get_text_range(self, start, end):\n

Get the text that falls between the start and end locations.

Parameters Name Type Description Default start Location

The start location of the selection.

required end Location

The end location of the selection.

required Returns Type Description str

The text between start (inclusive) and end (exclusive).

"},{"location":"widgets/text_area/#textual.document._document.DocumentBase.query_syntax_tree","title":"query_syntax_tree method","text":"
def query_syntax_tree(\nself, query, start_point=None, end_point=None\n):\n

Query the tree-sitter syntax tree.

The default implementation always returns an empty list.

To support querying in a subclass, this must be implemented.

Parameters Name Type Description Default query 'Query'

The tree-sitter Query to perform.

required start_point tuple[int, int] | None

The (row, column byte) to start the query at.

None end_point tuple[int, int] | None

The (row, column byte) to end the query at.

None Returns Type Description list[tuple['Node', str]]

A tuple containing the nodes and text captured by the query.

"},{"location":"widgets/text_area/#textual.document._document.DocumentBase.replace_range","title":"replace_range abstractmethod","text":"
def replace_range(self, start, end, text):\n

Replace the text at the given range.

Parameters Name Type Description Default start Location

A tuple (row, column) where the edit starts.

required end Location

A tuple (row, column) where the edit ends.

required text str

The text to insert between start and end.

required Returns Type Description EditResult

The new end location after the edit is complete.

"},{"location":"widgets/text_area/#textual.widgets.text_area.Edit","title":"Edit class","text":"

Implements the Undoable protocol to replace text at some range within a document.

"},{"location":"widgets/text_area/#textual.widgets._text_area.Edit.from_location","title":"from_location instance-attribute","text":"
from_location: Location\n

The start location of the insert.

"},{"location":"widgets/text_area/#textual.widgets._text_area.Edit.maintain_selection_offset","title":"maintain_selection_offset instance-attribute","text":"
maintain_selection_offset: bool\n

If True, the selection will maintain its offset to the replacement range.

"},{"location":"widgets/text_area/#textual.widgets._text_area.Edit.text","title":"text instance-attribute","text":"
text: str\n

The text to insert. An empty string is equivalent to deletion.

"},{"location":"widgets/text_area/#textual.widgets._text_area.Edit.to_location","title":"to_location instance-attribute","text":"
to_location: Location\n

The end location of the insert

"},{"location":"widgets/text_area/#textual.widgets._text_area.Edit.after","title":"after method","text":"
def after(self, text_area):\n

Possibly update the cursor location after the widget has been refreshed.

Parameters Name Type Description Default text_area TextArea

The TextArea this operation was performed on.

required"},{"location":"widgets/text_area/#textual.widgets._text_area.Edit.do","title":"do method","text":"
def do(self, text_area):\n

Perform the edit operation.

Parameters Name Type Description Default text_area TextArea

The TextArea to perform the edit on.

required Returns Type Description EditResult

An EditResult containing information about the replace operation.

"},{"location":"widgets/text_area/#textual.widgets._text_area.Edit.undo","title":"undo method","text":"
def undo(self, text_area):\n

Undo the edit operation.

Parameters Name Type Description Default text_area TextArea

The TextArea to undo the insert operation on.

required Returns Type Description EditResult

An EditResult containing information about the replace operation.

"},{"location":"widgets/text_area/#textual.widgets.text_area.EditResult","title":"EditResult class","text":"

Contains information about an edit that has occurred.

"},{"location":"widgets/text_area/#textual.document._document.EditResult.end_location","title":"end_location instance-attribute","text":"
end_location: Location\n

The new end Location after the edit is complete.

"},{"location":"widgets/text_area/#textual.document._document.EditResult.replaced_text","title":"replaced_text instance-attribute","text":"
replaced_text: str\n

The text that was replaced.

"},{"location":"widgets/text_area/#textual.widgets.text_area.LanguageDoesNotExist","title":"LanguageDoesNotExist class","text":"

Bases: Exception

Raised when the user tries to use a language which does not exist. This means a language which is not builtin, or has not been registered.

"},{"location":"widgets/text_area/#textual.widgets.text_area.Selection","title":"Selection class","text":"

Bases: NamedTuple

A range of characters within a document from a start point to the end point. The location of the cursor is always considered to be the end point of the selection. The selection is inclusive of the minimum point and exclusive of the maximum point.

"},{"location":"widgets/text_area/#textual.document._document.Selection.end","title":"end instance-attribute class-attribute","text":"
end: Location = (0, 0)\n

The end location of the selection.

If you were to click and drag a selection inside a text-editor, this is where you finished dragging.

"},{"location":"widgets/text_area/#textual.document._document.Selection.is_empty","title":"is_empty property","text":"
is_empty: bool\n

Return True if the selection has 0 width, i.e. it's just a cursor.

"},{"location":"widgets/text_area/#textual.document._document.Selection.start","title":"start instance-attribute class-attribute","text":"
start: Location = (0, 0)\n

The start location of the selection.

If you were to click and drag a selection inside a text-editor, this is where you started dragging.

"},{"location":"widgets/text_area/#textual.document._document.Selection.cursor","title":"cursor classmethod","text":"
def cursor(cls, location):\n

Create a Selection with the same start and end point - a \"cursor\".

Parameters Name Type Description Default location Location

The location to create the zero-width Selection.

required"},{"location":"widgets/text_area/#textual.widgets.text_area.SyntaxAwareDocument","title":"SyntaxAwareDocument class","text":"
def __init__(self, text, language):\n

Bases: Document

A wrapper around a Document which also maintains a tree-sitter syntax tree when the document is edited.

The primary reason for this split is actually to keep tree-sitter stuff separate, since it isn't supported in Python 3.7. By having the tree-sitter code isolated in this subclass, it makes it easier to conditionally import. However, it does come with other design flaws (e.g. Document is required to have methods which only really make sense on SyntaxAwareDocument).

If you're reading this and Python 3.7 is no longer supported by Textual, consider merging this subclass into the Document superclass.

Parameters Name Type Description Default text str

The initial text contained in the document.

required language str | Language

The language to use. You can pass a string to use a supported language, or pass in your own tree-sitter Language object.

required"},{"location":"widgets/text_area/#textual.document._syntax_aware_document.SyntaxAwareDocument.language","title":"language instance-attribute","text":"
language: Language | None = None\n

The tree-sitter Language or None if tree-sitter is unavailable.

"},{"location":"widgets/text_area/#textual.document._syntax_aware_document.SyntaxAwareDocument.get_line","title":"get_line method","text":"
def get_line(self, line_index):\n

Return the string representing the line, not including new line characters.

Parameters Name Type Description Default line_index int

The index of the line.

required Returns Type Description str

The string representing the line.

"},{"location":"widgets/text_area/#textual.document._syntax_aware_document.SyntaxAwareDocument.prepare_query","title":"prepare_query method","text":"
def prepare_query(self, query):\n

Prepare a tree-sitter tree query.

Queries should be prepared once, then reused.

To execute a query, call query_syntax_tree.

Returns Type Description Query | None

The prepared query.

"},{"location":"widgets/text_area/#textual.document._syntax_aware_document.SyntaxAwareDocument.query_syntax_tree","title":"query_syntax_tree method","text":"
def query_syntax_tree(\nself, query, start_point=None, end_point=None\n):\n

Query the tree-sitter syntax tree.

The default implementation always returns an empty list.

To support querying in a subclass, this must be implemented.

Parameters Name Type Description Default query Query

The tree-sitter Query to perform.

required start_point tuple[int, int] | None

The (row, column byte) to start the query at.

None end_point tuple[int, int] | None

The (row, column byte) to end the query at.

None Returns Type Description list[tuple['Node', str]]

A tuple containing the nodes and text captured by the query.

"},{"location":"widgets/text_area/#textual.document._syntax_aware_document.SyntaxAwareDocument.replace_range","title":"replace_range method","text":"
def replace_range(self, start, end, text):\n

Replace text at the given range.

Parameters Name Type Description Default start Location

A tuple (row, column) where the edit starts.

required end Location

A tuple (row, column) where the edit ends.

required text str

The text to insert between start and end.

required Returns Type Description EditResult

The new end location after the edit is complete.

"},{"location":"widgets/text_area/#textual.widgets.text_area.TextAreaTheme","title":"TextAreaTheme class","text":"

A theme for the TextArea widget.

Allows theming the general widget (gutter, selections, cursor, and so on) and mapping of tree-sitter tokens to Rich styles.

For example, consider the following snippet from the markdown.scm highlight query file. We've assigned the heading_content token type to the name heading.

(heading_content) @heading\n

Now, we can map this heading name to a Rich style, and it will be styled as such in the TextArea, assuming a parser which returns a heading_content node is used (as will be the case when language=\"markdown\").

TextAreaTheme('my_theme', syntax_styles={'heading': Style(color='cyan', bold=True)})\n

We can register this theme with our TextArea using the TextArea.register_theme method, and headings in our markdown files will be styled bold cyan.

"},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.base_style","title":"base_style instance-attribute class-attribute","text":"
base_style: Style | None = None\n

The background style of the text area. If None the parent style will be used.

"},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.bracket_matching_style","title":"bracket_matching_style instance-attribute class-attribute","text":"
bracket_matching_style: Style | None = None\n

The style to apply to matching brackets. If None, a legible Style will be generated.

"},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.cursor_line_gutter_style","title":"cursor_line_gutter_style instance-attribute class-attribute","text":"
cursor_line_gutter_style: Style | None = None\n

The style to apply to the gutter of the line the cursor is on. If None, a legible Style will be generated.

"},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.cursor_line_style","title":"cursor_line_style instance-attribute class-attribute","text":"
cursor_line_style: Style | None = None\n

The style to apply to the line the cursor is on.

"},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.cursor_style","title":"cursor_style instance-attribute class-attribute","text":"
cursor_style: Style | None = None\n

The style of the cursor. If None, a legible Style will be generated.

"},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.gutter_style","title":"gutter_style instance-attribute class-attribute","text":"
gutter_style: Style | None = None\n

The style of the gutter. If None, a legible Style will be generated.

"},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.name","title":"name instance-attribute","text":"
name: str\n

The name of the theme.

"},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.selection_style","title":"selection_style instance-attribute class-attribute","text":"
selection_style: Style | None = None\n

The style of the selection. If None a default selection Style will be generated.

"},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.syntax_styles","title":"syntax_styles instance-attribute class-attribute","text":"
syntax_styles: dict[str, Style] = field(\ndefault_factory=dict\n)\n

The mapping of tree-sitter names from the highlight_query to Rich styles.

"},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.builtin_themes","title":"builtin_themes classmethod","text":"
def builtin_themes(cls):\n

Get a list of all builtin TextAreaThemes.

Returns Type Description list[TextAreaTheme]

A list of all builtin TextAreaThemes.

"},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.default","title":"default classmethod","text":"
def default(cls):\n

Get the default syntax theme.

Returns Type Description TextAreaTheme

The default TextAreaTheme (probably \"monokai\").

"},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.get_builtin_theme","title":"get_builtin_theme classmethod","text":"
def get_builtin_theme(cls, theme_name):\n

Get a TextAreaTheme by name.

Given a theme_name, return the corresponding TextAreaTheme object.

Parameters Name Type Description Default theme_name str

The name of the theme.

required Returns Type Description 'TextAreaTheme' | None

The TextAreaTheme corresponding to the name or None if the theme isn't

'TextAreaTheme' | None

found.

"},{"location":"widgets/text_area/#textual._text_area_theme.TextAreaTheme.get_highlight","title":"get_highlight method","text":"
def get_highlight(self, name):\n

Return the Rich style corresponding to the name defined in the tree-sitter highlight query for the current theme.

Parameters Name Type Description Default name str

The name of the highlight.

required Returns Type Description Style | None

The Style to use for this highlight, or None if no style.

"},{"location":"widgets/text_area/#textual.widgets.text_area.ThemeDoesNotExist","title":"ThemeDoesNotExist class","text":"

Bases: Exception

Raised when the user tries to use a theme which does not exist. This means a theme which is not builtin, or has not been registered.

"},{"location":"widgets/toast/","title":"Toast","text":"

Added in version 0.30.0

A widget which displays a notification message.

  • Focusable
  • Container

Note that Toast isn't designed to be used directly in your applications, but it is instead used by notify to display a message when using Textual's built-in notification system.

"},{"location":"widgets/toast/#styling","title":"Styling","text":"

You can customize the style of Toasts by targeting the Toast CSS type. For example:

Toast {\npadding: 3;\n}\n

The three severity levels also have corresponding classes, allowing you to target the different styles of notification. They are:

  • -information
  • -warning
  • -error

If you wish to tailor the notifications for your application you can add rules to your CSS like this:

Toast.-information {\n/* Styling here. */\n}\nToast.-warning {\n/* Styling here. */\n}\nToast.-error {\n/* Styling here. */\n}\n

You can customize just the title wih the toast--title class. The following would make the title italic for an information toast:

Toast.-information .toast--title {\ntext-style: italic;\n}\n
"},{"location":"widgets/toast/#example","title":"Example","text":"Outputtoast.py

ToastApp \u258e\u258a \u258eIt's\u00a0an\u00a0older\u00a0code,\u00a0sir,\u00a0but\u00a0it\u00a0\u258a \u258echecks\u00a0out.\u258a \u258e\u258a \u258e\u258a \u258ePossible\u00a0trap\u00a0detected\u258a \u258eNow\u00a0witness\u00a0the\u00a0firepower\u00a0of\u00a0this\u258a \u258efully\u00a0ARMED\u00a0and\u00a0OPERATIONAL\u258a \u258ebattle\u00a0station!\u258a \u258e\u258a \u258e\u258a \u258eIt's\u00a0a\u00a0trap!\u258a \u258e\u258a \u258e\u258a \u258eIt's\u00a0against\u00a0my\u00a0programming\u00a0to\u00a0\u258a \u258eimpersonate\u00a0a\u00a0deity.\u258a \u258e\u258a

from textual.app import App\nclass ToastApp(App[None]):\ndef on_mount(self) -> None:\n# Show an information notification.\nself.notify(\"It's an older code, sir, but it checks out.\")\n# Show a warning. Note that Textual's notification system allows\n# for the use of Rich console markup.\nself.notify(\n\"Now witness the firepower of this fully \"\n\"[b]ARMED[/b] and [i][b]OPERATIONAL[/b][/i] battle station!\",\ntitle=\"Possible trap detected\",\nseverity=\"warning\",\n)\n# Show an error. Set a longer timeout so it's noticed.\nself.notify(\"It's a trap!\", severity=\"error\", timeout=10)\n# Show an information notification, but without any sort of title.\nself.notify(\"It's against my programming to impersonate a deity.\", title=\"\")\nif __name__ == \"__main__\":\nToastApp().run()\n
"},{"location":"widgets/toast/#reactive-attributes","title":"Reactive Attributes","text":"

This widget has no reactive attributes.

"},{"location":"widgets/toast/#messages","title":"Messages","text":"

This widget posts no messages.

"},{"location":"widgets/toast/#bindings","title":"Bindings","text":"

This widget has no bindings.

"},{"location":"widgets/toast/#component-classes","title":"Component Classes","text":"

The toast widget provides the following component classes:

Class Description toast--title Targets the title of the toast."},{"location":"widgets/toast/#textual.widgets._toast","title":"textual.widgets._toast","text":"

Widgets for showing notification messages in toasts.

"},{"location":"widgets/toast/#textual.widgets._toast.Toast","title":"Toast class","text":"
def __init__(self, notification):\n

Bases: Static

A widget for displaying short-lived notifications.

Parameters Name Type Description Default notification Notification

The notification to show in the toast.

required"},{"location":"widgets/toast/#textual.widgets._toast.Toast.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
COMPONENT_CLASSES: set[str] = {'toast--title'}\n
Class Description toast--title Targets the title of the toast."},{"location":"widgets/toast/#textual.widgets._toast.ToastHolder","title":"ToastHolder class","text":"

Bases: Container

Container that holds a single toast.

Used to control the alignment of each of the toasts in the main toast container.

"},{"location":"widgets/toast/#textual.widgets._toast.ToastRack","title":"ToastRack class","text":"

Bases: Container

A container for holding toasts.

"},{"location":"widgets/toast/#textual.widgets._toast.ToastRack.show","title":"show method","text":"
def show(self, notifications):\n

Show the notifications as toasts.

Parameters Name Type Description Default notifications Notifications

The notifications to show.

required"},{"location":"widgets/tree/","title":"Tree","text":"

Added in version 0.6.0

A tree control widget.

  • Focusable
  • Container
"},{"location":"widgets/tree/#example","title":"Example","text":"

The example below creates a simple tree.

Outputtree.py

TreeApp \u25bc\u00a0Dune \u2517\u2501\u2501\u00a0\u25bc\u00a0Characters \u2523\u2501\u2501\u00a0Paul \u2523\u2501\u2501\u00a0Jessica \u2517\u2501\u2501\u00a0Chani

from textual.app import App, ComposeResult\nfrom textual.widgets import Tree\nclass TreeApp(App):\ndef compose(self) -> ComposeResult:\ntree: Tree[dict] = Tree(\"Dune\")\ntree.root.expand()\ncharacters = tree.root.add(\"Characters\", expand=True)\ncharacters.add_leaf(\"Paul\")\ncharacters.add_leaf(\"Jessica\")\ncharacters.add_leaf(\"Chani\")\nyield tree\nif __name__ == \"__main__\":\napp = TreeApp()\napp.run()\n

Tree widgets have a \"root\" attribute which is an instance of a TreeNode. Call add() or add_leaf() to add new nodes underneath the root. Both these methods return a TreeNode for the child which you can use to add additional levels.

"},{"location":"widgets/tree/#reactive-attributes","title":"Reactive Attributes","text":"Name Type Default Description show_root bool True Show the root node. show_guides bool True Show guide lines between levels. guide_depth int 4 Amount of indentation between parent and child."},{"location":"widgets/tree/#messages","title":"Messages","text":"
  • Tree.NodeCollapsed
  • Tree.NodeExpanded
  • Tree.NodeHighlighted
  • Tree.NodeSelected
"},{"location":"widgets/tree/#bindings","title":"Bindings","text":"

The tree widget defines the following bindings:

Key(s) Description enter Select the current item. space Toggle the expand/collapsed space of the current item. up Move the cursor up. down Move the cursor down."},{"location":"widgets/tree/#component-classes","title":"Component Classes","text":"

The tree widget provides the following component classes:

Class Description tree--cursor Targets the cursor. tree--guides Targets the indentation guides. tree--guides-hover Targets the indentation guides under the cursor. tree--guides-selected Targets the indentation guides that are selected. tree--highlight Targets the highlighted items. tree--highlight-line Targets the lines under the cursor. tree--label Targets the (text) labels of the items."},{"location":"widgets/tree/#textual.widgets.Tree","title":"textual.widgets.Tree class","text":"
def __init__(\nself,\nlabel,\ndata=None,\n*,\nname=None,\nid=None,\nclasses=None,\ndisabled=False\n):\n

Bases: Generic[TreeDataType], ScrollView

A widget for displaying and navigating data in a tree.

Parameters Name Type Description Default label TextType

The label of the root node of the tree.

required data TreeDataType | None

The optional data to associate with the root node of the tree.

None name str | None

The name of the Tree.

None id str | None

The ID of the tree in the DOM.

None classes str | None

The CSS classes of the tree.

None disabled bool

Whether the tree is disabled or not.

False"},{"location":"widgets/tree/#textual.widgets._tree.Tree.BINDINGS","title":"BINDINGS class-attribute","text":"
BINDINGS: list[BindingType] = [\nBinding(\"enter\", \"select_cursor\", \"Select\", show=False),\nBinding(\"space\", \"toggle_node\", \"Toggle\", show=False),\nBinding(\"up\", \"cursor_up\", \"Cursor Up\", show=False),\nBinding(\n\"down\", \"cursor_down\", \"Cursor Down\", show=False\n),\n]\n
Key(s) Description enter Select the current item. space Toggle the expand/collapsed space of the current item. up Move the cursor up. down Move the cursor down."},{"location":"widgets/tree/#textual.widgets._tree.Tree.COMPONENT_CLASSES","title":"COMPONENT_CLASSES class-attribute","text":"
COMPONENT_CLASSES: set[str] = {\n\"tree--cursor\",\n\"tree--guides\",\n\"tree--guides-hover\",\n\"tree--guides-selected\",\n\"tree--highlight\",\n\"tree--highlight-line\",\n\"tree--label\",\n}\n
Class Description tree--cursor Targets the cursor. tree--guides Targets the indentation guides. tree--guides-hover Targets the indentation guides under the cursor. tree--guides-selected Targets the indentation guides that are selected. tree--highlight Targets the highlighted items. tree--highlight-line Targets the lines under the cursor. tree--label Targets the (text) labels of the items."},{"location":"widgets/tree/#textual.widgets._tree.Tree.auto_expand","title":"auto_expand instance-attribute class-attribute","text":"
auto_expand = var(True)\n

Auto expand tree nodes when clicked.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.cursor_line","title":"cursor_line instance-attribute class-attribute","text":"
cursor_line = var(-1, always_update=True)\n

The line with the cursor, or -1 if no cursor.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.cursor_node","title":"cursor_node property","text":"
cursor_node: TreeNode[TreeDataType] | None\n

The currently selected node, or None if no selection.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.guide_depth","title":"guide_depth instance-attribute class-attribute","text":"
guide_depth = reactive(4, init=False)\n

The indent depth of tree nodes.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.hover_line","title":"hover_line instance-attribute class-attribute","text":"
hover_line = var(-1)\n

The line number under the mouse pointer, or -1 if not under the mouse pointer.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.last_line","title":"last_line property","text":"
last_line: int\n

The index of the last line.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.root","title":"root instance-attribute","text":"
root = self._add_node(None, text_label, data)\n

The root node of the tree.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.show_guides","title":"show_guides instance-attribute class-attribute","text":"
show_guides = reactive(True)\n

Enable display of tree guide lines.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.show_root","title":"show_root instance-attribute class-attribute","text":"
show_root = reactive(True)\n

Show the root of the tree.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeCollapsed","title":"NodeCollapsed class","text":"
def __init__(self, node):\n

Bases: Generic[EventTreeDataType], Message

Event sent when a node is collapsed.

Can be handled using on_tree_node_collapsed in a subclass of Tree or in a parent node in the DOM.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeCollapsed.control","title":"control property","text":"
control: Tree[EventTreeDataType]\n

The tree that sent the message.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeCollapsed.node","title":"node instance-attribute","text":"
node: TreeNode[EventTreeDataType] = node\n

The node that was collapsed.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeExpanded","title":"NodeExpanded class","text":"
def __init__(self, node):\n

Bases: Generic[EventTreeDataType], Message

Event sent when a node is expanded.

Can be handled using on_tree_node_expanded in a subclass of Tree or in a parent node in the DOM.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeExpanded.control","title":"control property","text":"
control: Tree[EventTreeDataType]\n

The tree that sent the message.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeExpanded.node","title":"node instance-attribute","text":"
node: TreeNode[EventTreeDataType] = node\n

The node that was expanded.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeHighlighted","title":"NodeHighlighted class","text":"
def __init__(self, node):\n

Bases: Generic[EventTreeDataType], Message

Event sent when a node is highlighted.

Can be handled using on_tree_node_highlighted in a subclass of Tree or in a parent node in the DOM.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeHighlighted.control","title":"control property","text":"
control: Tree[EventTreeDataType]\n

The tree that sent the message.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeHighlighted.node","title":"node instance-attribute","text":"
node: TreeNode[EventTreeDataType] = node\n

The node that was highlighted.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeSelected","title":"NodeSelected class","text":"
def __init__(self, node):\n

Bases: Generic[EventTreeDataType], Message

Event sent when a node is selected.

Can be handled using on_tree_node_selected in a subclass of Tree or in a parent node in the DOM.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeSelected.control","title":"control property","text":"
control: Tree[EventTreeDataType]\n

The tree that sent the message.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.NodeSelected.node","title":"node instance-attribute","text":"
node: TreeNode[EventTreeDataType] = node\n

The node that was selected.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.UnknownNodeID","title":"UnknownNodeID class","text":"

Bases: Exception

Exception raised when referring to an unknown TreeNode ID.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.action_cursor_down","title":"action_cursor_down method","text":"
def action_cursor_down(self):\n

Move the cursor down one node.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.action_cursor_up","title":"action_cursor_up method","text":"
def action_cursor_up(self):\n

Move the cursor up one node.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.action_page_down","title":"action_page_down method","text":"
def action_page_down(self):\n

Move the cursor down a page's-worth of nodes.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.action_page_up","title":"action_page_up method","text":"
def action_page_up(self):\n

Move the cursor up a page's-worth of nodes.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.action_scroll_end","title":"action_scroll_end method","text":"
def action_scroll_end(self):\n

Move the cursor to the bottom of the tree.

Note

Here bottom means vertically, not branch depth.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.action_scroll_home","title":"action_scroll_home method","text":"
def action_scroll_home(self):\n

Move the cursor to the top of the tree.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.action_select_cursor","title":"action_select_cursor method","text":"
def action_select_cursor(self):\n

Cause a select event for the target node.

Note

If auto_expand is True use of this action on a non-leaf node will cause both an expand/collapse event to occur, as well as a selected event.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.action_toggle_node","title":"action_toggle_node method","text":"
def action_toggle_node(self):\n

Toggle the expanded state of the target node.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.clear","title":"clear method","text":"
def clear(self):\n

Clear all nodes under root.

Returns Type Description Self

The Tree instance.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.get_label_width","title":"get_label_width method","text":"
def get_label_width(self, node):\n

Get the width of the nodes label.

The default behavior is to call render_node and return the cell length. This method may be overridden in a sub-class if it can be done more efficiently.

Parameters Name Type Description Default node TreeNode[TreeDataType]

A node.

required Returns Type Description int

Width in cells.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.get_node_at_line","title":"get_node_at_line method","text":"
def get_node_at_line(self, line_no):\n

Get the node for a given line.

Parameters Name Type Description Default line_no int

A line number.

required Returns Type Description TreeNode[TreeDataType] | None

A tree node, or None if there is no node at that line.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.get_node_by_id","title":"get_node_by_id method","text":"
def get_node_by_id(self, node_id):\n

Get a tree node by its ID.

Parameters Name Type Description Default node_id NodeID

The ID of the node to get.

required Returns Type Description TreeNode[TreeDataType]

The node associated with that ID.

Raises Type Description Tree.UnknownID

Raised if the TreeNode ID is unknown.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.process_label","title":"process_label method","text":"
def process_label(self, label):\n

Process a str or Text value into a label.

Maybe overridden in a subclass to change how labels are rendered.

Parameters Name Type Description Default label TextType

Label.

required Returns Type Description Text

A Rich Text object.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.refresh_line","title":"refresh_line method","text":"
def refresh_line(self, line):\n

Refresh (repaint) a given line in the tree.

Parameters Name Type Description Default line int

Line number.

required"},{"location":"widgets/tree/#textual.widgets._tree.Tree.render_label","title":"render_label method","text":"
def render_label(self, node, base_style, style):\n

Render a label for the given node. Override this to modify how labels are rendered.

Parameters Name Type Description Default node TreeNode[TreeDataType]

A tree node.

required base_style Style

The base style of the widget.

required style Style

The additional style for the label.

required Returns Type Description Text

A Rich Text object containing the label.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.reset","title":"reset method","text":"
def reset(self, label, data=None):\n

Clear the tree and reset the root node.

Parameters Name Type Description Default label TextType

The label for the root node.

required data TreeDataType | None

Optional data for the root node.

None Returns Type Description Self

The Tree instance.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.scroll_to_line","title":"scroll_to_line method","text":"
def scroll_to_line(self, line, animate=True):\n

Scroll to the given line.

Parameters Name Type Description Default line int

A line number.

required animate bool

Enable animation.

True"},{"location":"widgets/tree/#textual.widgets._tree.Tree.scroll_to_node","title":"scroll_to_node method","text":"
def scroll_to_node(self, node, animate=True):\n

Scroll to the given node.

Parameters Name Type Description Default node TreeNode[TreeDataType]

Node to scroll in to view.

required animate bool

Animate scrolling.

True"},{"location":"widgets/tree/#textual.widgets._tree.Tree.select_node","title":"select_node method","text":"
def select_node(self, node):\n

Move the cursor to the given node, or reset cursor.

Parameters Name Type Description Default node TreeNode[TreeDataType] | None

A tree node, or None to reset cursor.

required"},{"location":"widgets/tree/#textual.widgets._tree.Tree.validate_cursor_line","title":"validate_cursor_line method","text":"
def validate_cursor_line(self, value):\n

Prevent cursor line from going outside of range.

Parameters Name Type Description Default value int

The value to test.

required Return

A valid version of the given value.

"},{"location":"widgets/tree/#textual.widgets._tree.Tree.validate_guide_depth","title":"validate_guide_depth method","text":"
def validate_guide_depth(self, value):\n

Restrict guide depth to reasonable range.

Parameters Name Type Description Default value int

The value to test.

required Return

A valid version of the given value.

"},{"location":"widgets/tree/#textual.widgets.tree.TreeNode","title":"textual.widgets.tree.TreeNode class","text":"
def __init__(\nself,\ntree,\nparent,\nid,\nlabel,\ndata=None,\n*,\nexpanded=True,\nallow_expand=True\n):\n

Bases: Generic[TreeDataType]

An object that represents a \"node\" in a tree control.

Parameters Name Type Description Default tree Tree[TreeDataType]

The tree that the node is being attached to.

required parent TreeNode[TreeDataType] | None

The parent node that this node is being attached to.

required id NodeID

The ID of the node.

required label Text

The label for the node.

required data TreeDataType | None

Optional data to associate with the node.

None expanded bool

Should the node be attached in an expanded state?

True allow_expand bool

Should the node allow being expanded by the user?

True"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.allow_expand","title":"allow_expand writable property","text":"
allow_expand: bool\n

Is this node allowed to expand?

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.children","title":"children property","text":"
children: TreeNodes[TreeDataType]\n

The child nodes of a TreeNode.

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.data","title":"data instance-attribute","text":"
data = data\n

Optional data associated with the tree node.

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.id","title":"id property","text":"
id: NodeID\n

The ID of the node.

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.is_expanded","title":"is_expanded property","text":"
is_expanded: bool\n

Is the node expanded?

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.is_last","title":"is_last property","text":"
is_last: bool\n

Is this the last child node of its parent?

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.is_root","title":"is_root property","text":"
is_root: bool\n

Is this node the root of the tree?

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.label","title":"label writable property","text":"
label: TextType\n

The label for the node.

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.line","title":"line property","text":"
line: int\n

The line number for this node, or -1 if it is not displayed.

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.parent","title":"parent property","text":"
parent: TreeNode[TreeDataType] | None\n

The parent of the node.

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.tree","title":"tree property","text":"
tree: Tree[TreeDataType]\n

The tree that this node is attached to.

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.RemoveRootError","title":"RemoveRootError class","text":"

Bases: Exception

Exception raised when trying to remove a tree's root node.

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.add","title":"add method","text":"
def add(\nself,\nlabel,\ndata=None,\n*,\nexpand=False,\nallow_expand=True\n):\n

Add a node to the sub-tree.

Parameters Name Type Description Default label TextType

The new node's label.

required data TreeDataType | None

Data associated with the new node.

None expand bool

Node should be expanded.

False allow_expand bool

Allow use to expand the node via keyboard or mouse.

True Returns Type Description TreeNode[TreeDataType]

A new Tree node

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.add_leaf","title":"add_leaf method","text":"
def add_leaf(self, label, data=None):\n

Add a 'leaf' node (a node that can not expand).

Parameters Name Type Description Default label TextType

Label for the node.

required data TreeDataType | None

Optional data.

None Returns Type Description TreeNode[TreeDataType]

New node.

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.collapse","title":"collapse method","text":"
def collapse(self):\n

Collapse the node (hide its children).

Returns Type Description Self

The TreeNode instance.

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.collapse_all","title":"collapse_all method","text":"
def collapse_all(self):\n

Collapse the node (hide its children) and all those below it.

Returns Type Description Self

The TreeNode instance.

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.expand","title":"expand method","text":"
def expand(self):\n

Expand the node (show its children).

Returns Type Description Self

The TreeNode instance.

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.expand_all","title":"expand_all method","text":"
def expand_all(self):\n

Expand the node (show its children) and all those below it.

Returns Type Description Self

The TreeNode instance.

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.remove","title":"remove method","text":"
def remove(self):\n

Remove this node from the tree.

Raises Type Description TreeNode.RemoveRootError

If there is an attempt to remove the root.

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.remove_children","title":"remove_children method","text":"
def remove_children(self):\n

Remove any child nodes of this node.

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.set_label","title":"set_label method","text":"
def set_label(self, label):\n

Set a new label for the node.

Parameters Name Type Description Default label TextType

A str or Text object with the new label.

required"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.toggle","title":"toggle method","text":"
def toggle(self):\n

Toggle the node's expanded state.

Returns Type Description Self

The TreeNode instance.

"},{"location":"widgets/tree/#textual.widgets._tree.TreeNode.toggle_all","title":"toggle_all method","text":"
def toggle_all(self):\n

Toggle the node's expanded state and make all those below it match.

Returns Type Description Self

The TreeNode instance.

"},{"location":"blog/archive/2023/","title":"2023","text":""},{"location":"blog/archive/2022/","title":"2022","text":""},{"location":"blog/category/release/","title":"Release","text":""},{"location":"blog/category/devlog/","title":"DevLog","text":""},{"location":"blog/category/news/","title":"News","text":""},{"location":"blog/page/2/","title":"Textual Blog","text":""},{"location":"blog/page/3/","title":"Textual Blog","text":""},{"location":"blog/page/4/","title":"Textual Blog","text":""},{"location":"blog/archive/2023/page/2/","title":"2023","text":""},{"location":"blog/archive/2023/page/3/","title":"2023","text":""},{"location":"blog/archive/2022/page/2/","title":"2022","text":""},{"location":"blog/category/release/page/2/","title":"Release","text":""},{"location":"blog/category/devlog/page/2/","title":"DevLog","text":""}]} \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml index 9d37b62c1a..500e851ddb 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -2,1252 +2,1252 @@ https://textual.textualize.io/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/FAQ/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/getting_started/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/help/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/roadmap/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/tutorial/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/widget_gallery/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/api/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/api/app/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/api/await_remove/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/api/binding/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/api/color/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/api/command/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/api/containers/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/api/content_switcher/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/api/coordinate/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/api/dom_node/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/api/errors/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/api/events/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/api/filter/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/api/fuzzy_matcher/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/api/geometry/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/api/logger/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/api/logging/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/api/map_geometry/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/api/message/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/api/message_pump/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/api/on/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/api/pilot/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/api/query/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/api/reactive/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/api/screen/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/api/scroll_view/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/api/scrollbar/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/api/strip/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/api/suggester/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/api/system_commands_source/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/api/timer/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/api/types/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/api/validation/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/api/walk/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/api/widget/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/api/work/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/api/worker/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/api/worker_manager/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/2023/03/15/no-async-async-with-python/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/2022/12/08/be-the-keymaster/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/2022/12/30/a-better-asyncio-sleep-for-windows-to-fix-animation/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/2023/02/11/the-heisenbug-lurking-in-your-async-code/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/2023/03/08/overhead-of-python-asyncio-tasks/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/2022/12/20/a-year-of-building-for-the-terminal/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/2022/11/06/new-blog/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/2023/01/09/so-youre-looking-for-a-wee-bit-of-textual-help/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/2022/11/22/what-i-learned-from-my-first-non-trivial-pr/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/2023/07/29/pull-requests-are-cake-or-puppies/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/2023/02/15/textual-0110-adds-a-beautiful-markdown-widget/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/2023/02/24/textual-0120-adds-syntactical-sugar-and-batch-updates/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/2023/03/09/textual-0140-shakes-up-posting-messages/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/2023/03/13/textual-0150-adds-a-tabs-widget/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/2023/03/22/textual-0160-adds-tabbedcontent-and-border-titles/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/2023/03/29/textual-0170-adds-translucent-screens-and-option-list/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/2023/04/04/textual-0180-adds-api-for-managing-concurrent-workers/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/2023/05/03/textual-0230-improves-message-handling/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/2023/05/08/textual-0240-adds-a-select-control/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/2023/06/01/textual-adds-sparklines-selection-list-input-validation-and-tool-tips/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/2023/07/03/textual-0290-refactors-dev-tools/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/2023/07/17/textual-0300-adds-desktop-style-notifications/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/2023/09/21/textual-0380-adds-a-syntax-aware-textarea/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/2022/11/08/version-040/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/2022/12/11/version-060/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/2023/09/15/textual-0370-adds-a-command-palette/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/2022/12/07/letting-your-cook-multitask-while-bringing-water-to-a-boil/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/2023/07/27/using-rich-inspect-to-interrogate-python-objects/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/2022/11/24/spinners-and-progress-bars-in-textual/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/2022/11/20/stealing-open-source-code-from-textual/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/2023/09/18/things-i-learned-while-building-textuals-textarea/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/2023/09/06/what-is-textual-web/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/2023/06/06/to-tui-or-not-to-tui/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/css_types/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/css_types/border/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/css_types/color/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/css_types/horizontal/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/css_types/integer/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/css_types/name/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/css_types/number/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/css_types/overflow/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/css_types/percentage/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/css_types/scalar/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/css_types/text_align/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/css_types/text_style/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/css_types/vertical/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/events/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/events/blur/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/events/click/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/events/descendant_blur/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/events/descendant_focus/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/events/enter/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/events/focus/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/events/hide/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/events/key/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/events/leave/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/events/load/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/events/mount/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/events/mouse_capture/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/events/mouse_down/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/events/mouse_move/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/events/mouse_release/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/events/mouse_scroll_down/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/events/mouse_scroll_up/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/events/mouse_up/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/events/paste/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/events/resize/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/events/screen_resume/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/events/screen_suspend/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/events/show/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/examples/styles/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/guide/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/guide/CSS/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/guide/actions/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/guide/animation/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/guide/app/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/guide/command_palette/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/guide/design/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/guide/devtools/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/guide/events/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/guide/input/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/guide/layout/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/guide/queries/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/guide/reactivity/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/guide/screens/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/guide/styles/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/guide/testing/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/guide/widgets/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/guide/workers/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/how-to/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/how-to/center-things/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/how-to/design-a-layout/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/reference/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/align/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/background/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/border/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/border_subtitle_align/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/border_subtitle_background/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/border_subtitle_color/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/border_subtitle_style/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/border_title_align/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/border_title_background/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/border_title_color/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/border_title_style/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/box_sizing/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/color/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/content_align/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/display/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/dock/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/height/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/layer/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/layers/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/layout/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/margin/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/max_height/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/max_width/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/min_height/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/min_width/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/offset/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/opacity/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/outline/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/overflow/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/padding/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/scrollbar_gutter/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/scrollbar_size/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/text_align/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/text_opacity/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/text_style/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/tint/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/visibility/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/width/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/grid/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/grid/column_span/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/grid/grid_columns/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/grid/grid_gutter/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/grid/grid_rows/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/grid/grid_size/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/grid/row_span/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/links/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/links/link_background/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/links/link_color/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/links/link_hover_background/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/links/link_hover_color/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/links/link_hover_style/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/links/link_style/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/scrollbar_colors/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/scrollbar_colors/scrollbar_background/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/scrollbar_colors/scrollbar_background_active/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/scrollbar_colors/scrollbar_background_hover/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/scrollbar_colors/scrollbar_color/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/scrollbar_colors/scrollbar_color_active/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/scrollbar_colors/scrollbar_color_hover/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/styles/scrollbar_colors/scrollbar_corner_color/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/widgets/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/widgets/button/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/widgets/checkbox/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/widgets/collapsible/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/widgets/content_switcher/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/widgets/data_table/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/widgets/digits/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/widgets/directory_tree/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/widgets/footer/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/widgets/header/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/widgets/input/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/widgets/label/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/widgets/list_item/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/widgets/list_view/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/widgets/loading_indicator/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/widgets/log/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/widgets/markdown/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/widgets/markdown_viewer/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/widgets/option_list/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/widgets/placeholder/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/widgets/pretty/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/widgets/progress_bar/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/widgets/radiobutton/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/widgets/radioset/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/widgets/rich_log/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/widgets/rule/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/widgets/select/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/widgets/selection_list/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/widgets/sparkline/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/widgets/static/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/widgets/switch/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/widgets/tabbed_content/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/widgets/tabs/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/widgets/text_area/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/widgets/toast/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/widgets/tree/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/archive/2023/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/archive/2022/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/category/release/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/category/devlog/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/category/news/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/page/2/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/page/3/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/page/4/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/archive/2023/page/2/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/archive/2023/page/3/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/archive/2022/page/2/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/category/release/page/2/ - 2023-09-21 + 2023-09-22 daily https://textual.textualize.io/blog/category/devlog/page/2/ - 2023-09-21 + 2023-09-22 daily \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz index 0a758367e3..5439efce27 100644 Binary files a/sitemap.xml.gz and b/sitemap.xml.gz differ diff --git a/styles/align/index.html b/styles/align/index.html index fee1d0a7bc..bc3a830764 100644 --- a/styles/align/index.html +++ b/styles/align/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/background/index.html b/styles/background/index.html index c3f4a70079..c613cb2072 100644 --- a/styles/background/index.html +++ b/styles/background/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/border/index.html b/styles/border/index.html index 1485744f23..a82e2992ba 100644 --- a/styles/border/index.html +++ b/styles/border/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/border_subtitle_align/index.html b/styles/border_subtitle_align/index.html index 1eecc6b880..3b16384f27 100644 --- a/styles/border_subtitle_align/index.html +++ b/styles/border_subtitle_align/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/border_subtitle_background/index.html b/styles/border_subtitle_background/index.html index 6e56af695b..2891807219 100644 --- a/styles/border_subtitle_background/index.html +++ b/styles/border_subtitle_background/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/border_subtitle_color/index.html b/styles/border_subtitle_color/index.html index df0fa85c7c..8f07716080 100644 --- a/styles/border_subtitle_color/index.html +++ b/styles/border_subtitle_color/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/border_subtitle_style/index.html b/styles/border_subtitle_style/index.html index effa8770f7..44bc7bd190 100644 --- a/styles/border_subtitle_style/index.html +++ b/styles/border_subtitle_style/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/border_title_align/index.html b/styles/border_title_align/index.html index 79b2744dc4..453046ee53 100644 --- a/styles/border_title_align/index.html +++ b/styles/border_title_align/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/border_title_background/index.html b/styles/border_title_background/index.html index 7a6c0f2943..c15fbf083a 100644 --- a/styles/border_title_background/index.html +++ b/styles/border_title_background/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/border_title_color/index.html b/styles/border_title_color/index.html index 5913382e3e..cb55761c9e 100644 --- a/styles/border_title_color/index.html +++ b/styles/border_title_color/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/border_title_style/index.html b/styles/border_title_style/index.html index 4e9607f557..309899a199 100644 --- a/styles/border_title_style/index.html +++ b/styles/border_title_style/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/box_sizing/index.html b/styles/box_sizing/index.html index ad1e580cf6..3099e77bc2 100644 --- a/styles/box_sizing/index.html +++ b/styles/box_sizing/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/color/index.html b/styles/color/index.html index 1b2c32477c..a007cca5aa 100644 --- a/styles/color/index.html +++ b/styles/color/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/content_align/index.html b/styles/content_align/index.html index 533e927031..37e9d2d1e7 100644 --- a/styles/content_align/index.html +++ b/styles/content_align/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/display/index.html b/styles/display/index.html index c26447e9d4..1a66f61908 100644 --- a/styles/display/index.html +++ b/styles/display/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/dock/index.html b/styles/dock/index.html index b0bc6d3908..87e88ff947 100644 --- a/styles/dock/index.html +++ b/styles/dock/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/grid/column_span/index.html b/styles/grid/column_span/index.html index 505fc0456c..19e4349ece 100644 --- a/styles/grid/column_span/index.html +++ b/styles/grid/column_span/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/grid/grid_columns/index.html b/styles/grid/grid_columns/index.html index 6d498aecba..5eeb9f2d47 100644 --- a/styles/grid/grid_columns/index.html +++ b/styles/grid/grid_columns/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/grid/grid_gutter/index.html b/styles/grid/grid_gutter/index.html index 56a1054b39..a885c6cadf 100644 --- a/styles/grid/grid_gutter/index.html +++ b/styles/grid/grid_gutter/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/grid/grid_rows/index.html b/styles/grid/grid_rows/index.html index 9217ed7e9b..b6675555f8 100644 --- a/styles/grid/grid_rows/index.html +++ b/styles/grid/grid_rows/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/grid/grid_size/index.html b/styles/grid/grid_size/index.html index 61d2cf614d..6ebcd694dd 100644 --- a/styles/grid/grid_size/index.html +++ b/styles/grid/grid_size/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/grid/index.html b/styles/grid/index.html index d472d4b4af..e6fa1735e8 100644 --- a/styles/grid/index.html +++ b/styles/grid/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/grid/row_span/index.html b/styles/grid/row_span/index.html index 77f5873651..298662118e 100644 --- a/styles/grid/row_span/index.html +++ b/styles/grid/row_span/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/height/index.html b/styles/height/index.html index b0c9213548..e60914dc05 100644 --- a/styles/height/index.html +++ b/styles/height/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/index.html b/styles/index.html index 4b57021497..0a62020118 100644 --- a/styles/index.html +++ b/styles/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/layer/index.html b/styles/layer/index.html index 07a04bc98c..ec7ea1ca71 100644 --- a/styles/layer/index.html +++ b/styles/layer/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/layers/index.html b/styles/layers/index.html index 7039761a93..95a0101c50 100644 --- a/styles/layers/index.html +++ b/styles/layers/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/layout/index.html b/styles/layout/index.html index 6075c3a069..29fbe26373 100644 --- a/styles/layout/index.html +++ b/styles/layout/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/links/index.html b/styles/links/index.html index cd70101090..2dbb8731c2 100644 --- a/styles/links/index.html +++ b/styles/links/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/links/link_background/index.html b/styles/links/link_background/index.html index e41958dc11..a8165a5108 100644 --- a/styles/links/link_background/index.html +++ b/styles/links/link_background/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/links/link_color/index.html b/styles/links/link_color/index.html index 69ee5ccf79..d3fafcf08c 100644 --- a/styles/links/link_color/index.html +++ b/styles/links/link_color/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/links/link_hover_background/index.html b/styles/links/link_hover_background/index.html index dc38886907..34409b1476 100644 --- a/styles/links/link_hover_background/index.html +++ b/styles/links/link_hover_background/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/links/link_hover_color/index.html b/styles/links/link_hover_color/index.html index c067658439..1509aef784 100644 --- a/styles/links/link_hover_color/index.html +++ b/styles/links/link_hover_color/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/links/link_hover_style/index.html b/styles/links/link_hover_style/index.html index a2ff4f1aa3..9815f3fef6 100644 --- a/styles/links/link_hover_style/index.html +++ b/styles/links/link_hover_style/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/links/link_style/index.html b/styles/links/link_style/index.html index baeebe67c2..c99b8d0734 100644 --- a/styles/links/link_style/index.html +++ b/styles/links/link_style/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/margin/index.html b/styles/margin/index.html index dd945002bf..e8b93efa17 100644 --- a/styles/margin/index.html +++ b/styles/margin/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/max_height/index.html b/styles/max_height/index.html index 7d3da2edd3..2c9a04bad4 100644 --- a/styles/max_height/index.html +++ b/styles/max_height/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/max_width/index.html b/styles/max_width/index.html index ba0ed60c9e..73e61d533e 100644 --- a/styles/max_width/index.html +++ b/styles/max_width/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/min_height/index.html b/styles/min_height/index.html index 35a53b0018..b5fe4874e6 100644 --- a/styles/min_height/index.html +++ b/styles/min_height/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/min_width/index.html b/styles/min_width/index.html index f156a09fcf..a9b4a9d05c 100644 --- a/styles/min_width/index.html +++ b/styles/min_width/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/offset/index.html b/styles/offset/index.html index 5b0bf68c33..92ace75d7c 100644 --- a/styles/offset/index.html +++ b/styles/offset/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/opacity/index.html b/styles/opacity/index.html index d4f432eb24..46bf467ba8 100644 --- a/styles/opacity/index.html +++ b/styles/opacity/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/outline/index.html b/styles/outline/index.html index 8a05959b40..5b38b08413 100644 --- a/styles/outline/index.html +++ b/styles/outline/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/overflow/index.html b/styles/overflow/index.html index f50449734c..9bea48187b 100644 --- a/styles/overflow/index.html +++ b/styles/overflow/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/padding/index.html b/styles/padding/index.html index 468d5b15f0..492348d79a 100644 --- a/styles/padding/index.html +++ b/styles/padding/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/scrollbar_colors/index.html b/styles/scrollbar_colors/index.html index fbce26da86..f217010eaf 100644 --- a/styles/scrollbar_colors/index.html +++ b/styles/scrollbar_colors/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/scrollbar_colors/scrollbar_background/index.html b/styles/scrollbar_colors/scrollbar_background/index.html index 1c8bcb1a5c..a6c97a6f69 100644 --- a/styles/scrollbar_colors/scrollbar_background/index.html +++ b/styles/scrollbar_colors/scrollbar_background/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/scrollbar_colors/scrollbar_background_active/index.html b/styles/scrollbar_colors/scrollbar_background_active/index.html index 884acc82a0..3aca405d41 100644 --- a/styles/scrollbar_colors/scrollbar_background_active/index.html +++ b/styles/scrollbar_colors/scrollbar_background_active/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/scrollbar_colors/scrollbar_background_hover/index.html b/styles/scrollbar_colors/scrollbar_background_hover/index.html index 4eefcc2c8c..26def18618 100644 --- a/styles/scrollbar_colors/scrollbar_background_hover/index.html +++ b/styles/scrollbar_colors/scrollbar_background_hover/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/scrollbar_colors/scrollbar_color/index.html b/styles/scrollbar_colors/scrollbar_color/index.html index 890b7998e3..cac6b1e9a7 100644 --- a/styles/scrollbar_colors/scrollbar_color/index.html +++ b/styles/scrollbar_colors/scrollbar_color/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/scrollbar_colors/scrollbar_color_active/index.html b/styles/scrollbar_colors/scrollbar_color_active/index.html index c004f26e17..7d17276a15 100644 --- a/styles/scrollbar_colors/scrollbar_color_active/index.html +++ b/styles/scrollbar_colors/scrollbar_color_active/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/scrollbar_colors/scrollbar_color_hover/index.html b/styles/scrollbar_colors/scrollbar_color_hover/index.html index 9772e11152..ccbb7c819c 100644 --- a/styles/scrollbar_colors/scrollbar_color_hover/index.html +++ b/styles/scrollbar_colors/scrollbar_color_hover/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/scrollbar_colors/scrollbar_corner_color/index.html b/styles/scrollbar_colors/scrollbar_corner_color/index.html index af0c10272d..2095819abf 100644 --- a/styles/scrollbar_colors/scrollbar_corner_color/index.html +++ b/styles/scrollbar_colors/scrollbar_corner_color/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/scrollbar_gutter/index.html b/styles/scrollbar_gutter/index.html index bdc7df9688..bc43ad2d36 100644 --- a/styles/scrollbar_gutter/index.html +++ b/styles/scrollbar_gutter/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/scrollbar_size/index.html b/styles/scrollbar_size/index.html index 3f587e9075..075af565a2 100644 --- a/styles/scrollbar_size/index.html +++ b/styles/scrollbar_size/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/text_align/index.html b/styles/text_align/index.html index a7fb58634b..88fca61f80 100644 --- a/styles/text_align/index.html +++ b/styles/text_align/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/text_opacity/index.html b/styles/text_opacity/index.html index c6c72a77ff..d08559e7d8 100644 --- a/styles/text_opacity/index.html +++ b/styles/text_opacity/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/text_style/index.html b/styles/text_style/index.html index 802ac4c8d8..77f0e50ea1 100644 --- a/styles/text_style/index.html +++ b/styles/text_style/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/tint/index.html b/styles/tint/index.html index f3a33e8111..32341c49c9 100644 --- a/styles/tint/index.html +++ b/styles/tint/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/visibility/index.html b/styles/visibility/index.html index be78bd9ee2..62c515bb57 100644 --- a/styles/visibility/index.html +++ b/styles/visibility/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/styles/width/index.html b/styles/width/index.html index cccf5e13a4..875a120a42 100644 --- a/styles/width/index.html +++ b/styles/width/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/tutorial/index.html b/tutorial/index.html index d9ee630cbb..431fc7c6ce 100644 --- a/tutorial/index.html +++ b/tutorial/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -517,7 +517,7 @@ - Welcome + Home @@ -7502,140 +7502,140 @@

Stopwatch Application + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - stopwatch.py + stopwatch.py - + - - StopwatchApp - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Stop 00:00:02.02 -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Stop 00:00:09.17 -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Stop 00:00:06.04 -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - D  Toggle dark mode  A  Add  R  Remove  + + StopwatchApp + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Stop 00:00:02.01 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Stop 00:00:10.09 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Stop 00:00:06.07 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + D  Toggle dark mode  A  Add  R  Remove  @@ -9363,146 +9363,146 @@

Wiring buttons + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - stopwatch06.py + stopwatch06.py - + - - StopwatchApp - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Start 00:00:00.00 Reset  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Stop 00:00:04.04 -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Start 00:00:00.00 Reset  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - D  Toggle dark mode  + + StopwatchApp + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Start 00:00:00.00 Reset  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Stop 00:00:04.03 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Start 00:00:00.00 Reset  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + D  Toggle dark mode  diff --git a/widget_gallery/index.html b/widget_gallery/index.html index b662ca9045..0ad7b88297 100644 --- a/widget_gallery/index.html +++ b/widget_gallery/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -517,7 +517,7 @@ - Welcome + Home @@ -8560,137 +8560,138 @@

DirectoryTree - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - DirectoryTreeApp + DirectoryTreeApp - - - - 📂 . -┣━━ 📁 .cache -┣━━ 📁 .faq -┣━━ 📁 .git -┣━━ 📁 .github -┣━━ 📁 .ipynb_checkpoints -┣━━ 📁 .mypy_cache -┣━━ 📁 .pytest_cache -┣━━ 📁 .screenshot_cache -┣━━ 📁 .vscode -┣━━ 📁 __pycache__ -┣━━ 📁 dist▃▃ -┣━━ 📁 docs -┣━━ 📁 examples -┣━━ 📁 imgs -┣━━ 📁 notes -┣━━ 📁 questions -┣━━ 📁 reference -┣━━ 📁 sandbox -┣━━ 📁 site -┣━━ 📁 src -┣━━ 📁 tests -┣━━ 📁 tools -┣━━ 📁 tree-sitter + + + + 📂 . +┣━━ 📁 .cache +┣━━ 📁 .faq +┣━━ 📁 .git +┣━━ 📁 .github +┣━━ 📁 .ipynb_checkpoints +┣━━ 📁 .mypy_cache +┣━━ 📁 .pytest_cache +┣━━ 📁 .screenshot_cache +┣━━ 📁 .vscode +┣━━ 📁 __pycache__ +┣━━ 📁 dist▁▁ +┣━━ 📁 docs +┣━━ 📁 examples +┣━━ 📁 imgs +┣━━ 📁 notes +┣━━ 📁 questions +┣━━ 📁 reference +┣━━ 📁 sandbox +┣━━ 📁 site +┣━━ 📁 src +┣━━ 📁 tests +┣━━ 📁 tools +┣━━ 📄 .coverage diff --git a/widgets/button/index.html b/widgets/button/index.html index 45f95f8091..1ed27cb6e7 100644 --- a/widgets/button/index.html +++ b/widgets/button/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -7902,8 +7902,8 @@

- class-attribute instance-attribute + class-attribute

@@ -7926,8 +7926,8 @@

- class-attribute instance-attribute + class-attribute

@@ -7950,8 +7950,8 @@

- class-attribute instance-attribute + class-attribute

diff --git a/widgets/checkbox/index.html b/widgets/checkbox/index.html index a83f565cc9..d416a729f9 100644 --- a/widgets/checkbox/index.html +++ b/widgets/checkbox/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/widgets/collapsible/index.html b/widgets/collapsible/index.html index e2dde64906..f9a928367d 100644 --- a/widgets/collapsible/index.html +++ b/widgets/collapsible/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/widgets/content_switcher/index.html b/widgets/content_switcher/index.html index d2503a1bb5..2287cd5a7a 100644 --- a/widgets/content_switcher/index.html +++ b/widgets/content_switcher/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -7871,8 +7871,8 @@

- class-attribute instance-attribute + class-attribute

diff --git a/widgets/data_table/index.html b/widgets/data_table/index.html index e9dc463a6f..ae598476a2 100644 --- a/widgets/data_table/index.html +++ b/widgets/data_table/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -11767,8 +11767,8 @@

- class-attribute instance-attribute + class-attribute

@@ -11842,8 +11842,8 @@

- class-attribute instance-attribute + class-attribute

@@ -11866,8 +11866,8 @@

- class-attribute instance-attribute + class-attribute

@@ -11890,8 +11890,8 @@

- class-attribute instance-attribute + class-attribute

@@ -11914,8 +11914,8 @@

- class-attribute instance-attribute + class-attribute

@@ -11961,8 +11961,8 @@

- class-attribute instance-attribute + class-attribute

@@ -12102,8 +12102,8 @@

- class-attribute instance-attribute + class-attribute

@@ -12126,8 +12126,8 @@

- class-attribute instance-attribute + class-attribute

@@ -12150,8 +12150,8 @@

- class-attribute instance-attribute + class-attribute

@@ -12174,8 +12174,8 @@

- class-attribute instance-attribute + class-attribute

diff --git a/widgets/digits/index.html b/widgets/digits/index.html index 6b1ec1fafc..e918722e65 100644 --- a/widgets/digits/index.html +++ b/widgets/digits/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -7391,132 +7391,132 @@

Example + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ClockApp + ClockApp - + - - - - - - - - - - - - ┓ ╺━┓   ╺━┓┏━┓   ┏━╸╺━┓ - ┃  ━┫ : ┏━┛┣━┫ : ┗━┓┏━┛ -╺┻╸╺━┛   ┗━╸┗━┛   ╺━┛┗━╸ - - - - - - - - - - + + + + + + + + + + + + ┓ ┏━╸   ┏━┓┏━┓   ╺━┓ ┓  + ┃ ┗━┓ : ┃ ┃┃ ┃ :  ━┫ ┃  +╺┻╸╺━┛   ┗━┛┗━┛   ╺━┛╺┻╸ + + + + + + + + + + diff --git a/widgets/directory_tree/index.html b/widgets/directory_tree/index.html index 5962b88234..52ad7aa756 100644 --- a/widgets/directory_tree/index.html +++ b/widgets/directory_tree/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -7681,137 +7681,136 @@

Filtering + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - DirectoryTreeApp + DirectoryTreeApp - - - - 📂 . -┣━━ 📁 __pycache__ -┣━━ 📁 dist -┣━━ 📁 docs -┣━━ 📁 examples -┣━━ 📁 imgs -┣━━ 📁 notes -┣━━ 📁 questions -┣━━ 📁 reference -┣━━ 📁 sandbox -┣━━ 📁 site -┣━━ 📁 src -┣━━ 📁 tests -┣━━ 📁 tools -┣━━ 📁 tree-sitter -┣━━ 📄 CHANGELOG.md -┣━━ 📄 CODE_OF_CONDUCT.md▄▄ -┣━━ 📄 CONTRIBUTING.md -┣━━ 📄 docs.md -┣━━ 📄 faq.yml -┣━━ 📄 keys.log -┣━━ 📄 LICENSE -┣━━ 📄 Makefile -┣━━ 📄 mkdocs-common.yml + + + + 📂 . +┣━━ 📁 __pycache__ +┣━━ 📁 dist +┣━━ 📁 docs +┣━━ 📁 examples +┣━━ 📁 imgs +┣━━ 📁 notes +┣━━ 📁 questions +┣━━ 📁 reference +┣━━ 📁 sandbox +┣━━ 📁 site +┣━━ 📁 src +┣━━ 📁 tests +┣━━ 📁 tools +┣━━ 📄 CHANGELOG.md +┣━━ 📄 CODE_OF_CONDUCT.md +┣━━ 📄 CONTRIBUTING.md +┣━━ 📄 docs.md +┣━━ 📄 faq.yml +┣━━ 📄 keys.log +┣━━ 📄 LICENSE +┣━━ 📄 Makefile +┣━━ 📄 mkdocs-common.yml +┣━━ 📄 mkdocs-nav-offline.yml @@ -8098,8 +8097,8 @@

- class-attribute instance-attribute + class-attribute

@@ -8122,8 +8121,8 @@

- class-attribute instance-attribute + class-attribute

diff --git a/widgets/footer/index.html b/widgets/footer/index.html index ac56e6d7e6..8dec727bd7 100644 --- a/widgets/footer/index.html +++ b/widgets/footer/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/widgets/header/index.html b/widgets/header/index.html index 79c0101120..e0ca56de6a 100644 --- a/widgets/header/index.html +++ b/widgets/header/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -7735,8 +7735,8 @@

- class-attribute instance-attribute + class-attribute

diff --git a/widgets/index.html b/widgets/index.html index 8a82e57796..160ce43345 100644 --- a/widgets/index.html +++ b/widgets/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/widgets/input/index.html b/widgets/input/index.html index f4f68eabab..6e4fbb47c6 100644 --- a/widgets/input/index.html +++ b/widgets/input/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -9196,8 +9196,8 @@

- class-attribute instance-attribute + class-attribute

@@ -9338,8 +9338,8 @@

- class-attribute instance-attribute + class-attribute

diff --git a/widgets/label/index.html b/widgets/label/index.html index 322dd38ce4..1b409d18cc 100644 --- a/widgets/label/index.html +++ b/widgets/label/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/widgets/list_item/index.html b/widgets/list_item/index.html index 148e8f1a0c..ab302d31eb 100644 --- a/widgets/list_item/index.html +++ b/widgets/list_item/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -7412,8 +7412,8 @@

- class-attribute instance-attribute + class-attribute

diff --git a/widgets/list_view/index.html b/widgets/list_view/index.html index cd436c6f03..cac484151d 100644 --- a/widgets/list_view/index.html +++ b/widgets/list_view/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -8121,8 +8121,8 @@

- class-attribute instance-attribute + class-attribute

@@ -8265,8 +8265,8 @@

- class-attribute instance-attribute + class-attribute

diff --git a/widgets/loading_indicator/index.html b/widgets/loading_indicator/index.html index a91935e2bd..aa72dbbc59 100644 --- a/widgets/loading_indicator/index.html +++ b/widgets/loading_indicator/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/widgets/log/index.html b/widgets/log/index.html index 1dca8f26aa..cb30f8e2c3 100644 --- a/widgets/log/index.html +++ b/widgets/log/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -7764,8 +7764,8 @@

- class-attribute instance-attribute + class-attribute

@@ -7882,8 +7882,8 @@

- class-attribute instance-attribute + class-attribute

diff --git a/widgets/markdown/index.html b/widgets/markdown/index.html index d8e96a9e6c..4870331656 100644 --- a/widgets/markdown/index.html +++ b/widgets/markdown/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -7961,8 +7961,8 @@

- class-attribute instance-attribute + class-attribute

diff --git a/widgets/markdown_viewer/index.html b/widgets/markdown_viewer/index.html index 1e2cf00258..81132de349 100644 --- a/widgets/markdown_viewer/index.html +++ b/widgets/markdown_viewer/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/widgets/option_list/index.html b/widgets/option_list/index.html index 090aa41af3..ba69097f23 100644 --- a/widgets/option_list/index.html +++ b/widgets/option_list/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -9285,8 +9285,8 @@

- class-attribute instance-attribute + class-attribute

diff --git a/widgets/placeholder/index.html b/widgets/placeholder/index.html index 1d2fc7b7ba..7f6ebe9ca9 100644 --- a/widgets/placeholder/index.html +++ b/widgets/placeholder/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -7639,8 +7639,8 @@

- class-attribute instance-attribute + class-attribute

diff --git a/widgets/pretty/index.html b/widgets/pretty/index.html index 4a94f90e3f..90f21d4c01 100644 --- a/widgets/pretty/index.html +++ b/widgets/pretty/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/widgets/progress_bar/index.html b/widgets/progress_bar/index.html index ac6009e79f..78db8f6996 100644 --- a/widgets/progress_bar/index.html +++ b/widgets/progress_bar/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -9356,8 +9356,8 @@

- class-attribute instance-attribute + class-attribute

@@ -9393,8 +9393,8 @@

- class-attribute instance-attribute + class-attribute

@@ -9417,8 +9417,8 @@

- class-attribute instance-attribute + class-attribute

diff --git a/widgets/radiobutton/index.html b/widgets/radiobutton/index.html index 839a9c0413..ba6dcef7dd 100644 --- a/widgets/radiobutton/index.html +++ b/widgets/radiobutton/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -7595,8 +7595,8 @@

- class-attribute instance-attribute + class-attribute

diff --git a/widgets/radioset/index.html b/widgets/radioset/index.html index 52a9afdad8..d06afbe1d6 100644 --- a/widgets/radioset/index.html +++ b/widgets/radioset/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -8220,8 +8220,8 @@

- class-attribute instance-attribute + class-attribute

diff --git a/widgets/rich_log/index.html b/widgets/rich_log/index.html index 93a4f09e4e..550a632118 100644 --- a/widgets/rich_log/index.html +++ b/widgets/rich_log/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -7772,8 +7772,8 @@

- class-attribute instance-attribute + class-attribute

@@ -7796,8 +7796,8 @@

- class-attribute instance-attribute + class-attribute

@@ -7820,8 +7820,8 @@

- class-attribute instance-attribute + class-attribute

@@ -7844,8 +7844,8 @@

- class-attribute instance-attribute + class-attribute

@@ -7868,8 +7868,8 @@

- class-attribute instance-attribute + class-attribute

@@ -7892,8 +7892,8 @@

- class-attribute instance-attribute + class-attribute

diff --git a/widgets/rule/index.html b/widgets/rule/index.html index 7ef6c2a0f9..ac9a1a7039 100644 --- a/widgets/rule/index.html +++ b/widgets/rule/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -7972,8 +7972,8 @@

- class-attribute instance-attribute + class-attribute

@@ -7996,8 +7996,8 @@

- class-attribute instance-attribute + class-attribute

diff --git a/widgets/select/index.html b/widgets/select/index.html index 356a581a17..61ef1d8aa6 100644 --- a/widgets/select/index.html +++ b/widgets/select/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -7978,8 +7978,8 @@

- class-attribute instance-attribute + class-attribute

@@ -8015,8 +8015,8 @@

- class-attribute instance-attribute + class-attribute

@@ -8039,8 +8039,8 @@

- class-attribute instance-attribute + class-attribute

@@ -8063,8 +8063,8 @@

- class-attribute instance-attribute + class-attribute

diff --git a/widgets/selection_list/index.html b/widgets/selection_list/index.html index 304507e963..aa07cb7117 100644 --- a/widgets/selection_list/index.html +++ b/widgets/selection_list/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -8959,8 +8959,8 @@

- class-attribute instance-attribute + class-attribute

diff --git a/widgets/sparkline/index.html b/widgets/sparkline/index.html index d3e7437826..0347611a8d 100644 --- a/widgets/sparkline/index.html +++ b/widgets/sparkline/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -8665,8 +8665,8 @@

- class-attribute instance-attribute + class-attribute

@@ -8689,8 +8689,8 @@

- class-attribute instance-attribute + class-attribute

diff --git a/widgets/static/index.html b/widgets/static/index.html index d1536788aa..d3b551929c 100644 --- a/widgets/static/index.html +++ b/widgets/static/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home diff --git a/widgets/switch/index.html b/widgets/switch/index.html index a1e4fb9e00..8970940c12 100644 --- a/widgets/switch/index.html +++ b/widgets/switch/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -7859,8 +7859,8 @@

- class-attribute instance-attribute + class-attribute

@@ -7883,8 +7883,8 @@

- class-attribute instance-attribute + class-attribute

diff --git a/widgets/tabbed_content/index.html b/widgets/tabbed_content/index.html index f987f1bc45..a64764cffc 100644 --- a/widgets/tabbed_content/index.html +++ b/widgets/tabbed_content/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -8238,8 +8238,8 @@

- class-attribute instance-attribute + class-attribute

@@ -8478,8 +8478,8 @@

- class-attribute instance-attribute + class-attribute

diff --git a/widgets/tabs/index.html b/widgets/tabs/index.html index 541b51f335..ffd6e574dd 100644 --- a/widgets/tabs/index.html +++ b/widgets/tabs/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -8540,8 +8540,8 @@

- class-attribute instance-attribute + class-attribute

@@ -8953,8 +8953,8 @@

- class-attribute instance-attribute + class-attribute

diff --git a/widgets/text_area/index.html b/widgets/text_area/index.html index 0878716aba..a09a1781f7 100644 --- a/widgets/text_area/index.html +++ b/widgets/text_area/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -11897,8 +11897,8 @@

- class-attribute instance-attribute + class-attribute

@@ -12380,8 +12380,8 @@ @@ -12404,8 +12404,8 @@

- property writable + property

@@ -12516,8 +12516,8 @@

- class-attribute instance-attribute + class-attribute

@@ -12565,8 +12565,8 @@

- class-attribute instance-attribute + class-attribute

@@ -12589,8 +12589,8 @@

- class-attribute instance-attribute + class-attribute

@@ -12636,8 +12636,8 @@

- class-attribute instance-attribute + class-attribute

@@ -12666,8 +12666,8 @@

- class-attribute instance-attribute + class-attribute

@@ -12714,8 +12714,8 @@

- class-attribute instance-attribute + class-attribute

@@ -15537,8 +15537,8 @@

- property abstractmethod + property

@@ -15561,8 +15561,8 @@

- property abstractmethod + property

@@ -15585,8 +15585,8 @@

- property abstractmethod + property

@@ -16474,8 +16474,8 @@

- class-attribute instance-attribute + class-attribute

@@ -16522,8 +16522,8 @@

- class-attribute instance-attribute + class-attribute

@@ -17047,8 +17047,8 @@

- class-attribute instance-attribute + class-attribute

@@ -17071,8 +17071,8 @@

- class-attribute instance-attribute + class-attribute

@@ -17095,8 +17095,8 @@

- class-attribute instance-attribute + class-attribute

@@ -17120,8 +17120,8 @@

- class-attribute instance-attribute + class-attribute

@@ -17144,8 +17144,8 @@

- class-attribute instance-attribute + class-attribute

@@ -17168,8 +17168,8 @@

- class-attribute instance-attribute + class-attribute

@@ -17215,8 +17215,8 @@

- class-attribute instance-attribute + class-attribute

@@ -17239,8 +17239,8 @@

- class-attribute instance-attribute + class-attribute

diff --git a/widgets/toast/index.html b/widgets/toast/index.html index bade96f6b8..348fb6ffd9 100644 --- a/widgets/toast/index.html +++ b/widgets/toast/index.html @@ -251,7 +251,7 @@ - Welcome + Home @@ -509,7 +509,7 @@ - Welcome + Home diff --git a/widgets/tree/index.html b/widgets/tree/index.html index c820ba0426..f24a9a0c8b 100644 --- a/widgets/tree/index.html +++ b/widgets/tree/index.html @@ -255,7 +255,7 @@ - Welcome + Home @@ -515,7 +515,7 @@ - Welcome + Home @@ -9273,8 +9273,8 @@

- class-attribute instance-attribute + class-attribute

@@ -9297,8 +9297,8 @@

- class-attribute instance-attribute + class-attribute

@@ -9344,8 +9344,8 @@

- class-attribute instance-attribute + class-attribute

@@ -9368,8 +9368,8 @@

- class-attribute instance-attribute + class-attribute

@@ -9438,8 +9438,8 @@

- class-attribute instance-attribute + class-attribute

@@ -9462,8 +9462,8 @@

- class-attribute instance-attribute + class-attribute

@@ -11025,8 +11025,8 @@

- property writable + property

@@ -11187,8 +11187,8 @@

- property writable + property