Skip to content

Support two-position gradient color stops - #199

Merged
TylerBrinks merged 1 commit into
TylerBrinks:masterfrom
jhaygood86:bugfix/two-position-gradient-color-stops
Jul 23, 2026
Merged

Support two-position gradient color stops#199
TylerBrinks merged 1 commit into
TylerBrinks:masterfrom
jhaygood86:bugfix/two-position-gradient-color-stops

Conversation

@jhaygood86

Copy link
Copy Markdown
Contributor

Problem

A gradient color stop with two positions is rejected, taking the whole gradient with it:

linear-gradient(red 0 50%, blue 50% 100%)   /* dropped */
linear-gradient(90deg, red 0 8px, blue)     /* dropped */
radial-gradient(red 0 8px, blue)            /* dropped */

CSS Images 4 §3.5.1 defines a stop's length as:

<linear-color-stop> = <color> <color-stop-length>?
<color-stop-length> = <length-percentage>{1,2}

with the note that two positions is "equivalent to specifying two color stops with the same color, one for each position" — a convenient way to make solid bands.

GradientConverter.ToGradientStop consumes only a single trailing <length-percentage>, so a stop written with two leaves a leftover token, ToGradientStop returns null, and ToGradientStops fails the entire value.

Fix

Parse a second position when one is present, and keep both in source order so the stop round-trips as authored — red 0 50% serializes back as red 0 50%, not red 50%. StopValue now holds an optional second position and joins whatever is present.

Guards from the grammar:

  • the two-position form requires a color — a bare <length-percentage> is a single-position <linear-color-hint>, so 0 50% with no color stays invalid
  • three positions (red 0 25% 50%) stay invalid

The radial case needed no separate change. ConvertFirstArgument already returns null for a comma group that begins with a <color>, so red 0 8px was always routed to the first stop — it simply couldn't be parsed until now. Confirmed by radial-gradient(red 0 8px, blue) passing with only this change.

Tests

9 cases in Gradient.cs: three two-position gradients (linear, linear-with-angle, radial) asserting the exact serialized value; three single-position/no-position/hint cases that must stay legal; and two malformed cases (three positions, two bare positions) that must stay rejected.

3 fail on master. The full suite (1263 existing tests) stays green with no existing assertion modified, and all seven target frameworks build with no new warnings.

CSS Images 4 defines a color stop's length as
<color-stop-length> = <length-percentage>{1,2}, so a stop may carry two
positions - "red 0 50%", equivalent to two same-color stops, one at each
position. ToGradientStop consumed only a single trailing position, so
any stop with two was rejected and the whole gradient failed:

  linear-gradient(red 0 50%, blue 50% 100%)   /* dropped */
  radial-gradient(red 0 8px, blue)            /* dropped */

Parse a second position when one is present and keep both, in source
order, so the stop round-trips as authored rather than losing a
position. The two-position form requires a color (a bare
<length-percentage> is a single-position <linear-color-hint>), and three
positions remain invalid.

The radial case needed no separate change: ConvertFirstArgument already
returns null for a leading color, so "red 0 8px" was always treated as
the first stop - it just could not be parsed until now.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants