From 50a34fde5b523987e924ec8650e6f75f0c187228 Mon Sep 17 00:00:00 2001 From: Benjamin Barrera-Altuna Date: Thu, 30 Apr 2026 18:25:14 -0400 Subject: [PATCH 1/4] fix: improve invalid color errors for literal color props Validate literal CSS color strings with clearer errors for color-like style keys and typed component color props (e.g. Var[str | Color]). Adds regressions for invalid style color values, valid color formats, and typed color prop rejection. --- .../src/reflex_base/components/component.py | 21 +- packages/reflex-base/src/reflex_base/style.py | 335 +++++++++++++++++- tests/units/components/test_component.py | 15 + tests/units/test_style.py | 20 ++ 4 files changed, 383 insertions(+), 8 deletions(-) diff --git a/packages/reflex-base/src/reflex_base/components/component.py b/packages/reflex-base/src/reflex_base/components/component.py index 8f47f447c7d..7e0ad6cbc95 100644 --- a/packages/reflex-base/src/reflex_base/components/component.py +++ b/packages/reflex-base/src/reflex_base/components/component.py @@ -35,6 +35,7 @@ MemoizationMode, PageNames, ) +from reflex_base.constants.colors import Color from reflex_base.constants.compiler import SpecialAttributes from reflex_base.constants.state import CAMEL_CASE_MEMO_MARKER from reflex_base.event import ( @@ -49,7 +50,7 @@ run_script, unwrap_var_annotation, ) -from reflex_base.style import Style, format_as_emotion +from reflex_base.style import Style, format_as_emotion, validate_literal_css_color_value from reflex_base.utils import console, format, imports, types from reflex_base.utils.imports import ImportDict, ImportVar, ParsedImportDict from reflex_base.vars import VarData @@ -885,18 +886,28 @@ def _post_init(self, *args, **kwargs): # Check whether the key is a component prop. if is_var: + field_type = types.get_field_type(type(self), key) + expected_var_type_args = typing.get_args(field_type) + expected_var_type = ( + expected_var_type_args[0] + if expected_var_type_args + else field_type + ) + if ( + isinstance(value, str) + and types.typehint_issubclass(Color, expected_var_type) + ): + validate_literal_css_color_value(key, value) try: kwargs[key] = LiteralVar.create(value) # Get the passed type and the var type. passed_type = kwargs[key]._var_type - expected_type = typing.get_args( - types.get_field_type(type(self), key) - )[0] + expected_type = expected_var_type except TypeError: # If it is not a valid var, check the base types. passed_type = type(value) - expected_type = types.get_field_type(type(self), key) + expected_type = field_type if not satisfies_type_hint(value, expected_type): value_name = value._js_expr if isinstance(value, Var) else value diff --git a/packages/reflex-base/src/reflex_base/style.py b/packages/reflex-base/src/reflex_base/style.py index d80d650297c..c6da31eb645 100644 --- a/packages/reflex-base/src/reflex_base/style.py +++ b/packages/reflex-base/src/reflex_base/style.py @@ -2,6 +2,7 @@ from __future__ import annotations +import re from collections.abc import Mapping from typing import Any, Literal @@ -93,6 +94,326 @@ def set_color_mode( "fontFamily": ("fontFamily", "--default-font-family"), } +_COLOR_STYLE_KEYS = frozenset({ + "accent_color", + "background_color", + "border_bottom_color", + "border_color", + "border_left_color", + "border_right_color", + "border_top_color", + "caret_color", + "color", + "column_rule_color", + "fill", + "flood_color", + "lighting_color", + "outline_color", + "stop_color", + "stroke", + "text_decoration_color", + "text_emphasis_color", +}) + +_CSS_COLOR_FUNCTION_PREFIXES = ( + "color(", + "color-contrast(", + "color-mix(", + "contrast-color(", + "device-cmyk(", + "hsl(", + "hsla(", + "hwb(", + "lab(", + "lch(", + "light-dark(", + "oklab(", + "oklch(", + "rgb(", + "rgba(", +) + +_CSS_COLOR_VALUE_PREFIXES = ( + "env(", + "var(", +) + +_CSS_COLOR_HEX_REGEX = re.compile( + r"^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$" +) + +_CSS_COLOR_KEYWORDS = frozenset({ + "accentcolor", + "accentcolortext", + "activetext", + "aliceblue", + "antiquewhite", + "aqua", + "aquamarine", + "azure", + "beige", + "bisque", + "black", + "blanchedalmond", + "blue", + "blueviolet", + "brown", + "burlywood", + "buttonborder", + "buttonface", + "buttontext", + "cadetblue", + "canvas", + "canvastext", + "chartreuse", + "chocolate", + "coral", + "cornflowerblue", + "cornsilk", + "crimson", + "currentcolor", + "cyan", + "darkblue", + "darkcyan", + "darkgoldenrod", + "darkgray", + "darkgreen", + "darkgrey", + "darkkhaki", + "darkmagenta", + "darkolivegreen", + "darkorange", + "darkorchid", + "darkred", + "darksalmon", + "darkseagreen", + "darkslateblue", + "darkslategray", + "darkslategrey", + "darkturquoise", + "darkviolet", + "deeppink", + "deepskyblue", + "dimgray", + "dimgrey", + "dodgerblue", + "field", + "fieldtext", + "firebrick", + "floralwhite", + "forestgreen", + "fuchsia", + "gainsboro", + "ghostwhite", + "gold", + "goldenrod", + "gray", + "graytext", + "green", + "greenyellow", + "grey", + "highlight", + "highlighttext", + "honeydew", + "hotpink", + "indianred", + "indigo", + "inherit", + "initial", + "ivory", + "khaki", + "lavender", + "lavenderblush", + "lawngreen", + "lemonchiffon", + "lightblue", + "lightcoral", + "lightcyan", + "lightgoldenrodyellow", + "lightgray", + "lightgreen", + "lightgrey", + "lightpink", + "lightsalmon", + "lightseagreen", + "lightskyblue", + "lightslategray", + "lightslategrey", + "lightsteelblue", + "lightyellow", + "lime", + "limegreen", + "linen", + "linktext", + "magenta", + "maroon", + "mark", + "marktext", + "mediumaquamarine", + "mediumblue", + "mediumorchid", + "mediumpurple", + "mediumseagreen", + "mediumslateblue", + "mediumspringgreen", + "mediumturquoise", + "mediumvioletred", + "midnightblue", + "mintcream", + "mistyrose", + "moccasin", + "navajowhite", + "navy", + "oldlace", + "olive", + "olivedrab", + "orange", + "orangered", + "orchid", + "palegoldenrod", + "palegreen", + "paleturquoise", + "palevioletred", + "papayawhip", + "peachpuff", + "peru", + "pink", + "plum", + "powderblue", + "purple", + "rebeccapurple", + "red", + "revert", + "revert-layer", + "rosybrown", + "royalblue", + "saddlebrown", + "salmon", + "sandybrown", + "seagreen", + "seashell", + "selecteditem", + "selecteditemtext", + "sienna", + "silver", + "skyblue", + "slateblue", + "slategray", + "slategrey", + "snow", + "springgreen", + "steelblue", + "tan", + "teal", + "thistle", + "tomato", + "transparent", + "turquoise", + "unset", + "violet", + "visitedtext", + "wheat", + "white", + "whitesmoke", + "yellow", + "yellowgreen", +}) + +_CSS_URL_COLOR_KEYS = frozenset({ + "fill", + "stroke", +}) + +_CSS_NONE_COLOR_KEYS = frozenset({ + "fill", + "stroke", +}) + +_CSS_CONTEXT_PAINT_KEYS = frozenset({ + "fill", + "stroke", +}) + + +def _is_color_style_key(style_key: str) -> bool: + """Check whether a style key expects a single CSS color value. + + Args: + style_key: The normalized style key. + + Returns: + Whether the style key expects a CSS color value. + """ + if style_key.startswith(("--", "&", "@", ":", "_")): + return False + return format.to_snake_case(style_key) in _COLOR_STYLE_KEYS + + +def _is_valid_css_color_value(style_key: str, style_value: str) -> bool: + """Check whether a literal string is a valid CSS color value. + + Args: + style_key: The normalized style key being validated. + style_value: The style value to validate. + + Returns: + Whether the value is accepted as a CSS color. + """ + if constants.REFLEX_VAR_OPENING_TAG in style_value: + return True + + normalized = style_value.strip() + if not normalized: + return False + + lower = normalized.lower() + if lower.endswith("!important"): + lower = lower[: -len("!important")].rstrip() + if not lower: + return False + + if lower == "none": + return format.to_snake_case(style_key) in _CSS_NONE_COLOR_KEYS + + if lower in ("context-fill", "context-stroke"): + return format.to_snake_case(style_key) in _CSS_CONTEXT_PAINT_KEYS + + if lower in _CSS_COLOR_KEYWORDS: + return True + if _CSS_COLOR_HEX_REGEX.fullmatch(lower): + return True + + if lower.startswith("url(") and lower.endswith(")"): + return format.to_snake_case(style_key) in _CSS_URL_COLOR_KEYS + + return lower.endswith(")") and any( + lower.startswith(prefix) + for prefix in _CSS_COLOR_FUNCTION_PREFIXES + _CSS_COLOR_VALUE_PREFIXES + ) + + +def validate_literal_css_color_value(style_key: str, style_value: str) -> None: + """Validate a literal string value for color-style keys. + + Args: + style_key: The style key being assigned. + style_value: The literal string value. + + Raises: + ValueError: If the value is not a recognized CSS color. + """ + if _is_color_style_key(style_key) and not _is_valid_css_color_value( + style_key, style_value + ): + msg = ( + f"Invalid value {style_value!r} for CSS color property {style_key!r}. " + "Expected a hex color, a CSS color function (for example rgb(), hsl(), " + "hwb(), lab(), oklch(), color(), or color-mix()), a CSS variable " + "(var(--...)), or a named CSS color. See " + "https://developer.mozilla.org/en-US/docs/Web/CSS/named-color." + ) + raise ValueError(msg) + def media_query(breakpoint_expr: str): """Create a media query selector. @@ -108,11 +429,13 @@ def media_query(breakpoint_expr: str): def convert_item( style_item: int | str | Var, + style_keys: tuple[str, ...] = (), ) -> tuple[str | Var, VarData | None]: """Format a single value in a style dictionary. Args: style_item: The style item to format. + style_keys: Candidate normalized style keys that use this value. Returns: The formatted style item and any associated VarData. @@ -132,6 +455,10 @@ def convert_item( if isinstance(style_item, Var): return style_item, style_item._get_all_var_data() + if isinstance(style_item, str): + for style_key in style_keys: + validate_literal_css_color_value(style_key, style_item) + # Otherwise, convert to Var to collapse VarData encoded in f-string. new_var = LiteralVar.create(style_item) var_data = new_var._get_all_var_data() if new_var is not None else None @@ -140,11 +467,13 @@ def convert_item( def convert_list( responsive_list: list[str | dict | Var], + style_keys: tuple[str, ...] = (), ) -> tuple[list[str | dict[str, Var | list | dict]], VarData | None]: """Format a responsive value list. Args: responsive_list: The raw responsive value list (one value per breakpoint). + style_keys: Candidate normalized style keys that use this value list. Returns: The recursively converted responsive value list and any associated VarData. @@ -156,7 +485,7 @@ def convert_list( # Recursively format nested style dictionaries. item, item_var_data = convert(responsive_item) else: - item, item_var_data = convert_item(responsive_item) + item, item_var_data = convert_item(responsive_item, style_keys) converted_value.append(item) item_var_datas.append(item_var_data) return converted_value, VarData.merge(*item_var_datas) @@ -208,10 +537,10 @@ def update_out_dict( update_out_dict(return_val, keys) elif isinstance(value, list): # Responsive value is a list of dict or value - return_val, new_var_data = convert_list(value) + return_val, new_var_data = convert_list(value, keys) update_out_dict(return_val, keys) else: - return_val, new_var_data = convert_item(value) + return_val, new_var_data = convert_item(value, keys) update_out_dict(return_val, keys) # Combine all the collected VarData instances. var_data = VarData.merge(var_data, new_var_data) diff --git a/tests/units/components/test_component.py b/tests/units/components/test_component.py index de397442311..80bf8560acf 100644 --- a/tests/units/components/test_component.py +++ b/tests/units/components/test_component.py @@ -624,6 +624,21 @@ def test_invalid_prop_type(component1, text: str, number: int): component1.create(text=text, number=number) +def test_invalid_typed_color_prop_raises_error(): + """Test that invalid literal color values are rejected on typed color props.""" + + class TypedColorPropComponent(Component): + stop_color: Var[str | rx.Color] + + with pytest.raises( + ValueError, + match=( + "Invalid value 'not-a-color' for CSS color property 'stop_color'" + ), + ): + TypedColorPropComponent.create(stop_color="not-a-color") + + def test_var_props(component1, test_state: type[TestState]): """Test that we can set a Var prop. diff --git a/tests/units/test_style.py b/tests/units/test_style.py index dab993d8644..e977765b872 100644 --- a/tests/units/test_style.py +++ b/tests/units/test_style.py @@ -519,3 +519,23 @@ def test_component_as_css_value_raises_error(): rx.el.div( style={"_hover": {"content": rx.text("hover")}}, ) + + +def test_invalid_literal_color_style_value_raises_error(): + """Test that invalid literal CSS color values raise a helpful error.""" + with pytest.raises( + ValueError, + match=( + "Invalid value 'not-a-color' for CSS color property 'color'" + ), + ): + Style({"color": "not-a-color"}) + + +def test_valid_literal_color_style_values_are_accepted(): + """Test that common valid literal CSS color values are accepted.""" + Style({"color": "rebeccapurple"}) + Style({"background_color": "rgb(34 12 64 / 0.6)"}) + Style({"border_color": "var(--accent-9) !important"}) + Style({"fill": "url(#gradient-id)"}) + Style({"stroke": "context-stroke"}) From 6d2c747245e738098c0fe12d31ad7436b260a17a Mon Sep 17 00:00:00 2001 From: Benjamin Barrera-Altuna Date: Thu, 30 Apr 2026 19:24:24 -0400 Subject: [PATCH 2/4] fix(types): avoid TypedDict subclass checks in color validation --- .../reflex-base/src/reflex_base/components/component.py | 9 +++------ packages/reflex-base/src/reflex_base/utils/types.py | 2 +- tests/units/components/test_component.py | 4 +--- tests/units/test_style.py | 4 +--- tests/units/utils/test_types.py | 5 +++++ 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/reflex-base/src/reflex_base/components/component.py b/packages/reflex-base/src/reflex_base/components/component.py index 319d1941b0d..08d65fac330 100644 --- a/packages/reflex-base/src/reflex_base/components/component.py +++ b/packages/reflex-base/src/reflex_base/components/component.py @@ -929,13 +929,10 @@ def _post_init(self, *args, **kwargs): field_type = types.get_field_type(type(self), key) expected_var_type_args = typing.get_args(field_type) expected_var_type = ( - expected_var_type_args[0] - if expected_var_type_args - else field_type + expected_var_type_args[0] if expected_var_type_args else field_type ) - if ( - isinstance(value, str) - and types.typehint_issubclass(Color, expected_var_type) + if isinstance(value, str) and types.typehint_issubclass( + Color, expected_var_type ): validate_literal_css_color_value(key, value) try: diff --git a/packages/reflex-base/src/reflex_base/utils/types.py b/packages/reflex-base/src/reflex_base/utils/types.py index feeb3b0da50..724cea0fa1c 100644 --- a/packages/reflex-base/src/reflex_base/utils/types.py +++ b/packages/reflex-base/src/reflex_base/utils/types.py @@ -993,7 +993,7 @@ def typehint_issubclass( if provided_type_origin is None and accepted_type_origin is None: # In this case, we are dealing with a non-generic type, so we can use issubclass - return issubclass(possible_subclass, possible_superclass) + return safe_issubclass(possible_subclass, possible_superclass) if treat_literals_as_union_of_types and is_literal(possible_superclass): args = get_args(possible_superclass) diff --git a/tests/units/components/test_component.py b/tests/units/components/test_component.py index fb325b36ff3..7ffd87c8bf8 100644 --- a/tests/units/components/test_component.py +++ b/tests/units/components/test_component.py @@ -627,9 +627,7 @@ class TypedColorPropComponent(Component): with pytest.raises( ValueError, - match=( - "Invalid value 'not-a-color' for CSS color property 'stop_color'" - ), + match=("Invalid value 'not-a-color' for CSS color property 'stop_color'"), ): TypedColorPropComponent.create(stop_color="not-a-color") diff --git a/tests/units/test_style.py b/tests/units/test_style.py index e977765b872..e8cca832406 100644 --- a/tests/units/test_style.py +++ b/tests/units/test_style.py @@ -525,9 +525,7 @@ def test_invalid_literal_color_style_value_raises_error(): """Test that invalid literal CSS color values raise a helpful error.""" with pytest.raises( ValueError, - match=( - "Invalid value 'not-a-color' for CSS color property 'color'" - ), + match=("Invalid value 'not-a-color' for CSS color property 'color'"), ): Style({"color": "not-a-color"}) diff --git a/tests/units/utils/test_types.py b/tests/units/utils/test_types.py index acdf326bf08..52725937d8d 100644 --- a/tests/units/utils/test_types.py +++ b/tests/units/utils/test_types.py @@ -100,6 +100,11 @@ class UserInfoTotal(TypedDict, total=True): email: str +def test_typehint_issubclass_with_typeddict_union_returns_false() -> None: + """TypedDict unions should return False instead of raising TypeError.""" + assert types.typehint_issubclass(int, str | UserInfo) is False + + @pytest.mark.parametrize( ("value", "cls", "expected"), [ From 9df8c1086a9602ea7bd4891a6650b8519c690c15 Mon Sep 17 00:00:00 2001 From: Benjamin Barrera-Altuna Date: Thu, 30 Apr 2026 19:43:15 -0400 Subject: [PATCH 3/4] fix(style): allow Reflex color tokens and tighten color checks --- .../src/reflex_base/components/component.py | 49 +++++++++++++++---- packages/reflex-base/src/reflex_base/style.py | 6 +++ tests/units/test_style.py | 1 + 3 files changed, 47 insertions(+), 9 deletions(-) diff --git a/packages/reflex-base/src/reflex_base/components/component.py b/packages/reflex-base/src/reflex_base/components/component.py index 08d65fac330..4260048bdeb 100644 --- a/packages/reflex-base/src/reflex_base/components/component.py +++ b/packages/reflex-base/src/reflex_base/components/component.py @@ -65,6 +65,34 @@ import reflex.state FIELD_TYPE = TypeVar("FIELD_TYPE") +_COLOR_VALIDATION_PROP_KEYS = frozenset({"fill", "stroke"}) + + +def _is_color_validation_candidate_prop(prop_name: str) -> bool: + """Check whether a prop name may carry color values. + + Args: + prop_name: The component prop name. + + Returns: + Whether color validation should be considered for this prop. + """ + prop_name = prop_name.lower() + return "color" in prop_name or prop_name in _COLOR_VALIDATION_PROP_KEYS + + +def _typehint_includes_color(type_hint: Any) -> bool: + """Check whether a type hint contains the Reflex Color type. + + Args: + type_hint: The type hint to inspect. + + Returns: + Whether the type hint contains Color. + """ + if type_hint is Color: + return True + return any(_typehint_includes_color(arg) for arg in typing.get_args(type_hint)) class ComponentField(BaseField[FIELD_TYPE]): @@ -924,6 +952,14 @@ def _post_init(self, *args, **kwargs): else: continue + if key in component_specific_triggers: + kwargs["event_triggers"][key] = EventChain.create( + value=value, + args_spec=component_specific_triggers[key], + key=key, + ) + continue + # Check whether the key is a component prop. if is_var: field_type = types.get_field_type(type(self), key) @@ -931,8 +967,10 @@ def _post_init(self, *args, **kwargs): expected_var_type = ( expected_var_type_args[0] if expected_var_type_args else field_type ) - if isinstance(value, str) and types.typehint_issubclass( - Color, expected_var_type + if ( + isinstance(value, str) + and _is_color_validation_candidate_prop(key) + and _typehint_includes_color(expected_var_type) ): validate_literal_css_color_value(key, value) try: @@ -959,13 +997,6 @@ def _post_init(self, *args, **kwargs): f"Invalid var passed for prop {type(self).__name__}.{key}, expected type {expected_type}, got value {value_name} of type {passed_type}." + additional_info ) - # Check if the key is an event trigger. - if key in component_specific_triggers: - kwargs["event_triggers"][key] = EventChain.create( - value=value, - args_spec=component_specific_triggers[key], - key=key, - ) # Remove any keys that were added as events. for key in kwargs["event_triggers"]: diff --git a/packages/reflex-base/src/reflex_base/style.py b/packages/reflex-base/src/reflex_base/style.py index 9cbbe1464e4..39f678b0446 100644 --- a/packages/reflex-base/src/reflex_base/style.py +++ b/packages/reflex-base/src/reflex_base/style.py @@ -141,6 +141,10 @@ def set_color_mode( _CSS_COLOR_HEX_REGEX = re.compile( r"^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$" ) +_REFLEX_COLOR_TOKEN_REGEX = re.compile( + r"^[a-z][a-z0-9_-]*\.[a-z0-9_-]+$", + flags=re.IGNORECASE, +) _CSS_COLOR_KEYWORDS = frozenset({ "accentcolor", @@ -375,6 +379,8 @@ def _is_valid_css_color_value(style_key: str, style_value: str) -> bool: return True if _CSS_COLOR_HEX_REGEX.fullmatch(lower): return True + if _REFLEX_COLOR_TOKEN_REGEX.fullmatch(lower): + return True if lower.startswith("url(") and lower.endswith(")"): return format.to_snake_case(style_key) in _CSS_URL_COLOR_KEYS diff --git a/tests/units/test_style.py b/tests/units/test_style.py index e8cca832406..db1c56a0262 100644 --- a/tests/units/test_style.py +++ b/tests/units/test_style.py @@ -533,6 +533,7 @@ def test_invalid_literal_color_style_value_raises_error(): def test_valid_literal_color_style_values_are_accepted(): """Test that common valid literal CSS color values are accepted.""" Style({"color": "rebeccapurple"}) + Style({"color": "gray.600"}) Style({"background_color": "rgb(34 12 64 / 0.6)"}) Style({"border_color": "var(--accent-9) !important"}) Style({"fill": "url(#gradient-id)"}) From 4a9b71801e97b51a14698facba2e7afc71b98cbb Mon Sep 17 00:00:00 2001 From: Benjamin Barrera-Altuna Date: Thu, 30 Apr 2026 20:35:01 -0400 Subject: [PATCH 4/4] fix(style): allow legacy none and border-color var shorthands --- packages/reflex-base/src/reflex_base/style.py | 20 ++++++++++++++++++- tests/units/test_style.py | 2 ++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/reflex-base/src/reflex_base/style.py b/packages/reflex-base/src/reflex_base/style.py index 39f678b0446..5ed239cf159 100644 --- a/packages/reflex-base/src/reflex_base/style.py +++ b/packages/reflex-base/src/reflex_base/style.py @@ -330,6 +330,13 @@ def set_color_mode( _CSS_URL_COLOR_KEYS = _SVG_PAINT_KEYS _CSS_NONE_COLOR_KEYS = _SVG_PAINT_KEYS _CSS_CONTEXT_PAINT_KEYS = _SVG_PAINT_KEYS +_CSS_COMPOSITE_COLOR_KEYS = frozenset({ + "border_color", + "border_top_color", + "border_right_color", + "border_bottom_color", + "border_left_color", +}) def _is_color_style_key(style_key: str) -> bool: @@ -370,7 +377,9 @@ def _is_valid_css_color_value(style_key: str, style_value: str) -> bool: return False if lower == "none": - return format.to_snake_case(style_key) in _CSS_NONE_COLOR_KEYS + # Compatibility: existing apps and docs frequently use "none" with color-like + # props such as backgroundColor. + return True if lower in ("context-fill", "context-stroke"): return format.to_snake_case(style_key) in _CSS_CONTEXT_PAINT_KEYS @@ -385,6 +394,15 @@ def _is_valid_css_color_value(style_key: str, style_value: str) -> bool: if lower.startswith("url(") and lower.endswith(")"): return format.to_snake_case(style_key) in _CSS_URL_COLOR_KEYS + snake_style_key = format.to_snake_case(style_key) + if ( + snake_style_key in _CSS_COMPOSITE_COLOR_KEYS + and any(ch.isspace() for ch in lower) + and "var(" in lower + ): + # Allow legacy shorthand-like border strings, e.g. "1px solid var(--grass-1)". + return True + # Intentional trade-off: check for well-known color-function prefixes # without implementing full CSS function parsing. return lower.endswith(")") and any( diff --git a/tests/units/test_style.py b/tests/units/test_style.py index db1c56a0262..9832b1348a6 100644 --- a/tests/units/test_style.py +++ b/tests/units/test_style.py @@ -534,7 +534,9 @@ def test_valid_literal_color_style_values_are_accepted(): """Test that common valid literal CSS color values are accepted.""" Style({"color": "rebeccapurple"}) Style({"color": "gray.600"}) + Style({"background_color": "none"}) Style({"background_color": "rgb(34 12 64 / 0.6)"}) Style({"border_color": "var(--accent-9) !important"}) + Style({"border_color": "1px solid var(--grass-1)"}) Style({"fill": "url(#gradient-id)"}) Style({"stroke": "context-stroke"})