diff --git a/CHANGELOG.md b/CHANGELOG.md
index 33fa73d917..35395ba0ef 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Fixed `RadioSet` not being scrollable https://github.com/Textualize/textual/issues/5100
+### Added
+
+- Added `background-tint` CSS rule https://github.com/Textualize/textual/pull/5117
+
## [0.83.0] - 2024-10-10
### Added
diff --git a/docs/examples/styles/background_tint.py b/docs/examples/styles/background_tint.py
new file mode 100644
index 0000000000..a85540c15e
--- /dev/null
+++ b/docs/examples/styles/background_tint.py
@@ -0,0 +1,24 @@
+from textual.app import App, ComposeResult
+from textual.containers import Vertical
+from textual.widgets import Label
+
+
+class BackgroundTintApp(App):
+ CSS_PATH = "background_tint.tcss"
+
+ def compose(self) -> ComposeResult:
+ with Vertical(id="tint1"):
+ yield Label("0%")
+ with Vertical(id="tint2"):
+ yield Label("25%")
+ with Vertical(id="tint3"):
+ yield Label("50%")
+ with Vertical(id="tint4"):
+ yield Label("75%")
+ with Vertical(id="tint5"):
+ yield Label("100%")
+
+
+if __name__ == "__main__":
+ app = BackgroundTintApp()
+ app.run()
diff --git a/docs/examples/styles/background_tint.tcss b/docs/examples/styles/background_tint.tcss
new file mode 100644
index 0000000000..276ef44ead
--- /dev/null
+++ b/docs/examples/styles/background_tint.tcss
@@ -0,0 +1,9 @@
+Vertical {
+ background: $panel;
+ color: auto 90%;
+}
+#tint1 { background-tint: $foreground 0%; }
+#tint2 { background-tint: $foreground 25%; }
+#tint3 { background-tint: $foreground 50%; }
+#tint4 { background-tint: $foreground 75% }
+#tint5 { background-tint: $foreground 100% }
diff --git a/docs/styles/background.md b/docs/styles/background.md
index 420d08eae8..c1c98622b1 100644
--- a/docs/styles/background.md
+++ b/docs/styles/background.md
@@ -90,4 +90,5 @@ widget.styles.background = Color(120, 60, 100)
## See also
+ - [`background-tint`](./background_tint.md) to blend a color with the background.
- [`color`](./color.md) to set the color of text in a widget.
diff --git a/docs/styles/background_tint.md b/docs/styles/background_tint.md
new file mode 100644
index 0000000000..5718994ece
--- /dev/null
+++ b/docs/styles/background_tint.md
@@ -0,0 +1,77 @@
+# Background-tint
+
+The `background-tint` style modifies the background color by tinting (blending) it with a new color.
+
+This style is typically used to subtly change the background of a widget for emphasis.
+For instance the following would make a focused widget have a slightly lighter background.
+
+```css
+MyWidget:focus {
+ background-tint: white 10%
+}
+```
+
+The background tint color should typically have less than 100% alpha, in order to modify the background color.
+If the alpha component is 100% then the tint color will replace the background color entirely.
+
+## Syntax
+
+--8<-- "docs/snippets/syntax_block_start.md"
+background-tint: <color> [<percentage>];
+--8<-- "docs/snippets/syntax_block_end.md"
+
+The `background-tint` style requires a [``](../css_types/color.md) optionally followed by [``](../css_types/percentage.md) to specify the color's opacity (clamped between `0%` and `100%`).
+
+## Examples
+
+### Basic usage
+
+This example shows background tint applied with alpha from 0 to 100%.
+
+=== "Output"
+
+ ```{.textual path="docs/examples/styles/background_tint.py"}
+ ```
+
+=== "background_tint.py"
+
+ ```python
+ --8<-- "docs/examples/styles/background_tint.py"
+ ```
+
+=== "background.tcss"
+
+ ```css hl_lines="5-9"
+ --8<-- "docs/examples/styles/background_tint.tcss"
+ ```
+
+
+## CSS
+
+```css
+/* 10% backgrouhnd tint */
+background-tint: blue 10%;
+
+
+/* 20% RGB color */
+background-tint: rgb(100, 120, 200, 0.2);
+
+```
+
+## Python
+
+You can use the same syntax as CSS, or explicitly set a `Color` object for finer-grained control.
+
+```python
+# Set 20% blue background tint
+widget.styles.background_tint = "blue 20%"
+
+from textual.color import Color
+# Set with a color object
+widget.styles.background_tint = Color(120, 60, 100, 0.5)
+```
+
+## See also
+
+ - [`background`](./background.md) to set the background color of a widget.
+ - [`color`](./color.md) to set the color of text in a widget.
diff --git a/mkdocs-nav.yml b/mkdocs-nav.yml
index 53b75f0391..232e7b23ac 100644
--- a/mkdocs-nav.yml
+++ b/mkdocs-nav.yml
@@ -75,6 +75,7 @@ nav:
- "styles/index.md"
- "styles/align.md"
- "styles/background.md"
+ - "styles/background_tint.md"
- "styles/border.md"
- "styles/border_subtitle_align.md"
- "styles/border_subtitle_background.md"
diff --git a/src/textual/css/_styles_builder.py b/src/textual/css/_styles_builder.py
index 6d1baf45e8..8bbb26767a 100644
--- a/src/textual/css/_styles_builder.py
+++ b/src/textual/css/_styles_builder.py
@@ -670,6 +670,7 @@ def process_color(self, name: str, tokens: list[Token]) -> None:
process_tint = process_color
process_background = process_color
+ process_background_tint = process_color
process_scrollbar_color = process_color
process_scrollbar_color_hover = process_color
process_scrollbar_color_active = process_color
diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py
index 4e00d74d23..e2353b9568 100644
--- a/src/textual/css/styles.py
+++ b/src/textual/css/styles.py
@@ -91,6 +91,8 @@ class RulesMap(TypedDict, total=False):
background: Color
text_style: Style
+ background_tint: Color
+
opacity: float
text_opacity: float
@@ -215,6 +217,7 @@ class StylesBase:
"auto_color",
"color",
"background",
+ "background_tint",
"opacity",
"text_opacity",
"tint",
@@ -285,6 +288,11 @@ class StylesBase:
Supports `Color` objects but also strings e.g. "red" or "#ff0000"
You can also specify an opacity after a color e.g. "blue 10%"
"""
+ background_tint = ColorProperty(Color(0, 0, 0, 0))
+ """Set a color to tint (blend) with the background.
+ Supports `Color` objects but also strings e.g. "red" or "#ff0000"
+ You can also specify an opacity after a color e.g. "blue 10%"
+ """
text_style = StyleFlagsProperty()
"""Set the text style of the widget using Rich StyleFlags.
e.g. `"bold underline"` or `"b u strikethrough"`.
@@ -1011,6 +1019,8 @@ def append_declaration(name: str, value: str) -> None:
append_declaration("color", self.color.hex)
if "background" in rules:
append_declaration("background", self.background.hex)
+ if "background_tint" in rules:
+ append_declaration("background-tint", self.background_tint.hex)
if "text_style" in rules:
append_declaration("text-style", str(get_rule("text_style")))
if "tint" in rules:
diff --git a/src/textual/dom.py b/src/textual/dom.py
index 8cc4e4437b..4608a3681b 100644
--- a/src/textual/dom.py
+++ b/src/textual/dom.py
@@ -1026,8 +1026,12 @@ def rich_style(self) -> Style:
has_rule = styles.has_rule
opacity *= styles.opacity
if has_rule("background"):
- text_background = background + styles.background
- background += styles.background.multiply_alpha(opacity)
+ text_background = (
+ background + styles.background + styles.background_tint
+ )
+ background += (
+ styles.background + styles.background_tint
+ ).multiply_alpha(opacity)
else:
text_background = background
if has_rule("color"):
@@ -1115,7 +1119,7 @@ def background_colors(self) -> tuple[Color, Color]:
for node in reversed(self.ancestors_with_self):
styles = node.styles
base_background = background
- background += styles.background
+ background += styles.background + styles.background_tint
return (base_background, background)
@property
@@ -1131,7 +1135,9 @@ def _opacity_background_colors(self) -> tuple[Color, Color]:
styles = node.styles
base_background = background
opacity *= styles.opacity
- background += styles.background.multiply_alpha(opacity)
+ background += (styles.background + styles.background_tint).multiply_alpha(
+ opacity
+ )
return (base_background, background)
@property
@@ -1146,7 +1152,7 @@ def colors(self) -> tuple[Color, Color, Color, Color]:
for node in reversed(self.ancestors_with_self):
styles = node.styles
base_background = background
- background += styles.background
+ background += styles.background + styles.background_tint
if styles.has_rule("color"):
base_color = color
if styles.auto_color:
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_background_tint.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_background_tint.svg
new file mode 100644
index 0000000000..22b4fda3db
--- /dev/null
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_background_tint.svg
@@ -0,0 +1,153 @@
+
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_css_property[background_tint.py].svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_css_property[background_tint.py].svg
new file mode 100644
index 0000000000..0617ea714b
--- /dev/null
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_css_property[background_tint.py].svg
@@ -0,0 +1,154 @@
+
diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py
index 7f7d4708ac..dca33964cf 100644
--- a/tests/snapshot_tests/test_snapshots.py
+++ b/tests/snapshot_tests/test_snapshots.py
@@ -2311,3 +2311,28 @@ def compose(self) -> ComposeResult:
yield Footer() # Not allowed
assert snap_compare(MaximizeApp(), press=["m"])
+
+
+def test_background_tint(snap_compare):
+ class BackgroundTintApp(App):
+ CSS = """
+ Vertical {
+ background: $panel;
+ }
+ #tint1 { background-tint: $foreground 0%; }
+ #tint2 { background-tint: $foreground 33%; }
+ #tint3 { background-tint: $foreground 66%; }
+ #tint4 { background-tint: $foreground 100% }
+ """
+
+ def compose(self) -> ComposeResult:
+ with Vertical(id="tint1"):
+ yield Label("0%")
+ with Vertical(id="tint2"):
+ yield Label("33%")
+ with Vertical(id="tint3"):
+ yield Label("66%")
+ with Vertical(id="tint4"):
+ yield Label("100%")
+
+ assert snap_compare(BackgroundTintApp())