diff --git a/CHANGELOG.md b/CHANGELOG.md
index d69a02dee..69653cba4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,6 +24,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Added `radio_set` attribute to `RadioSet` events https://github.com/Textualize/textual/pull/1940
- Added `switch` attribute to `Switch` events https://github.com/Textualize/textual/pull/1940
- Breaking change: Added `toggle_button` attribute to RadioButton and Checkbox events, replaces `input` https://github.com/Textualize/textual/pull/1940
+- A percentage alpha can now be applied to a border https://github.com/Textualize/textual/issues/1863
+- Added `Color.multiply_alpha`.
### Fixed
diff --git a/docs/styles/border.md b/docs/styles/border.md
index 2e370fc16..5e9322909 100644
--- a/docs/styles/border.md
+++ b/docs/styles/border.md
@@ -9,15 +9,15 @@ The `border` rule enables the drawing of a box around a widget.
## Syntax
--8<-- "docs/snippets/syntax_block_start.md"
-border: [<border>] [<color>];
+border: [<border>] [<color>] [<percentage>];
-border-top: [<border>] [<color>];
-border-right: [<border>] [<color>];
-border-bottom: [<border>] [<color>];
-border-left: [<border>] [<color>];
+border-top: [<border>] [<color>] [<percentage>];
+border-right: [<border>] [<color> [<percentage>]];
+border-bottom: [<border>] [<color> [<percentage>]];
+border-left: [<border>] [<color> [<percentage>]];
--8<-- "docs/snippets/syntax_block_end.md"
-The style `border` accepts an optional [``](../../css_types/border) that sets the visual style of the widget border and an optional [``](../../css_types/color) to set the color of the border.
+The style `border` accepts an optional [``](../../css_types/border) that sets the visual style of the widget border, an optional [``](../../css_types/color) to set the color of the border, and an optional [``](../../css_types/percentage) to specify the color transparency.
Borders may also be set individually for the four edges of a widget with the `border-top`, `border-right`, `border-bottom` and `border-left` rules.
@@ -41,6 +41,7 @@ The CSS snippet above will add a solid green border around `Static` widgets, exc
If `` is specified but `` is not, it defaults to `"solid"`.
If `` is specified but ``is not, it defaults to green (RGB color `"#00FF00"`).
+If `` is not specified it defaults to `100%`.
## Border command
@@ -126,8 +127,11 @@ This example also shows that a widget cannot contain both a `border` and an `out
/* Set a heavy white border */
border: heavy white;
-/* set a red border on the left */
+/* Set a red border on the left */
border-left: outer red;
+
+/* Set a rounded orange border, 50% transparency. */
+border: round orange 50%;
```
## Python
diff --git a/src/textual/color.py b/src/textual/color.py
index 663fff2cf..a5bde511a 100644
--- a/src/textual/color.py
+++ b/src/textual/color.py
@@ -341,6 +341,18 @@ class Color(NamedTuple):
r, g, b, _ = self
return Color(r, g, b, alpha)
+ def multiply_alpha(self, alpha: float) -> Color:
+ """Create a new color, multiplying the alpha with a new alpha.
+
+ Args:
+ alpha: The alpha value to multiple by.
+
+ Returns:
+ A new color.
+ """
+ r, g, b, a = self
+ return Color(r, g, b, a * alpha)
+
def blend(
self, destination: Color, factor: float, alpha: float | None = None
) -> Color:
diff --git a/src/textual/css/_styles_builder.py b/src/textual/css/_styles_builder.py
index 26b67b07c..b98cd8c04 100644
--- a/src/textual/css/_styles_builder.py
+++ b/src/textual/css/_styles_builder.py
@@ -441,6 +441,7 @@ class StylesBuilder:
def _parse_border(self, name: str, tokens: list[Token]) -> BorderValue:
border_type: EdgeType = "solid"
border_color = Color(0, 255, 0)
+ border_alpha: float | None = None
def border_value_error():
self.error(name, token, border_property_help_text(name, context="css"))
@@ -462,9 +463,18 @@ class StylesBuilder:
except ColorParseError:
border_value_error()
+ elif token_name == "scalar":
+ alpha_scalar = Scalar.parse(token.value)
+ if alpha_scalar.unit != Unit.PERCENT:
+ self.error(name, token, "alpha must be given as a percentage.")
+ border_alpha = alpha_scalar.value / 100.0
+
else:
border_value_error()
+ if border_alpha is not None:
+ border_color = border_color.multiply_alpha(border_alpha)
+
return normalize_border_value((border_type, border_color))
def _process_border_edge(self, edge: str, name: str, tokens: list[Token]) -> None:
@@ -612,7 +622,7 @@ class StylesBuilder:
if color is not None or alpha is not None:
if alpha is not None:
- color = (color or Color(255, 255, 255)).with_alpha(alpha)
+ color = (color or Color(255, 255, 255)).multiply_alpha(alpha)
self.styles._rules[name] = color # type: ignore
process_tint = process_color
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
index ac2a53ff3..4c6089d71 100644
--- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
@@ -158,6 +158,169 @@
'''
# ---
+# name: test_border_alpha
+ '''
+
+
+ '''
+# ---
# name: test_buttons_render
'''